GCC汇编器语法
由于Linux内核代码使用GCC汇编器语法,如果可以了解的话,对理解内核有一定帮助。我结合了一些资料总结了一下。由于本人初次研究且水平有限,涉及 到的可能只是很简单的一部分,希望各位大虾拍砖。
A. GCC汇编器语法 //////////////////////////////////////////////////////////////
GCC编译器使用AT&T语法。它与Intel语法有几个主要的不同点:
1. 操作数的目的和源位置颠倒
在Intel语法中,第一个操作数是目的,而第二个操作数是源;而AT&T则正好相反
2. 寄存器命名
在AT&T语法中,寄存器被加上了%前缀,写为%eax等
3. 立即操作数
在AT&T语法中,立即操作数往往需要$前缀,对于16进制的数字加上0x前缀,例如$0x4F;
在Intel语法中往往是4fh,用'h'作为后缀
4. 操作数大小
AT&T语法中,操作数占内存大小决定于汇编命令操作符的最后一个字符的内容。 操作符以'b', 'w'和 'l'为后缀指明内存访问长度是byte(8-bit), word(16-bit)还是long(32-bit). 而Intel语法在操作数前加上'byte ptr','word ptr'和'dword ptr'的内存操作数(这个操作数不是汇编命令操作符)来达到相同目的.
因此, Intel "mov al, byte ptr foo" 用AT&T语法就是 :"movb foo, %al"
5. 内存操作数
在Intel的语法中,基址寄存器用'['和']'扩起来,但是在AT&T中改用'('和')'。 此外,在Intel语法中一个间接内存寻址:
section:[base + index * scale + disp],在AT&T中则为:
section:disp(base, index, scale)
总是需要记住的一点就是,当一个常数被用作disp或者scale时,就不用加'$'前缀。
对比的例子:
Intel Code
AT&T code
mov eax,1
mov $1,%eax
mov ebx,0ffh
mov $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 -0x02(%ebx, %ecx, 0x04),%eax
B. 基本形式的内联汇编 //////////////////////////////////////////////////////////
以下2种都合法:
__asm__("assembly code");
asm("assembly code")
如果 我们有多余一条的指令,可以分行写,每行要加上"",每个指令末尾添加\n\t,这是因为GCC将每行指令作为一个字符串传给as(GAS),使用换行和 TAB可以给汇编器传送正确的格式化好的代码行。
C. 扩展形式的内联汇编 //////////////////////////////////////////////////////////
前面介绍的基础形式的内联汇编方法只涉及到嵌入汇编指令。在高级形式中,我们将可以指定操作数,它允许我们指
定输入输出寄存器(内联函 数使用这些寄存器作为存储输入输出变量)和程序中涉及到的clobbered寄存器列表(clobbered
registers:内联汇编程序可 能要改变其内容的寄存器)也并不是一定要要显式指明使用具体的寄存器,我们也可以把它留给GCC去选择,这样GCC还可能更好的进行优化处理。高级内联汇 编的基本格式如下:
asm ( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
其中 assemblertemplate包含汇编指令部分。括号中每个操作数用C表达式常量串描述。不同部分之间用冒号分开。相同部分中的每个小部分用逗号分 开。
例如:
int a=10, b;
asm ( "movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);
上面代码所做的就是用汇编代码把a的值赋给b。值得注意的几点有:
1) "b"是输出操作数,用%0来访问,”a”是输入操作数,用%1来访问。
2) "r" 是一个constraint, 关于constraint后面有详细的介绍。这里我们只要记住这里constraint
"r"让GCC自己选择一个寄存器去存储变量a。输出部分 的constraint前必须要有个"=",用来说明是一个这是一个输出操作数,并且只写。
3) 你可能看到有的寄存器名字前面写了两个%,这是用来帮助GCC区分操作数和寄存器。操作数只需要一个%前缀。
4) 在第三个冒号后面的clobbered register,%eax说明在内联汇编代码中将要改变eax中的内容,GCC不要用它存储其他值。
当 这段代码执行结束后,"b"的值将会被改掉,因为它被指定作为输出操作数。换句话说,在"asm"内部对b的改动将影响到asm外面.
D. 汇编模板 ////////////////////////////////////////////////////////////////////
每条指令放在一个双引号内,或者将所有的指令都放着一个双引号内。每条指令都要包含一个分隔符。合法的分隔符是换行符(\n)或者分号。用换行 符的时候通常后面放一个制表符"\t"。我们已经知道为什么使用换行符+制表符了(前面部分有解释)。其中,访问C操作数用%0,%1等等
E. 操作数 //////////////////////////////////////////////////////////////////////
C语言表达式(大多情况是C变量)将作为"asm"内部使用的操作数。每一个操作数都以双引号开始。对于输出操作数,还要写一个修改标志 (=)。constraint和修改标志都放在双引号内。接下来部分就是C表达式了(放在括号内)。
标准形式如下:
"constraint" (C expression) [ 如: "=r"(result) ]
对于输出操作数还有一个修改标志(=)。 constraint主要用来指定操作数的寻址类型(内存寻址或寄存器寻址),也用来指明使用哪个寄存器。如果输出表达式不能直接寻址(比如是bit- field),constraint就必须指定一个寄存器.这种情况下,GCC将使用寄存器作为asm的输出。
现在我们来看一些例子,把一个数字 乘以5使用汇编指令lea
asm( "leal (%1,%1,4), %0"
: "=r" (five_times_x)
: "r" (x)
);
这里输入操作数是'x',不指定具体使用那个寄存器,GCC会自己选择输入输出的寄 存器来操作。如果我们也可以让GCC把输入和输出寄存器限定同一个。只需要使用读写操作数,使用合适的constraint,看下具体方法:
asm( "leal (%0,%0,4), %0"
: "=r" (five_times_x)
: "0" (x)
);
上面使输入和输出操作数存在相同的寄存器中,我们不知道GCC具体使那个寄存器,但是我们也可以指定一个,像这样:
asm( "leal (%0,%0,4), %0"
: "=c" (five_times_x)
: "c" (x)
);
F. Clobber List ////////////////////////////////////////////////////////////////
一些指令破坏了一个寄存器值,我们就不得不在asm里面第三个冒号后的Clobber List中标示出来,通知GCC这个里面的值要被改掉。这样GCC将不再假设之前存入这些寄存器中的值是合法的了。我们不需要把输入输出寄存器在这个部分 标出,因为GCC知道asm将使用这些寄存器。(因为它们已经显式的被作为输入输出标出)。如果此外指令中还用到其他寄存器无论显示还是隐式的使用到(没 有在输入输出中标示出的),这些指令必须在clobbered list中标明。
如果指令中以不可预见形式修改了内存值,要加上"memory" 到clobbered list中。这使得GCC不去缓存在这些内存值。还有,如果内存被改变而没有被列在输入和出部分 要加上volatile关键字。
如果汇编代码必须在我们放的位置被执行(例如不能被循环优化而移出循环),那就在asm之后()之前,放一个valatile关键字。 这样可以禁止这些代码被移动或删除。
G. 常用constraints /////////////////////////////////////////////////////////////
o. 寄存器操作数constraint(r)
constraints "r"被指定时,GCC可能在任何一个可用的通用寄存器中保存这个值。当然如果你要指定具体使用那个寄存器就要指定具体使用哪个寄存器的
+---+--------------------+
| r |
Register(s)
|
+---+--------------------+
| a |
%eax, %ax, %al
|
| b |
%ebx, %bx, %bl
|
| c |
%ecx, %cx, %cl
|
| d |
%edx, %dx, %dl
|
| S |
%esi, %si
|
| D |
%edi, %di
|
+---+--------------------+
o. 内存操作数constraint(m)
当操作数在内存中时,任何对其操作将直接通过内存地址进行。和寄存器constraint相反,内存操作是先把值存在一个寄存器中,修改后再将值回写到这 个内存地址。寄存器constraint通常只用在对速度要求非常严格的场合。因为内存constraint可以更有效率的将一个C语言变量在asm中跟 新[不需要寄存器中转],而且可能你也不想用一个寄存器来暂存这个变量的值。
o. 匹配constraint
某些情况下,一个变量可能用来保存输入和输出两种用途。这种情况下我们就用匹配constraint
asm ("incl %0" : "=a"(var) : "0"(var));
这个例子中eax寄存器被用来保存输入也用来保存输出变量。输入变量被读入eax中,incl执行之后eax被跟新并
且又保存到变量var中。这儿的constraint "0"指定使用和第一个输出相同的寄存器。就是说,输入的变量应该只能放在eax中。这个constraint可以在下面的情况下被使用:
a) 输入值从一个变量读入,这个变量将被修改并且修改过的值要写回同一个变量;
b) 没有必要把输入和输出操作数分开。
o. 其他constranints
a) "m": 使用一个内存操作数,内存地址可以是机器支持的范围内
b) "o": 使用一个内存操作数,但是要求内存地址范围在在同一段内 。例如,加上一个小的偏移量来形成一个可用的地址
c) "V": 内存操作数,但是不在同一个段内。换句话说,就是使用"m"的所有的情况除了"o"
d) "i": 使用一个立即整数操作数(值固定);也包含仅在编译时才能确定其值的符号常量
e) "n": 一个确定值的立即数。很多系统不支持汇编时常数操作数小于一个字。这时候使用n就比使用i好
f) "g": 除了通用寄存器以外的任何寄存器,内存和立即整数
在使用constraint的时候,为了更精确的控制约束,GCC提供了一些修改标记,常用的修改标记有:
a) "="指这个输出操作数是只写的;之前保存在其中的值将废弃而被输出值所代替。
b) "&"用于对输出操作数的修饰。当使用它进行修饰时,等于向GCC声明:"GCC不得为任何输入操作数分配与此输出操作数相同的寄存器"。其原因 是&修饰符意味着被其修饰的输出操作数要在所有的输入操作数被输入前输出。