学习linux内核少不了要读at&t的汇编,这个对大多数使用intel汇编的人是一个不幸的消息,要另起炉灶。什么?你不是学得intel汇编?你是不是中国学生?记得我们有门课是学<intel 8086/8088汇编语言>,学得挺感兴趣,窃以为这个就是学电脑的最高境界。当时怎么也没人说这个是intel汇编语言格式,还有个什么at&t格式..
不扯远了,这两者之间还是有章可循的,我觉得要注意的是转移指令,这个要仔细琢磨一下才看的明白。内存间接寻址的格式看起来也比较费劲,其他的东西是到是可以看一次就明白的。如果您有耐心,希望查看更权威的资料建议您查阅as.info和gcc.info,实际上,严格是说,gas只是众多遵循at&t格式汇编程序中的一个实现特例,而你寻找的汇编语言规则,只与你使用的汇编程序和编译器有关。
1、 指令操作数的顺序是先源后目的,与Intel指令的先目的后源的顺序相反;
2、 寄存器操作数总是以'%'作为前缀。
3、 立即数前加前缀$;
4、 操作码加后缀以指明操作数的长度,这些后缀有b(8位)、w(16位)、l(32位);
例:
AT&T Intel
moww %bx, %ax // mov ax, bx
xorl %eax, %eax // xor eax, eax
movw $1, %ax // mov ax,1
movb X, %ah // mov ah, byte ptr X
movw X, %ax // mov ax, word ptr X
movl X, %eax // mov eax, X
如果操作码中没有后缀来指定操作数的长度,并且该指令中没有内存操作数,gas会
根据指令中指定的寄存器操作数补上相应的后缀字符。所以,下面两个指令是等价的(这是Gas的特性,而真正的AT&T的Unix汇编程序会将将没有字长后缀的指令的操作数字长全部当作32位):
在linux中,因为是使用gas进行汇编的,故此
mov %ax,%bx
等价于
movw %ax,%bx
5、 大部分指令的操作码都与Intel指令相同,只有下面几个是例外:
movsSD // movsx ,短到长,高位补符号位
movzSD // movzx ,短到长,高位补0
这里,S 是代表源操作数长度的后缀,D 是代表目的操作数长度的后缀。如:
movswl %ax, %ecx // movsx ecx, ax
cbtw // cbw ,将AL中的一个字节扩充为一个字(AX),
// AH是AL的符号位
cwtl // cwde ,将AX中的一个字扩充为一个双字(EAX),
// EAX的高位是AX的符号位
cwtd // cwd ,将AX中的一个字扩充为两个字(DX:AX)
// DX是AX的符号位
cltd // cdq ,将EAX中的一个双字扩充为两个双字
// (EDX:EAX),EDX是EAX的符号位
lcall $S,$O // call far S:O
ljmp $S,$O // jmp far S:O
lret $V // ret far V
6、 操作码的前缀(如rep)要单独写一行,不要和它修饰的指令(如movsb、stosb)写在同一行上;
7、 内存间接寻址的格式不同
AT&T Intel
disp(base, index, scale) [base + index*scale + disp]
例:
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])
8、 局部标号可以用数字,而且可以重复。在以这些标号为目的的转移指令上,标号要带上后缀,b表示向前,f表示向后。
例:
orw %bx,%bx
jz 1f
1:
movl $0x101000,%eax
movl %eax,%cr3 /* set the page table pointer.. */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* ..and set paging (PG) bit */
jmp 1f /* flush the prefetch-queue */
1:
movl $1f,%eax
jmp *%eax /* make sure eip is relocated */
1:
绝对跳转/调用指令中的内存操作数必须以'*'为前缀,否则gas总是认为是相对跳转/调用指令,而且gas汇编程序自动对跳转指令进行优化,总是使用尽可能小的跳转偏移量。如果8比特的偏移量无法满足要求的话,as会使用一个32位的偏移量,as汇编程序暂时还不支持16位的跳转偏移量,所以对跳转指令使用'addr16'前缀是无效的。还有一些跳转指令只支持8位的跳转偏移量,这些指令是:
'jcxz','jecxz','loop','loopz','loope','loopnz''loopne'
如果你在汇编中使用了这些指令,用gas的汇编可能会出错,因为gcc在编译过程中不产生这些指令,所以在c语言中不必担心这些问题。
9、 实模式下的语法与Intel指令语法基本相同;可以用上述格式的汇编单独写程序(有许多宏定义和它特有的文件格式),而后用gcc/gas将其汇编成目标代码。在linux中,这种形式的代码主要集中在启动部分。
10、还有一个比较特别的,就是gcc可以编译嵌入在c语言中的汇编段落。在Linux代码中很多地方都使用了这种形式的汇编语言,嵌入汇编程序的格式如下:
__asm__ __volatile__ (
asm statements
: outputs
: inputs
: registers-modified
);
asm statements是一组AT&T格式的汇编语言语句,每个语句一行,由\n分隔各行。所有的语句都被包裹在一对双引号内。其中使用的寄存器前面要加两个%%做前缀;转移指令多是局部转移,因此多使用数字标号。
inputs指明程序的输入参数,每个输入参数都括在一对圆括号内,各参数用逗号分开。每个参数前加一个用双引号括起来的标志,告诉编译器把该参数装入到何处。可用的标志有:“g”:让编译器决定如何装入它;“a”:装入到ax/eax;“b”:装入到bx/ebx;“c”:装入到cx/ecx;“d”:装入到dx/edx;“D”:装入到di/edi;“S”:装入到si/esi;“q”:a、b、c、d寄存器等;“r”:任一通用寄存器;“i”:整立即数;“p”:有效内存地址;“=”:输出;“+”:既是输入又是输出;“&”:改变其值之前写;“%”:与下一个操作数之间可互换;“#”:忽略其后的字符,直到逗号;“*”:当优先选择寄存器时,忽略下面的字符;“0~9”:指定一个操作数,它既做输入又做输出。通常用“g”。
outputs指明程序的输出位置,通常是变量。每个输出变量都括在一对圆括号内,各个输出变量间用逗号隔开。每个输出变量前加一个标志,告诉编译器从何处输出。可用的标志与输入参数用的标志相同,只是前面加“=”。如“=g”。输出操作数必须是左值,而且必须是只写的。如果一个操作数即做输出又做输入,那么必须将它们分开:一个只写操作数,一个输入操作数。输入操作数前加一个数字限制(0~9),指出输出操作数的序号,告诉编译器它们必须在同一个物理位置。两个操作数可以是同一个表达式,也可以是不同的表达式。
registers-modified告诉编译器程序中将要修改的寄存器。每个寄存器都用双引号括起来,并用逗号隔开。如“ax”。如果汇编程序中引用了某个特定的硬件寄存器,就应该在此处列出这些寄存器,以告诉编译器这些寄存器的值被改变了。如果汇编程序中用某种不可预测的方式修改了内存,应该在此处加上“memory”。这样以来,在整个汇编程序中,编译器就不会把它的值缓存在寄存器中了。
__volatile__是可选的,它防止编译器修改该段汇编语句(重排序、重组、删除等)。
输入参数和输出变量按顺序编号,先输出后输入,编号从0开始。程序中用编号代表输入参数和输出变量(加%做前缀)。
输入、输出、寄存器部分都可有可无。如有,顺序不能变;如无,应保留“:”,除非不引起二意性。
看一个在C语言中使用at&t的嵌入汇编程序的例子,c语言中的3个int变量,一般会是三个内存地址。每个操作数的长度则要根据操作系统和编译器来决定,一般32位操作系统为32位,则每个操作数占用4个字节:
int i=0, j=1, k=0;
__asm__ __volatile__("
pushl %%eax\n //asm statement
movl %1, %%eax\n //asm statement
addl %2, %%eax\n //asm statement
movl %%eax, %0\n //...
popl %%eax" //...
: "=g" (k) //outputs
: "g" (i), "g" (j) //inputs
: "ax", "memory" //registers modified
);
按照参数编号原则输出参数参数k为%0,输入参数i和j依次为%1和%2。值得注意的是输出和输入标志都使用了"g",所以我们不必关心这些参数究竟是使用了寄存器还是内存操作数,编译器自己会决定。