gcc内嵌汇编介绍

时间:2022-01-14 14:59:50

阅读AndroidLinux的源码时,有时会遇到使用内嵌汇编的代码。阅读内嵌汇编代码不是一件特别容易的事,如果只了解普通汇编语言,没学习过内嵌汇编,从语言上大概能明白内嵌汇编代码的作用,但是要精确的了解每行代码,每个寄存器的含义就不太可能了。内嵌汇编其实并不复杂,只不过gcc的内嵌汇编必须是AT&T格式,而且有自己一套独特的标记,加上资料很少,有限的资料写的又很晦涩,所以学习起来比较困难。本文争取用简短的篇幅介绍明白内嵌汇编的规则和作用。如果对AT&T汇编格式还不了解,请先参考前一篇博文:AT&T汇编格式介绍。

 

一、简单内嵌汇编格式

内嵌汇编是用来和C语言混合编程的,所以要有方法把它的语句和C语句隔开,下面看一个简单的例子:

__asm__ __volatile__ ("movl $5, %eax");

内嵌汇编语句必须放在__asm__开头,并且加上括号和引号。__volatile__修饰符不是必须的,它的作用是告诉编译器不要调整我们写的汇编语句(通常编译器会因为优化的原因这么做)。

也可以把多条汇编语句放在一个__asm__中,例如:

__asm__ __volatile__ ("movl $5, % eax; movl $5, %ebx ");

汇编语句之间要使用分号‘;’隔开。除了用分号‘;’作为语句间的分割符,还可以用"\n"来作为分割符。本人也试过“\n\r,或者"\n\t"也能编译通过。如果看到有些内嵌汇编语句使用它们作为分隔符,不要奇怪。

其实,最简单的内嵌汇编就这点内容,把汇编语句放到__asm__块中就完了。这种简单的内嵌汇编用处并不是很大(当然我们可以用它来调用一些特殊的CPU指令),主要是因为它不能和C代码交流,只是孤零零的汇编代码用处有限。

 

二、复杂的内嵌汇编格式

如何才能让汇编代码要和C语句交流呢?答案很简单,就是让汇编中能使用和改变C语言中的变量。因此内嵌汇编需要用一种方式来表示这种意图。首先,我们需要告诉编译器我们要在哪个地方使用变量,因此内嵌汇编用“%0到“%910个符号来代替汇编语句中的操作数,同时把c的表达式放在汇编语句后面的括号中,例如:

int kk = 0

__asm__ __volatile__ ("movl %0, %%eax":"=m"(kk));   //把变量kk的值放到eax寄存器中

上面汇编语句中的“%0又被称为占位符,09的数值表示后面变量的位置。例如下面的例子中有两个变量:

int kk = 0mm

__asm__ __volatile__ ("movl %0, %% eax; movl %%eax, %1":"=m"(kk),"=m"(mm));

上面的例子中%0代表变量kk%1代表变量mm。可能大家已经注意到上面的例子中继承器前面已经多了一个‘%号,这也是内嵌汇编的要求,为了和占位符区别,使用两个百分号来表示寄存器。

汇编语句和后面的变量之间用‘:’隔开。整个语句中有无分割符‘:’正是区别简单内嵌汇编和复杂内嵌汇编的标志。而且整个语句中的分隔符‘:’可以多达三个,这三个‘:’把整个语句分成四段,分别是模板指令域,output域,input域和Clobber/Modify域。后面的三个不必都有,可以空着。例如:

__asm__ __volatile__ ("movl %0, %%eax"::"m"(kk));

__asm__ __volatile__ ("movl %0, %%eax":::"memory");

这些“域”都是做什么的呢?第一个当然很容易理解,就是带有占位符的汇编语句。第四个最后再解释。中间的两个output域和input域都是用来放C表达式的,这里的所谓表达式可能是变量,也可是一个数。为什么要两个域呢?因为我们对变量有两种操作,读值或存值。如果一个变量被放到了output域,在汇编语句结束后编译器会插入给变量赋值的指令,而放到input域的变量则不会,所以这里需要两个域把它们分开。例如:

int kk = 0

__asm__ __volatile__ ("movl $5, %%eax":"=a"(kk));

上面的例子中汇编代码的功能只是把立即数5放到了寄存器eax中,可是运行的结果是变量kk的值也变成了5。这就是因为变量kk放在了output域,编译器自动把eax中的值赋给了kk。这样,新的问题又来了,编译器是如何知道要把eax的值赋给了kk,难道是因为汇编指令中只用到了eax?当然不是这个原因,其实是我们自己指定的。上面的例子中所有变量的前面都带有一个字符串,这个字符串称为限定符,我们通过限定符来告诉编译器我们要使用哪个寄存器。上面例子中的字母‘a’就是寄存器eax的缩写。其实不仅仅是eax,根据后面变量的类型,编译器会把字母‘a’解释为al/ax/eax/rax中的一种。常用的几种寄存器的缩写如下:

a代表al/ax/eax/rax寄存器

b代表bl/bx/ebx/rbx寄存器

c代表cl/cx/ecx/rcx寄存器

d代表dl/dx/edx/rdx寄存器

D代表di寄存器

S代表si寄存器

如果我们不打算指定一个固定的寄存器,可以使用缩写‘q’或‘r’。

如果希望使用浮点寄存器,可以使用缩写‘f’。而缩写‘t’和‘u'分别表示要使用第一浮点寄存器和第二浮点寄存器。

在什么情况下我们会使用‘q’或‘r’来让编译器给我们选择一个寄存器呢?前面的output域的例子肯定不行,这个例子中我们必须很明白的告诉编译器我们希望最后把哪个寄存器的值赋给输出变量kk。我们要考虑另外一种情况,在汇编语句中,地址表达式的使用是受限的,x86中规定了汇编语句的两个操作数至少有一个是寄存器。因此我们会先把地址表达式的值放到一个寄存器中,然后再使用它。寄存器限定符的另一个作用就是告诉编译器将变量的值放到哪个寄存器后再使用。这时缩写‘q’或‘r’就能派上用场。正因为寄存器限定符的第二个作用,所以input域中的变量前面同样也要使用限定符。

寄存器限定符解决了变量使用寄存器的问题,但是有一个副作用,使用限定符会让编译器产生将变量值放进寄存器的指令,这条指令对于需要在汇编语句中使用变量的情况是有用的,但是如果只需要改变一个变量的值而不需要在汇编代码中用到它,那么这条指令就多余了。解决的办法是再加一个=’号限定符。这就是前面例子中经常出现的‘=’号的作用。一旦使用了‘=’号,寄存器限定符的第二个作用就消失了。假如我们两个作用都需要呢?答案是使用‘+’号。不管是‘=’或‘+’号,它们都只能用在output域的变量前,input域的变量前是不能加上‘=’或‘+’号的,因为input域变量的作用就是输入。output域的变量前必须使用‘=’或者“+”号,否则编译不通过。看一个例子。

__asm__ __volatile__ ("movl %0, %%ebx":"=a"(kk));

上面这个例子编译可以通过。表面上看它的功能是把变量kk的值放到了寄存器ebx中了,实际上这条汇编直接被编译器扔掉了。原因就是变量kk的前面已经使用了‘=’限定符,它不能做为输入用在汇编语句中了。如果一定要用,必须使用限定符‘+’,例如:

__asm__ __volatile__ ("movl %0, %%ebx":"+a"(kk));

到这里,内嵌汇编的基本原理和语法规则就完了。其实也就这点东西,用一些符号来告诉编译器如何使用变量而已。

 

三、其余的限定符

但是还没有结束,我们还需要学习另外的一些限定符,这些限定符用来处理一些特殊的情况。

1)      如果我们使用变量的时候不希望通过寄存器中转,希望直接使用。这时可以使用限定符‘m’,例如:

__asm__ __volatile__ ("movl %0, %%ebx"::"m"(kk));

这个例子产生的汇编指令会直接使用变量kk作为第一操作数。请注意,上面的例子中使用了两个‘:’分隔符,说明变量kk放在了intput域。我们这里的例子需要变量kk作为输入,放在input域当然没问题。但是限定符‘m’同样也可以放到output域的变量前,但是一旦这样用来,会导致output域的变量失去输出作用,不管这时变量的前面使用了‘=’还是‘+’号。

__asm__ __volatile__ ("movl %0, %%ebx":"=m"(kk));

前面我们讲过,如果output域的变量前加上‘=’是不能用在汇编语句中的,但是使用这个‘m’限定符后就可以了,所以上面的汇编语句会把变量kk的值赋给寄存器ebx,但是结束后编译器将不会自动给变量kk赋值了。这是使用限定符‘m’需要注意的。

2)        我们前面的例子都很简单,只使用了output域或者input域。如果两者同时使用,会有一种冲突情况:我们知道input域的变量前可以使用通用寄存器限定符‘q’或‘r’来让编译器来挑选寄存器。如果编译器挑选的寄存器是我们在output域中指定的寄存器,有可能产生冲突,例如:

__asm__ __volatile__ ("movl %0, %%ebx":"+a"(kk):"r"(mm));

上面这个例子中,如果变量mm的寄存器也被选定为寄存器eax,逻辑上有可能会和变量kk使用的eax发生冲突,导致最后的结果出错。解决的办法是使用限定符‘&’告诉编译器不要挑选eax,例如上例改成下面这样就可以了:

__asm__ __volatile__ ("movl %0, %%ebx":"+&a"(kk):"r"(mm));

3)        一种简单的情况是input域使用的不是变量,而是立即数(output域是不能出现立即数的)。需要使用‘i’限定符告诉编译器后面括号中的是整数,或者使用‘f’限定符告诉编译器后面括号中的是浮点数。

__asm__ __volatile__ ("movl %0, %%ebx"::"i"(5));

4)        最后一种限定符是数字09,只能用在input域中,表示和前面的第noutput域的变量使用相同的寄存器。例如:

__asm__ __volatile__ ("movl %0, %%ebx":"+a"(kk):"0"(mm));

 

四、Clobber/Modify

最后,我们看看第四个域:Clobber/Modify域。

1)       如果我们在内嵌汇编的语句中使用了某些寄存器,但是这些寄存器可能之前已经被使用了,为了在内嵌汇编语句结束后这些寄存器的值保持原样,可以把它们的缩写放在Clobber/Modify域。这样编译器将自动产生pushpop指令来保存这些寄存器的值。例如:

__asm__ ("movl %0, %% ebx" : : "a"(kk) :"bx");

上面的例子中汇编语句中使用了寄存器ebx,因为担心破坏以前的值,在Clobber/Modify域加上了缩写“bx”。

一旦在Clobber/Modify域中指定了某个寄存器,这个寄存器将不能再用在inputoutput域的限定符中,否则编译通不过。

Clobber/Modify域寄存器的缩写和代表的寄存器如下:

ax代表al/ax/eax/rax寄存器

bx代表bl/bx/ebx/rbx寄存器

cx代表cl/cx/ecx/rcx寄存器

dx代表dl/dx/edx/rdx寄存器

di代表di寄存器

si代表si寄存器

2)       Clobber/Modify域中还可以使用“memroy”,例如:

__asm__ ("movl %0, %% ebx" :: "a"(kk):"memory");

memory在这里的作用是“编译优化屏障”。目的是防止编译器把前面的代码和内嵌汇编中的代码一起优化。

3)       如果内嵌汇编中的语句会影响条件寄存器中的值,比如z标识,溢出,进位标志等,需要在Clobber/Modify域中使用“cc”向编译器声明。

 

以上介绍是以x86为基础进行的,如果是arm体系,语法规则是一样的,只不过寄存器名不一样了。