AT&T语法

我们在大学所学习的汇编语言大多数都是Intel语法,也许这和教学系统都是微软的操作系统DOS和Windows有关,翻翻过去的教材,一律全是DOS下汇编或Windows下汇编。Linux内核中的汇编代码一般都是AT&T语法,我想,随着Linux普及,以后在教学中会越来越多采取AT&T语法啦。

什么是AT&T语法?

AT&T是汇编语言的一种语法风格、格式。在某一处理器平台上,无论汇编代码是什么语法,其编译出来的机器码是一样的,所以不要误以为AT&T是一种新的机器语言。它仅仅是表达方式不同,意思是一样的。比如,。
AT&T首先在UNIX中使用,可当初UNIX并不是在x86处理器上开发的,最初是在PDP-11机器上开发的,后来又移植到VAX和68000的处理器上,所以AT&T的语法自然更接近于这些处理器的特性。虽然UNIX后来又移植到x86上了,但还是要尊重UNIX圈内的习惯,其汇编语法接近于那些前辈处理器上的语法,这就是AT&T语法。
无论语法再怎么变,汇编语言中指令关键字肯定不能有太大出入,名字非常接近,只是在指令名字的最后加上了操作数大小后缀,b表示1字节,w表示2字节,l表示4字节。比如压栈指令,Intel中是push,AT&T中是pushl,最后这个’l’表示压入4字节(long型大小)。在了解Intel汇编指令的情况下,基本上能够看懂AT&T的汇编指令。它们的主要差别是语法风格,咱们对照着看下这两种风格的区别吧。

无论是哪种汇编语言风格,都要有访问内存的能力,这就是内存寻址。

Intel语法中,立即数就是普通的数字,如果让立即数成为内存地址,需要将它用中括号括起来,“[立即数]”这样才表示以“立即数”为地址的内存。
AT&T认为,内存地址既然是数字,那数字也应该被当作内存地址,所以,数字被优先认为是内存地址,也就是说,操作数若为数字,则统统按以该数字为地址的内存来访问。这样,立即数的地位比较次要了,如果想表示成单纯的立即数,需要额外在前面加个前缀$。
Intel汇编语法中的很多寻址方式,就内存寻址来说,有直接寻址、基址寻址、变址寻址、基址变址寻址。也可能是习惯了的原因,我个人觉得Intel语法真的很直白,容易理解,尤其是在和AT&T的内存寻址相比较之后……
而在AT&T中的内存寻址还是挺独特的,它的内存寻址有固定的格式
segreg(段基址):base_address(offset_address,index,size)
该格式对应的表达式为:segreg(段基址):base_address+ offset_address+ indexsize。
此表达式的格式和Intel 32位内存寻址中的基址变址寻址类似,Intel的格式:segreg:[base+index
size+offset]
不过与Intel不同的是AT&T地址表达式的值是内存地址,直接被当作内存来读写,而不是普通数字。
看上去格式有些怪异,但其实这是一种“通用”格式,格式中短短的几个成员囊括了它所有内存寻址的方式,任意一种内存寻址方式,其格式都是这个通用格式的子集,都是格式中各种成员的组合。下面介绍下这些成员项。
base_address是基地址,可以为整数、变量名,可正可负。
offset_address是偏移地址。
index是索引值,这两个必须是那8个通用寄存器之一。
size是个长度,只能是1、2、4、8(Intel语法中也是只能乘以这4个数)。
下面看看内存寻址中有哪些方式,注意,这些方式都是上面通用格式的一部分。
直接寻址:此寻址中只有base_address项,即后面括号中的内容全不要,base_address便为内存啦,比如movl $255,0xc00008F0,或者用变量名:mov $6,var。
寄存器间接寻址:此寻址中只有offset_address项,即格式为(offset_address),要记得,offset_address只能是通用寄存器。寄存器中是地址(去这个地址取值),不要忘记格式中的圆括号,如mov (%eax), %ebx。
寄存器相对寻址:此寻址中有offset_address项和base_address项,即格式为base_address(offset_address)。这样得出的内存地址是基址+偏移地址之和。各部分还是要按照格式填写,如movb -4(%ebx),%al,功能是将地址(ebx-4)所指向的内存复制1字节到寄存器al。
变址寻址:此类寻址称为变址的原因是含有通用格式中的变量Index。因为index是size的倍数,所以有index的地方就有size。既然是变址,只要有index和size就成了,base_address和offset_address可有可无,注意,格式中没有的部分也要保留逗号来占位。一共有4种变址寻址组合,下面各举个例子。
无base_address,无offset_address:movl %eax, (,%esi,2) ; index和size功能是将eax的值写入esi2所指向的内存。
无base_address,有offset_address:movl %eax, (%ebx,%esi,2) ; 功能是将eax的值写入ebx+esi
2所指向的内存。
有base_address,无offset_address:movl %eax, base_value(,%esi,2)功能是将eax的值写入base_value+esi2所指向的内存。
有base_address,有offset_address:movl %eax,base_value(%ebx,%esi,2)功能是将eax的值写入base_value+ebx+esi
2所指向的内存。
好啦,AT&T就简单介绍到这,咱们重点是内联汇编。

基本内联汇编

基本内联汇编是最简单的内联形式,其格式为:asm [volatile] (“assembly code”)
各关键字之间可以用空格或制表符分隔,也可以紧凑挨在一起不分隔,各部分意义如下:
关键字asm用于声明内联汇编表达式,这是内联汇编固定的部分,不可少。
是由gcc定义的宏:#define
因为gcc有个优化选项-O,可以指定优化级别。当用-O来编译时,gcc按照自己的意图优化代码,说不定就会把自己所写的代码修改了。
关键字volatile是可选项,它告诉gcc:“不要修改我写的汇编代码,请原样保留”。
“assembly code”是咱们所写的汇编代码,它必须位于圆括号中,而且必须用双引号引起来。这是格式要求,只要满足了这个格式asm [volatile] (“”),assembly code甚至可以为空。

下面说下assembly code的规则。

(1)指令必须用双引号引起来,无论双引号中是一条指令或多条指令。(2)一对双引号不能跨行,如果跨行需要在结尾用反斜杠’’转义。(3)指令之间用分号’;’、换行符’\n’或换行符加制表符’\n’’\t’分隔。
提醒一下,即使是指令分布在多个双引号中,gcc最终也要把它们合并到一起来处理,合并之后,指令间必须要有分隔符。
所以,当指令在多个双引号中时,除最后一个双引号外,其余双引号中的代码最后一定要有分隔符,这和其他编程语言中表示代码结束的分隔符是一样的,如:
asm(“movl $9,%eax;” ”pushl %eax”) 正确asm(“movl $9,%eax” ”pushl %eax”) 错误大家注意,在内联汇编中,咱们要注意操作数的顺序啦,现在是和Intel反着的。
给大家举个例子,见文件inlineASM.c。

  1. #include <stdio.h>
  2. /* 大家注意到没有,inlineASM.c中的变量count和str定义为全局变量。对的,在基本内联汇编中,若要引用C变量,只能将它定义为全局变量。如果定义为局部变量,链接时会找不到这两个符号 */
  3. char* str="hello,world this is my first inlineASM.\n";
  4. int len = 0;
  5. int count = 0;
  6. int result = 0;
  7. int cont_str(char *s)
  8. {
  9. int i = 0;
  10. while ( str[i] != 0) {
  11. printf("%d\n",str[i]);
  12. i++;
  13. }
  14. return i;
  15. }
  16. void main(){
  17. len = cont_str(str);
  18. printf("%d\n",len);
  19. printf("\n");
  20. /*寄存器前面加前缀%,立即数前面加前缀$,操作数由左到右的顺序。*/
  21. /* write的功能是把buf指向的缓冲区中的count个字节写入fd指向的文件描述符,执行成功后返回写入的字节数,失败则返回-1。 write(1,"hello,world\n",4); */
  22. /*
  23. eax寄存器用来存储子功能号(寄存器eip、ebp、esp是不能使用的)。5个参数存放在以下寄存器中,传送参数的顺序如下。
  24. (1)ebx存储第1个参数。
  25. (2)ecx存储第2个参数。
  26. (3)edx存储第3个参数。
  27. (4)esi存储第4个参数。
  28. (5)edi存储第5个参数。
  29. */
  30. asm volatile (
  31. "pusha;/* 将8个通用寄存器压栈 */\
  32. movl $4,%eax;/* 传入第4号系统调用,这就是write的调用号*/\
  33. movl $1,%ebx;/* fd */\
  34. movl str,%ecx;/* buffer */\
  35. movl len,%edx;/* buffer_len */\
  36. int $0x80;/* 执行系统调用0x80 */\
  37. mov %eax,count;/* 获取write的返回值,返回值都是存储在eax寄存器中,所以将其复制到变量count中。 */\
  38. mov %eax, result;\
  39. popa;/* 将8个通用寄存器出栈 */\
  40. "
  41. );
  42. printf("The system caller 0x80's return value is %d\n",result);
  43. }
  44. gcc -m32 -o inlineASM.bin inlineASM.c

扩展内联汇编

asm [volatile] (“assembly code”:output : input : clobber/modify)和前面的基本内联汇编相比,扩展内联汇编在圆括号中变成了4部分,多了output、input和clobber/modify三项。其中的每一部分都可以省略,甚至包括assembly code。省略的部分要保留冒号分隔符来占位,如果省略的是后面的一个或多个连续的部分,分隔符也不用保留,比如省略了clobber/modify,不需要保留input后面的冒号。
assembly code:还是用户写入的汇编指令,和基本内联汇编一样。
汇编代码的运行是需要输入参数的,其运行之后也可产出结果。
在C代码中内嵌汇编的目的是让汇编帮助C完成某些功能,所以C代码就要为其提供参数和用于存放其输出结果的空间。这样一来,内联汇编代码类似机器,C代码类似人。机器要运行,人就要为机器提供加工的源材料(input),机器运行后,将生产出来的成果放到人能够得着的地方(output),人才能获取机器的输出结果。input和output正是C为汇编提供输入参数和存储其输出的部分,这是汇编与c交互的关键,我们之前的讨论就通过这两项解决。
output:output用来指定汇编代码的数据如何输出给C代码使用。内嵌的汇编指令运行结束后,如果想将运行结果存储到c变量中,就用此项指定输出的位置。output中每个操作数的格式为:
“操作数修饰符约束名”(C变量名)
其中的引号和圆括号不能少,操作数修饰符通常为等号’=’。多个操作数之间用逗号’,’分隔。
input:input用来指定C中数据如何输入给汇编使用。要想让汇编使用C中的变量作为参数,就要在此指定。input中每个操作数的格式为:
“[操作数修饰符] 约束名”(C变量名)
其中的引号和圆括号不能少,操作数修饰符为可选项。多个操作数之间用逗号’,’分隔。
单独强调一下,以上的output()和input()括号中的是C代码中的变量,output(c变量)和input(c变量)就像C语言中的函数,将C变量(值或变量地址)转换成汇编代码的操作数。
clobber/modify:汇编代码执行后会破坏一些内存或寄存器资源,通过此项通知编译器,可能造成寄存器或内存数据的破坏,这样gcc就知道哪些寄存器或内存需要提前保护起来,后面会展开细说。
assembly code中引用的所有操作数其实是经过gcc转换后的复本,“原件”都是在output和input括号中的c变量,后面通过各种例子您就明白了。
上面所说的“要求”,在扩展内联汇编中称为“约束”,它所起的作用就是把C代码中的操作数(变量、立即数)映射为汇编中所使用的操作数,实际就是描述C中的操作数如何变成汇编操作数。这些约束的作用域是input和output部分,咱们看看这些约束是怎么体现的,约束分为四大类。
- 寄存器约束
寄存器约束就是要求gcc使用哪个寄存器,将input或output中变量约束在某个寄存器中。常见的寄存器约束有:
a:表示寄存器eax/ax/al
b:表示寄存器ebx/bx/bl
c:表示寄存器ecx/cx/cl
d:表示寄存器edx/dx/dl
D:表示寄存器edi/di
S:表示寄存器esi/si
q:表示任意这4个通用寄存器之一:eax/ebx/ecx/edx
r:表示任意这6个通用寄存器之一:eax/ebx/ecx/edx/esi/edi
g:表示可以存放到任意地点(寄存器和内存)。相当于除了同q一样外,还可以让gcc安排在内存中
A:把eax和edx组合成64位整数
f:表示浮点寄存器t:表示第1个浮点寄存器
u:表示第2个浮点寄存器
下面咱们先暂停一下,体验一下基本内联汇编和扩展内联汇编的区别,用加法指令addl在两种方式下做个简单的加法运算。

在基本内联汇编中的寄存器用单个%做前缀,在扩展内联汇编中,单个%有了新的用途,用来表示占位符(一会儿细讲),所以在扩展内联汇编中的寄存器前面用两个%做前缀。

扩展内联汇编中寄存器前缀是两个%。同样是为加法指令提供参数,in_a和in_b是在input部分中输入的,用约束名a为c变量in_a指定了用寄存器eax,用约束名b为c变量in_b指定了用寄存器ebx。addl指令的结果存放到了寄存器eax中,在output中用约束名a指定了把寄存器eax的值存储到c变量out_sum中。output中的’=’号是操作数类型修饰符,表示只写,其实就是out_sum=eax的意思。

- 内存约束\
内存约束是要求gcc直接将位于input和output中的C变量的内存地址作为内联汇编代码的操作数,不需要寄存器做中转,直接进行内存读写,也就是汇编代码的操作数是C变量的指针。
m:表示操作数可以使用任意一种内存形式。
o:操作数为内存变量,但访问它是通过偏移量的形式访问,即包含offset_address的格式。

- 立即数约束
立即数即常数,此约束要求gcc在传值的时候不通过内存和寄存器,直接作为立即数传给汇编代码。由于立即数不是变量,只能作为右值,所以只能放在input中。
i:表示操作数为整数立即数
F:表示操作数为浮点数立即数
I:表示操作数为0~31之间的立即数
J:表示操作数为0~63之间的立即数
N:表示操作数为0~255之间的立即数
O:表示操作数为0~32之间的立即数
X:表示操作数为任何类型立即数
为节约篇幅,后面将立即数约束同其他约束一起演示,这里没有单独样例。

- 通用约束
0~9:此约束只用在input部分,但表示可与output和input中第n个操作数用相同的寄存器或内存。