目录
GCC汇编器语法
基本内联
扩展汇编
汇编模板
操作数
clobber list
Volatile...?
更多限制
常用限制
限制修饰符
Some Useful Recipes.
linux的GUN C 编译器,使用的是AT&T汇编语法。
+------------------------------+------------------------------------+
| Intel Code | AT&T Code |
+------------------------------+------------------------------------+
| mov eax,1 | movl $1,%eax |
| mov ebx,0ffh | movl $0xff,%ebx |
| int 80h | int $0x80 |
| mov ebx, eax | movl %eax, %ebx |
| mov eax,[ecx] | movl (%ecx),%eax |
| mov eax,[ebx+3] | movl 3(%ebx),%eax |
| mov eax,[ebx+20h] | movl 0x20(%ebx),%eax |
| add eax,[ebx+ecx*2h] | addl (%ebx,%ecx,0x2),%eax |
| lea eax,[ebx+ecx] | leal (%ebx,%ecx),%eax |
| sub eax,[ebx+ecx*4h-20h] | subl -0x20(%ebx,%ecx,0x4),%eax |
+------------------------------+------------------------------------+
基本内联汇编的格式非常简单。它的基本形式是
asm("assembly code"); 例子: asm("movl %ecx %eax"); /* moves the contents of ecx to eax */ __asm__("movb %bh (%eax)"); /*moves the byte from bh to the memory pointed by eax */
上面用到asm和__asm__,两个都是有效的。当关键字asm在程序中有冲突的时候,可以使用__asm__。如果有多个指令,每行写一个双引号,并在指令后添加"\n\t"。这是因为gcc将每条指令作为字符串发送给as(GAS),通过换行符/制表符区分来发送正确格式化字符给汇编器。
例子:
__asm__ ("movl %eax, %ebx\n\t" "movl $56, %esi\n\t" "movl %ecx, $label(%edx,%ebx,$4)\n\t" "movb %ah, (%ebx)");
如果我们的代码里使用了寄存器, 并且在返回的时候没有还原它, 这将有坏的情况发生. 因为GCC并不知道寄存器的值改变了, 特别是编译器对代码进行优化的时候. 编译器会认为,那些存放变量的寄存器,我们并没有改变它,然后继续自己的优化. 为了避免这种情况, 要么, 我们不改变寄存器的值, 要么, 汇编函数返回之前, 还原寄存器使用前的值, 或者 等着代码崩溃(wait for something to crash). 正是由于存在这样的问题,我们需要使用"Extended Asm". 它将提供给我们扩展功能, 解决上边的问题.
在基本嵌入汇编格式中,我们只使用了指令. 在扩展汇编中, 我们还可以指定更多操作. 它允许我们指定输入寄存器, 输出寄存器和变化表(clobber list). 我们并不一定要指定使用哪些寄存器. 我们可以把这件头痛的事情交给GCC去做. 扩展汇编的格式如下:
asm ( assembler template : output operands /* optional */ : input operands /* optional */ : list of clobbered registers /* optional */ );
这个模板由若干条汇编指令组成, 每个操作数(括号里C语言的变量)都有一个限制符(“”中的内容)加以描述. 冒号用来分割输入的操作和输出的操作. 如果每组内有多个操作数,用逗号分割它们. 操作数最多为10个, 或者依照具体机器而异 .
如果没有输出操作, 但是又有输入, 你必须使用连续两个冒号, 两个连续冒号中无内容, 表示没有输出结果的数据操作 .
例子:
asm ("cld\n\t" "rep\n\t" "stosl" : /* no output registers */ : "c" (count), "a" (fill_value), "D" (dest) : "%ecx", "%edi" );
上面这段代码做了什么? 这段内嵌汇编把 fill_value, count装入寄存器,同时告知GCC,clobber list目录中的寄存器eax,edi,已经改变.
看一个更详细的例子:
int a=10, b; asm ("movl %1, %%eax; movl %%eax, %0;" :"=r"(b) /* output */ :"r"(a) /* input */ :"%eax" /* clobbered register */ );
代码目的是让'b'的值与'a'的值相等.
当这段汇编代码执行完毕,'b'变量将会存储这个结果,,正如例子里声明这个变量为输出。 换句话说, 'b'用来反映汇编程序里值的变化。
这个汇编模板包含一套完整的汇编指令,帮助在c语言内嵌入汇编语言。具体格式如下:每条指令应该加上双括号,或者给整套汇编指令加上双括号(如,最后一个例子)。每条指令结尾都应加上结束符,合法的结束符有(\n)和(;),或许还应该在 \n后边加上一个 \t,我们应该了解原因吧? 括号里的若干操作数,依次对应%0,%1...等。
c表达式作为asm中汇编指令的操作数,每个操作数首先加上双引号。对于输出操作数,首先会有一个限制符,然后跟上C变量,运算结果将存入这个变量。
双引号内的“限制符”是一个规定的格式。在输出操作中,这个限制符会额外多一个符号(=)。限制符主要用来决定操作数的寻址方式。同时还可指定使用某一个寄存器。
如果我们使用多个操作数,它们之间用逗号分隔。
在汇编器模板中,每个操作数都由数字与之对应。编号如下进行。如果总共有 n 个操作数(包括输入和输出),则第一个输出操作数编号为 0,按递增顺序继续,最后一个输入操作数编号为 n-1。操作数的最大数量与我们在上一节中看到的一样。
输出操作数表达式必须是左值。输入操作数不受这样的限制。它们可能是表达式。扩展汇编常常用于实现机器平台自身特殊的指令,编译器可能并不能识别他们:-)。如果输出表达式不能直接寻址(例如,它是一个位字段),我们的约束必须允许一个寄存器。在这种情况下,GCC 将使用寄存器作为 asm 的输出,然后将该寄存器内容存储到输出中。
我们现在分析几个例子。我们想给一个数乘以5。因此,我们使用lea指令: (汇编语句leal(r1,r2,4),r3语句表示r1+r2*4→r3。这个例子可以非常快地将x乘5。)
asm ("leal (%1,%1,4), %0" : "=r" (five_times_x) : "r" (x) );
这里,输入一个变量x,我们并没指定特定的寄存器来存储它,GCC会选择一个(“r”表示gcc选择)。如我们所要求的,gcc会自动选择两个寄存器,一个给input,一个给output。如果我们想给input和output指定同一个寄存器,我们可以要求GCC这样做(通过更改“限制符”内容)。
asm ("leal (%0,%0,4), %0" : "=r" (five_times_x) : "0" (x) );
上例,我们就让input和output使用同一个寄存器,但是不知道具体哪一个。(如果输入操作的限制符为0或为空,则说明使用与相应输出一样的寄存器。)如果,我们想指定使用具体一个寄存器,可以看看如下代码:“c”表示使用寄存器ecx。
asm ("leal (%%ecx,%%ecx,4), %%ecx" : "=c" (x) : "c" (x) );
上面三个例子,我们没有把任何寄存器放入clobber list,为什么?前两个例子,由GCC选择寄存器,所以它知道那些寄存器值改变了。最后一个例子,我们没有把ecx寄存器放入clobber list,GCC知道它的值变成x了。因此,既然GCC知道ecx寄存器的值,就没必要加入到clobber list。
一些指令改变了硬件寄存器的值。这是需要在clobber list中列举出这些寄存器,位置在asm函数中第三个':'后。这是为了告知GCC,我们将使用和更改列举出的寄存器。那么,GCC就知道之前装载到寄存器里的值已经无效了,不会使用寄存器的旧值进行错误操作。我们不必把input,output所使用的寄存器列入clobber list,因为GCC知道汇编代码已经使用和改变了那些寄存器。
如果汇编代码将改变条件码寄存器,我们需要在clobber list中加入“cc”。
如果我们的指令以一种不可预知的方式修改了内存,需在clobber list中加入“memory”。这将使GCC在整个汇编指令的寄存器中不保留缓存值。如果受影响的内存没有列在asm的输入或输出中,我们还必须添加volatile关键字。
clobber list中的寄存器可以反复读写。参考下面这个例子,代码子程序__foo用eax,ecx寄存器传递参数。则这俩寄存器的值不再可靠,所以加入到clobber list中。
asm ("movl %0,%%eax; movl %1,%%ecx; call _foo" : /* no outputs */ : "g" (from), "g" (to) : "eax", "ecx" ); // from和to是任意寄存器
如果你熟悉内核代码或者像她一样漂亮的代码,你一定见到过许多函数被volatile或__volatile__修饰,通常紧跟在 asm或__asm__后边。我先前提到过asm和__asm__的区别。那volatile呢?
如果你不希望自己编写的汇编代码被gcc优化或移动,你需要使用到volatile这个keyword,将其放在asm和()之间即可。
对volatile关键字的使用应非常谨慎。
如果我汇编代码仅仅做一些简单计算并且没有什么副作用,那么最好不用volatile。不使用它,可以帮助GCC优化代码,让代码更“漂亮”。
看到这里,你应该知道汇编里的限制符(constraint)做了很多的事。但,我们只花了很少的篇幅叙述限制符。比如,限制符可以指定一个寄存器,限制符可以指向一块内存空间,限制符可以是一个立即数...等。
有大量的限制符,我们常用使用其中很少一部份,现在来看看:
其他一些限制:
x86特定限制符:
在使用限制符,为了更精确的控制限制的影响,GCC提供给我们一些限制语句修饰符。最常用的修饰符有:"=","&"。
现在我们已经接触GCC内联汇编的基本理论,我应该专注于几个简单的例子。使用内联汇编来定义宏是非常精妙的,我们可以在内核代码中看到许多asm函数。
1.首先以一个简单的开始,两个数相加。
int main(void) { int foo = 10, bar = 15; __asm__ __volatile__("addl %%ebx, %%eax" :"=a"(foo) // output :"a"(foo), "b"(bar) // input ); printf("foo+bar=%d\n",foo); return 0; }
这里我们指定gcc让eax寄存器存储foo,bar存放在ebx,并且将结果放在eax寄存器中。这里的"="表示是存放输出结果的寄存器。
接下来,我们可以用一些其他方式来添加一个整数。
__asm__ __volatile__( " lock ;\n" " addl %1,%0 ;\n" :"=m" (my_var) : "ir" (my_int), "m" (my_var) : );
这是一个原子加法。可以去除"lock"指令来去除原子性。在输出域,"=m"的意思是my_var是输出操作数,并且是在内存中。相似的,"ir"的意思是my_int是一个整数,可以选择一个通用寄存器来存放。没有寄存器在clobber list中。
2.现在我们执行一些操作在寄存器/变量上,并且比较它们的值。
__asm__ __volatile__( "decl %0; sete %1" : "=m"(my_var), "=q" (cond) : "m" (my_var) : "memory" );
这里,my_var的值自减一,如果结果为0,则cond置1。可以通过增加指令"lock;\n\t"来确保原子性。
类似的方式,我们可以使用“incl %0”而不是“decl %0”,这个是增加 my_var。
要点:1.my_var值的变化是直接在内存中。2.cond的限制是"=q",所以它使用的寄存器是eax,ebx,ecx,edx中的一个。3.可以看到"memory"在clobber list中,即,代码改变的内容都在内存中。
3.如何设置/清除寄存器的一位?请看下面的例子:
__asm__ __volatile__( "btsl %1, %0" : "=m" (ADDR) : "Ir" (pos) : "cc" );
将'pos'的ADDR(内存变量)变量的比特位置1。我们可以使用'blrl'代替'btsl'来清理这个比特位。pos的限制"Ir"是说pos位于寄存器中,值的范围0-31位。即,我们可以设置/清除任何0-31比特位的值在ADDR中。因为条件代码将会改变,所以我们增加"cc"到clobberlist。
4.现在我看几个复杂但是有用的函数,字符串拷贝。
static inline char *strcpy(char *dest, const char *src) { int d0, d1, d2; __asm__ volatile__( "1:\tlodsb\n\t" //加载DS:[esi]处1字节->al,并更新esi "stosb\n\t" //存储字节al->ES:[edi],并更新edi "testb %%al, %%al\n\t" //刚存储的字节是0? "jne 1b" : "=&S" (d0), "=&D" (d1), "=&a" (d2) //output "S":esi "D":edi : "0"(src), "1" (dest) // input :"memory"); return dest; }
源地址存放在esi中,目标在edi中,然后开始拷贝,当到达0,拷贝完成。限制符"&S","&D","&a"是说寄存器esi,edi和eax,并且它们是早期改变的寄存器,即,它们的内容在函数结束之前会有改变。所以clobberlist同样为"memory"。
接下来看一个相似的函数,移动2个字节的块。注意,它以宏的方式实现。
#define mov_blk(src, dest, numwords) \ __asm__ __volatile__ ( \ "cld\n\t" \ "rep\n\t" \ "movsl" \ : \ : "S" (src), "D" (dest), "c" (numwords) \ : "%ecx", "%esi", "%edi" \ )
这里没有输出,寄存器ecx,esi和edi的内容发生变化是因为块的移动的原因。所以需要将它们添加到colbberlist中。
5.在Linux中,系统调用是使用GCC内联汇编实现的。接下来我们看看系统调用是如何实现的。所有的系统都写成宏(linux/unistd.h)。举例,具有三个参数的系统调用被定义为如下所示的宏。
#define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \ type name(type1 arg1,type2 arg2,type3 arg3) \ { \ long __res; \ __asm__ volatile ( "int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \ "d" ((long)(arg3))); \ __syscall_return(type,__res); \ }
在每次进行带有三个参数的系统调用时,都会使用上面显示的宏进行调用。系统调用名称放在 eax 中,参数放在 ebx、ecx、edx 中。 最后“int 0x80”是使系统调用工作的指令。 返回值存入 eax 。
每个系统调用都以类似的方式实现。 Exit 是一个单参数系统调用,让我们看看它的代码是什么样子的。 如下:
{ asm("movl $1,%%eax; /* SYS_exit is 1 */ xorl %%ebx,%%ebx; /* Argument is in ebx, it is 0 */ int $0x80" /* Enter kernel mode */ ); }
exit的编号是1,参数是0。所以将eax存放1,ebx存放0,然后执行指令"int $0x80"。这就是exit的工作方式。