GCC内联汇编
常规的函数调用在调用时会有压栈的行为。假如我们想引导编译器将一段函数代码插入到调用者调用的位置处执行,而不是以默认压栈调用的方式,常规函数调用就无法满足我们的需求了,于是引入了内联函数。内联函数减少了函数的调用开销:如果多次被调用的某个函数实参相同,则其返回值必然是相同的,编译器可利用此特性对程序进行优化,而内联汇编相当于用汇编语句写成的内联函数,具有方便、快速的特点,在系统编程中广泛使用。
GCC内联汇编格式
GCC (GNU Compiler for Linux) 使用AT&T/UNIX汇编语法,与Intel格式的汇编有一些不同,差别如下所示:
1.源/目的操作数顺序
AT&T和Intel汇编语法源操作数和目的操作数的方向正好相反。Intel中第一个操作数作为目的操作数,第二个操作数作为源操作数。而在AT&T中,第一个操作数是源操作数,第二个是目的操作数。
1 | AT&T: movl %eax, %ebx Intel: mov ebx, eax |
2.寄存器命名
1 | AT&T: %eax Intel: eax |
3.常数、立即数的格式
在AT&T语法中,立即数都有’$’前缀。引用的C语言静态变量也必须放上’$’前缀;除此之外,在Intel语法中, 16进制的常数是以’h’作为后缀的,但是在AT&T语法中, 是以’0x’作为前缀的。因此,在AT&T语法中,一个16进制常数的写法是:首先以$开头,紧跟着是0x,最后是常数本身。
1 | AT&T: movl $_value, %ebx Intel: mov eax, _value |
4.寻址方式
1 | AT&T: immed32(basepointer, indexpointer, indexscale) |
5.操作数长度标识
1 | AT&T: movw %ax, %bx Intel: mov bx, ax |
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 |
基本内联汇编
基本内联汇编如下所示:
1 | asm("statements"); |
例如:
1 | asm("movl %ecx, %eax"); //把ecx内容移动到eax |
其中”asm” 和 “asm“ 的含义是完全一样的。如果内联汇编有多条指令,那么每行要加上 “\n\t”,让 gcc 把内联汇编代码转换为一般的汇编代码时能够保证换行和留有一定的空格。
例如
1 | __asm__ ( "movl %eax, %ebx\n\t" |
扩展内联汇编
当寄存器的值发生改变而GCC仍然认为寄存器的值不变时,基本内联汇编在程序优化会出现问题,所以引入扩展内联汇编来解决这一问题。
扩展内联汇编格式如下:
1 | asm ( assembler template |
其中assembler template为汇编指令部分。括号内的操作数都是C语言表达式中常量字符串。不同部分之间使用冒号分隔。相同部分语句中的每个小部分用逗号分隔。最多可以指定10个操作数。
例如:
1 | asm ( "cld\n\t" |
以上循环count次将fill_value的值到填充到edi寄存器指定的内存位置。并且告诉GCC,寄存器ecx和edi中的内容可能已经被改变了。
再例如
1 | int a=10, b; |
以上将a赋值给b,其中:
- “b”是输出操作数,用%0来访问,”a”是输入操作数,用%1来访问。
- “r” 是一个constraint,让GCC自由选择一个寄存器来存储变量a。
汇编模板
汇编模板部分就是嵌入在C程序中的汇编指令,格式如下:
- 每条指令放在一个双引号内,或者将所有的指令都放着一个双引号内。
- 每条指令都要包含一个分隔符。合法的分隔符是换行符(\n)或者分号。用换行符的时候通常后面放一个制表符\t。
- 访问C语言变量用%0,%1…等等。
操作数
”asm”内部使用C语言字符串作为操作数,操作数都要放在双引号中。constraint和修饰都放在双引号内。如下所示:
1 | "constraint" (C expression) //"=r"(result) |
对于输出操作数一定要用 “=“修饰。 constraint主要用来指定操作数的寻址类型 (内存寻址或寄存器寻址),也用来指明使用哪个寄存器,多个操作数间用逗号分隔。
例如
1 | asm ( "leal (%1,%1,4), %0" |
这里输入操作数是 ‘x’,在没有指定具体寄存器的情况下,GCC会自己选择合适的输入输出寄存器。我们也可以修改constraint部分内容,让GCC固定使用同一个寄存器,具体方法如下:
1 | asm( "lea (%0,%0,4),%0" |
Clobber List
如果某个指令改变了某个寄存器的值,我们就必须在asm中第三个冒号后的Clobber List中标示出该寄存器来通知GCC,让其不再假定之前存入这些寄存器中的值依然合法。输入输出寄存器不用放Clobber List中,因为GCC明确了asm将使用这些寄存器。其他寄存器无论是显示还是隐式地使用,必须在clobbered list中标明。
如果指令中以无法预料的形式修改了内存值,需要在clobbered list中加上”memory”。从而使得GCC不覆盖该位置的值。此外,如果要改变没有被列在输入和出部分的内存内容时,需要加上volatile关键字说明。clobbered list中列出的寄存器可以被多次读写。
以下给出内联汇编实现乘法的例子:
1 | asm( "movl %0,%%eax; |
constraints
常用constraints参数:
寄存器操作数constraints: r
添加上这个参数后,操作数将被存储在通用寄存器中。
1 | asm ( "movl %%eax, %0" : "=r" (myval)); |
若需指定使用哪个寄存器,可以指定以下限制
a | %eax, %ax, %al |
---|---|
b | %ebx, %bx, %bl |
c | %ecx, %cx, %cl |
d | %edx, %dx, %adl |
S | %esi, %si |
D | %edi, %di |
内存操作数constraint: m
当操作数在内存中时,任何对其操作会直接在内存中进行。
1 | asm (“sidt” %0” : : “m”(loc) ); |
m, v, o | 内存单元 |
---|---|
R | 任何通用寄存器 |
Q | 寄存器eax, ebx, ecx,edx之一 |
I, h | 直接操作数 |
E, F | 浮点数 |
G | 任意 |
I | 常数(0~31) |