AT&T的汇编世界

时间:2021-11-09 13:04:04

出处:http://blog.sina.com.cn/s/blog_4e0367770100dh0i.html

PS:这段时间在研究Linux源代码,遇到了AT&T汇编,故转贴个不错的AT&T汇编贴。

就像软件的真谛——“给我一个标准,我用我的逻辑舞动世界”一样,AT&T 汇编是汇编语言里的另一种标准,这是相对于鼎鼎大名的intel的x86汇编来说。即使对于电子专业的学生来说,一旦掌握了c51单片机的汇编,x86的汇编也已经入门了,但x86毕竟有着强大的寄存器,在串操作指令和操作系统类指令方面,单片机还是望尘莫及的。

    下面的两段话就轻易的涉及到了关于前缀、操作数的方向、操作码的后缀这些概念的不同。关于前缀,AT&T 汇编中,寄存器前被冠以“%”,立即数前被冠以“$”,十六进制数前被冠以“0x”。所以,如果gemfield在AT&T 语境中说到386的通用寄存器时,会这样描述:8个32-bit寄存器 %eax,%ebx,%ecx,%edx,%edi,%esi,%ebp,%esp。比如:在stage1.s中,有这么一个定义,#define ABS(x) (x-_start+0x7c00),那么你就会知到0x7c00是个十六进制数(_start函数的入口地址就位于内存的0x7c00处)。

    而在设置int 0x13的0x42功能号时,它是这么说的:movb $0x42,%ah 。这句告诉了我们一些不同之处:首先,操作码的后缀l表示的是操作码的大小,l是长整数32位,那么相应的,movw是16位,movb是8位;其次,立 即数是用$前缀来表示的,就像$0x42;再次,寄存器的名字是有%前缀的,像例子中的%ah;最后,操作数的方向有点不一样,比如把立即数$0x42放 到寄存器%ah中,用的是movb $0x42,%ah,也即源操作数在前,目的操作数在后。

    对于内存单元操作数来说,在AT&T 中是把寄存器用()括起来,而非【】。比如,movl %ebx,8(%si),将ebx寄存器里的值放到内存地址是8(%si)的内存单元上。正好,这里同时遇到了另一个问题,就是在AT&T 汇编中,间接寻址方式是有别于x86的。上例中的8(%si)就相当于x86中的【si+8】。

    还有一种叫做label(标号)的程序控制语句,比如,在stage1.s中,有这么一段指令:

    cmpb $GRUB_INVALID_DRIVE,%al

    je 1f

    movb %al,%dl

 1:

    pushw %dx

    上面就用到了标号,je1f,前面的两个数进行比较,如果相等就跳转到1的位置。注意,1后面的f表示的是forward,即从je指令后继续往前走来寻找1这个标号。所以,如果程序中有好几个叫做1的标号,就要看是1f还是1b了,b代表backward,方向和f相反。青岛之光论坛里有这么一个例子可以更好的帮助我们理解:

   je 1f或者je 1b  是跳转到对应的标号的地方。这里的1表示标号(label),f和b表示向前还是向后,f(forward)向前,b(backward)向后 ,如下例:
1行  1:    cmp  $0,  (%si)  
2行          je  1f            ///////跳转到后面的1标示的地方,也就是第6行
3行          movsb  
4行          stosb  
5行          jmp  1b       ////////跳转到前面1表示的地方  ,也就是第1行
6行  1:    jmp  1b        ////////跳转到前面1表示的地方,第6行,其实就是个死循环

    然后,在AT&T 汇编中出现最多的大概就是称作assembler directive的东西了,我们称作“AT&T 汇编程序代码控制”。 所有的汇编器命令名都由句号('.')开头。命令名的其余是字母,通常使用小写。在青岛之光论坛 http://www.civilnet.cn/bbs/read.php?tid=970 处有一篇关于程序代码控制的详细介绍,下面gemfield只挑出几个常用的来说明一下。

  一、.byte 表达式(expression_rs)

.byte.byte可不带参数或者带多个表达式参数,表达式之间由逗点分隔。每个表达式参数都被汇编成下一个字节。在stage1.s中,有这么一段代码:

after_BPB:

CLI

.byte 0x80,0xca

那么编译器在编译时,就会在cli指令的下面接着放上0x80和0xca,因为每个表达式要占用1个字节,所以此处一共占用2个字节。

  二、.word 表达式

这个表达式表示任意一节中的一个或多个表达式(同样用逗号分开),表达式占一个字(两个字节)。类似的还有.long。例:.word 0x800

  三、.file 字符(string)

     .file 通告编译器我们准备开启一个新的逻辑文件。 string 是新文件名。总的来说,文件名是否使用引号‘"’都可以;但如果您希望规定一个空文件名时,必须使用引号""。本语
句将来可能不再使用—允许使用它只是为了与旧版本的as编译器程序兼容。在as的一些配置中,已经删除了.file以避免与其它的汇编器冲突。例如stage1.s中:.file ”stage1.s”。

  四、.text 小节(subsection)

通知as编译器把后续语句汇编到编号为subsection的正文子段的末尾,subsection是一个纯粹的表达式。如果省略了参数subsection,则使用编号为0的子段。例:.text

  五 、.code16

告诉编译器生成16位的指令

  六、.globl symbol

.globl使得连接程序(ld)能够看到symbol,如果gemfield在局部程序中定义了symbol,那么与这个局部程序链接的局部程序也能存取symbol,例:

.globl SYMBOL_NAME(idt) 定义idt为全局符号。

  七、.fill repeat , size , value
repeat, size 和value都必须是纯粹的表达式。本命令生成size个字节的repeat个副本。
Repeat可以是0或更大的值。Size 可以是0或更大的值, 但即使size大于8,也被视作8,以
兼容其它的汇编器。各个副本中的内容取自一个8字节长的数。最高4个字节为零,最低的
4个字节是value,它以as正在汇编的目标计算机的整数字节顺序排列。每个副本中的size
个字节都取值于这个数最低的size个字节。再次说明,这个古怪的动作只是为了兼容其他
的汇编器。size参数和value参数是可选的。如果不存在第2个逗号和value参数,则假定value为零。如果不存在第1个逗号和其后的参数,则假定size为1。

    例如,在linux初始化的过程中,对全局描述符表GDT进行设置的最后一句为:

.fill NR_CPUS*4,8,0,意思是.fill给每个cpu留有存放4个描述符的位置,并且每个描述符是8个字节。

    等等,不一而足。不过要注意的是,这种包含程序已初始化数据的节(.data)和包含程序程序还未初始化的数据的节(.bss),编译器会把它们在4字节上对齐,例如,.data是25字节,那么编译器会将它放在28个字节上。.

    当这种以后缀名.s编写的A T&T格式的汇编代码完成后,就是编译和链接了。linux下有两种方式,一种是使用汇编程序GAS和连接程序ld:

as filename.s –o filename.o

ld filename.o –o filename 最终将源代码转换为目标文件.o再连接为可执行文件filename

    另一种就是大名鼎鼎的被gemfield提到过的gcc:

gcc –o gemfield gemfield.S

    源程序gemfield.S的后缀名可以使用大写,是因为这样可以使gcc自动识别汇编程序中的c预处理命令,包括头文件中的情况,像#include、#define 、#ifdef等。

    本文少了嵌入式汇编的形式,才使得AT&T 汇编看起来井井有条,而非想象中的艰难。如果想要真正锻炼一下这种汇编,源代码stage1.s就是一个绝佳的习题。