gcc采用的是AT&T的汇编格式,MS采用Intel的格式.
一 AT&T 语法
AT&T语法本身比Intel语法有着更为严格的要求。下面通过AT&T语
法与Intel语法的不同来说明AT&T语法:
寄存器命名原则:
在AT&T语法格式中一个最明显的差异就是所有寄存器前都须加上前缀 %。因此,Intel语法中
'eax ax al ah' 须写成 '%eax %ax %al %ah' 形式。
操作码命令格式:
1. 源/目的操作数顺序:
Intel语法格式中命令表示格式为:
"opcode dest, src" ;
"操作码 目标, 源"
而AT&T语法格式表示为:
"opcode src, dest" ;
"操作码 源, 目标"
因此Intel格式的命令 'mov eas, ebx' 在AT&T语法中要写成 'mov %ebx, %eax'。
2. 操作数长度标识:
在AT&T语法中,通过在指令后添加后缀来指明该指令运算对象的尺寸.
后缀 'b' 指明运算对象是一个字节(byte)
后缀 'w' 指明运算对象是一个字(word)
后缀 'l' 指明运算对象是一个双字(long)
Intel语法中指令'mov'在AT&T语法必须根据运算对象的实际情况写成:'movb','movw'
或'movl'。(注:若在AT&T中省略这些后缀,GAS将通过使用的寄存器大小来猜测指令的
操作数尺寸)
3. 另外,'FAR'不是GAS的关键字,因此对far的call或jmp指令须加前缀 'l', 'far call'
要写成 'lcall' , 'far jmp' 要写成 'ljmp' , 'ret far' 写成 'lret'。
常数/立即数的格式:
在AT&T语法中对立即数,须在其前加前缀 $ 来指明,而Inter语法则不需要。因此, 在
AT&T语法中, 'push 4' 指令要写成 'push $4' 。
另外, 在常数前也必须加一个前缀字符 * ,而Inter语法则也是不需要的。
内存寻址方式:
在Intel语法中,使用下面格式来表示存储器寻址方式:
SECTION:[BASE + INDEX*SCALE + DISP] ;
段:[基地址+变址*比例因子+偏移量]
这里BASE是基地址索引寄存器(可以是任一通用寄存器),INDEX是变址寄存器(除ESP外
的任一通用寄存器),SCALE是变址寄存器的比例常数,DISP是基址/变址寄存器的位移量。
下面是一些Intel语法中寻址的例子:
[ebp - 4] [BASE DISP] (Note: DISP偏移量是-4)
[foo + eax*4] [DISP + INDEX*SCALE]
[foo] [DISP] (Value pointed to by 'foo')
gs:foo SECTION
ISP (Contents of variable 'foo')
AT&T语法则使用不同的格式来表示寻址方式:
SECTION
ISP(BASE, INDEX, SCALE) ;
段:偏移量(基地址,变址,比例因子)
同样的例子,在AT&T语法中为如下格式:
-4(%ebp) DISP(BASE)
foo(,%eax,4) DISP(,INDEX,SCALE)
foo(,1) DISP(,SCALE) (Note: 这里的逗号必须要有)
%gs:foo SECTION
ISP
注:在括号中必须使用逗号来表示跳过的地址元素(例如,你不需要使用基址寄存器)。
下面再例举一些不同的寻址方式两种语法格式的比较:
__AT&T______________________ __Intel_________________________
movl 4(%ebp), %eax mov eax, [ebp+4])
addl (%eax,%eax,4), %ecx add ecx, [eax + eax*4])
movb $4, %fs
%eax) mov fs:eax, 4
movl _array(,%eax,4), %eax mov eax, [4*eax + array])
movw _array(%ebx,%eax,4), %cx mov cx, [ebx + 4*eax + array])
标号 &
标识符:
所有的标号必须以一个字母,点或下划线开始,标号后加一个冒号表示标号的结束。
局部标号使用数字0-9后跟一个冒号,使用局部标号时要在数字后跟一个字符'b'(向后
引用)或字符'f'(向前引用)。因为只能使用数字0-9作为局部标号名,所以最多只能定义10
个局部标号。
一个标识符能给它赋于一个值。(如:'TRUE=1', 或者使用 .set 或 .equ 指令)。
二 基本的行内汇编
基本的行内汇编很简单,一般是按照下面的格式
asm("statements");
例如:asm("nop");
asm("cli");
asm 和 __asm__是完全一样的.
如果有多行汇编,则每一行都要加上 "/n/t"
例如:
asm( "pushl %eax/n/t"
"movl $0,%eax/n/t"
"popl %eax");
实际上gcc在处理汇编时,是要把asm(...)的内容"打印"到汇编
文件中,所以格式控制字符是必要的.
再例如:
asm("movl %eax,%ebx");
asm("xorl %ebx,%edx");
asm("movl $0,_booga);
在上面的例子中,由于我们在行内汇编中改变了edx和ebx的值,但是
由于gcc的特殊的处理方法,即先形成汇编文件,再交给GAS去汇编,
所以GAS并不知道我们已经改变了edx和ebx的值,如果程序的上下文
需要edx或ebx作暂存,这样就会引起严重的后果.对于变量_booga也
存在一样的问题.为了解决这个问题,就要用到扩展的行内汇编语法.
三 扩展的行内汇编
扩展的行内汇编类似于Watcom.
基本的格式是:
asm ( "statements" : output_regs : input_regs : clobbered_regs);
clobbered_regs指的是被改变的寄存器.
下面是一个例子(为方便起见,我使用全局变量):
int count=1;
int value=1;
int buf[10];
void main()
{
asm(
"cld /n/t"
"rep /n/t"
"stosl"
:
: "c" (count), "a" (value) , "D" (buf[0])
: "%ecx","%edi" );
}
得到的主要汇编代码为:
movl count,%ecx
movl value,%eax
movl buf,%edi
#APP
cld
rep
stosl
#NO_APP
cld,rep,stos就不用多解释了.
这几条语句的功能是向buf中写上count个value值.
冒号后的语句指明输入,输出和被改变的寄存器.
通过冒号以后的语句,编译器就知道你的指令需要和改变哪些寄存器,
从而可以优化寄存器的分配.
其中符号"c"(count)指示要把count的值放入ecx寄存器
类似的还有:
a eax
b ebx
c ecx
d edx
S esi
D edi
I 常数值,(0 - 31)
q,r 动态分配的寄存器
g eax,ebx,ecx,edx或内存变量
A 把eax和edx合成一个64位的寄存器(use long longs)
我们也可以让gcc自己选择合适的寄存器.
如下面的例子:
asm("leal (%1,%1,4),%0"
: "=r" (x)
: "0" (x) );
这段代码实现5*x的快速乘法.
得到的主要汇编代码为:
movl x,%eax
#APP
leal (%eax,%eax,4),%eax
#NO_APP
movl %eax,x
几点说明:
1.使用q指示编译器从eax,ebx,ecx,edx分配寄存器.
使用r指示编译器从eax,ebx,ecx,edx,esi,edi分配寄存器.
2.我们不必把编译器分配的寄存器放入改变的寄存器列表,因为寄存器
已经记住了它们.
3."="是标示输出寄存器,必须这样用.
4.数字%n的用法:
数字表示的寄存器是按照出现和从左到右的顺序映射到用"r"或"q"请求
的寄存器.如果我们要重用"r"或"q"请求的寄存器的话,就可以使用它们.
5.如果强制使用固定的寄存器的话,如不用%1,而用ebx,则
asm("leal (%%ebx,%%ebx,4),%0"
: "=r" (x)
: "0" (x) );
注意要使用两个%,因为一个%的语法已经被%n用掉了.