Linux 汇编器:对比 GAS 和 NASM
转自 http://www.ibm.com/developerworks/cn/linux/l-gas-nasm.html#ibm-pcon
与其他语言不同,汇编语言要求开发人员了解编程所用机器的处理器体系结构。汇编程序不可移植,维护和理解常常比较麻烦,通常包含大量代码行。但是,在机器上执行的运行时二进制代码在速度和大小方面有优势。
对于在 Linux 上进行汇编级编程已经有许多参考资料,本文主要讲解语法之间的差异,帮助您更轻松地在汇编形式之间进行转换。本文源于我自己试图改进这种转换的尝试。
本文使用一系列程序示例。每个程序演示一些特性,然后是对语法的讨论和对比。尽管不可能讨论 NASM 和 GAS 之间存在的每个差异,但是我试图讨论主要方面,给进一步研究提供一个基础。那些已经熟悉 NASM 和 GAS 的读者也可以在这里找到有用的内容,比如宏。
本文假设您至少基本了解汇编的术语,曾经用符合 Intel? 语法的汇编器编写过程序,可能在 Linux 或 Windows 上使用过 NASM。本文并不讲解如何在编辑器中输入代码,或者如何进行汇编和链接(但是下面的边栏可以帮助您 快速回忆一下)。您应该熟悉 Linux 操作系统(任何 Linux 发行版都可以;我使用的是 Red Hat 和 Slackware)和基本的 GNU 工具,比如 gcc 和 ld,还应该在 x86 机器上进行编程。
现在,我描述一下本文讨论的范围。
清单 1. 一个使用退出码 2 退出的程序 行号NASMGAS 清单 2. 寻找三个数字中最大者的程序 行号NASMGAS 清单 3. 读取字符串并向用户显示问候语的程序 行号NASMGAS 清单 4. 在整数数组上实现选择排序 行号NASMGAS初看起来清单 4 似乎非常复杂,实际上它是非常简单的。这个清单演示了函数、各种内存寻址方案、堆栈和库函数的使用方法。这个程序对包含 10 个数字的数组进行排序,并使用外部 C 库函数 puts 和 printf 输出未排序数组和已排序数组的完整内容。为了实现模块化和介绍函数的概念,排序例程本身实现为一个单独的过程,数组输出例程也是这样。我们来逐一分析一下。
在声明数据之后,这个程序首先执行对 puts 的调用(第 31 行)。puts 函数在控制台上显示一个字符串。它惟一的参数是要显示的字符串的地址,通过将字符串的地址压入堆栈(第 30 行),将这个参数传递给它。
在 NASM 中,任何不属于我们的程序但是需要在链接时解析的标签都必须预先定义,这就是 extern 关键字的作用(第 24 行)。GAS 没有这样的要求。在此之后,字符串的地址 usort_str 被压入堆栈(第 30 行)。在 NASM 中,内存变量(比如 usort_str)代表内存位置本身,所以 push usort_str 这样的调用实际上是将地址压入堆栈的顶部。但是在 GAS 中,变量 usort_str 必须加上前缀 $,这样它才会被当作地址。如果不加前缀 $,那么会将内存变量代表的实际字节压入堆栈,而不是地址。
因为在堆栈中压入一个变量会让堆栈指针移动一个双字,所以给堆栈指针加 4(双字的大小)(第 32 行)。
现在将三个参数压入堆栈,并调用 print_array10 函数(第 37 行)。在 NASM 和 GAS 中声明函数的方法是相同的。它们仅仅是通过 call 指令调用的标签。
在调用函数之后,ESP 代表堆栈的顶部。esp + 4 代表返回地址,esp + 8 代表函数的第一个参数。在堆栈指针上加上双字变量的大小(即 esp + 12、esp + 16 等等),就可以访问所有后续参数。
在函数内部,通过将 esp 复制到 ebp (第 62 行)创建一个局部堆栈框架。和程序中的处理一样,还可以为局部变量分配空间(第 63 行)。方法是从 esp 中减去所需的字节数。esp 4 表示为一个局部变量分配 4 字节的空间,只要堆栈中有足够的空间容纳局部变量,就可以继续分配。
清单 4 演示了基间接寻址模式(第 64 行),也就是首先取得一个基地址,然后在它上面加一个偏移量,从而到达最终的地址。在清单的 NASM 部分中,[ebp + 8] 和 [ebp 4](第 71 行)就是基间接寻址模式的示例。在 GAS 中,寻址方法更简单一些:4(%ebp) 和 -4(%ebp)。
在 print_array10 例程中,在 push_loop 标签后面可以看到另一种寻址模式(第 74 行)。在 NASM 和 GAS 中的表示方法如下:
NASM:mov al, byte [ebx + esi]
GAS:movb (%ebx, %esi, 1), %al
这种寻址模式称为基索引寻址模式。这里有三项数据:一个是基地址,第二个是索引寄存器,第三个是乘数。因为不可能决定从一个内存位置开始访问的字节数,所以需要用一个方法计算访问的内存量。NASM 使用字节操作符告诉汇编器要移动一个字节的数据。在 GAS 中,用一个乘数和助记符中的 b、w 或 l 后缀(例如 movb)来解决这个问题。初看上去 GAS 的语法似乎有点儿复杂。
GAS 中基索引寻址模式的一般形式如下:
%segment:ADDRESS (, index, multiplier)
或
%segment:(offset, index, multiplier)
或
%segment:ADDRESS(base, index, multiplier)
使用这个公式计算最终的地址:
ADDRESS or offset + base + index * multiplier.
因此,要想访问一个字节,就使用乘数 1;对于字,乘数是 2;对于双字,乘数是 4。当然,NASM 使用的语法比较简单。上面的公式在 NASM 中表示为:
Segment:[ADDRESS or offset + index * multiplier]
为了访问 1、2 或 4 字节的内存,在这个内存地址前面分别加上 byte、word 或 dword。
清单 5. 读取命令行参数,将它们存储在内存中,然后输出它们 行号NASMGAS
001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056057058059060061
section .data; Command table to store at most; 10 command line arguments cmd_tbl: %rep 10 dd 0 %endrepsection .text global _start _start:; Set up the stack frame mov ebp, esp; Top of stack contains the; number of command line arguments.; The default value is 1 mov ecx, [ebp]; Exit if arguments are more than 10 cmp ecx, 10 jg _exit mov esi, 1 mov edi, 0; Store the command line arguments; in the command table store_loop: mov eax, [ebp + esi * 4] mov [cmd_tbl + edi * 4], eax inc esi inc edi loop store_loop mov ecx, edi mov esi, 0 extern puts print_loop:; Make some local space sub esp, 4; puts function corrupts ecx mov [ebp - 4], ecx mov eax, [cmd_tbl + esi * 4] push eax call puts add esp, 4 mov ecx, [ebp - 4] inc esi loop print_loop jmp _exit _exit: mov eax, 1 mov ebx, 0 int 80h
.section .data// Command table to store at most// 10 command line arguments cmd_tbl: .rept 10 .long 0 .endr.section .text .globl _start _start:// Set up the stack frame movl %esp, %ebp// Top of stack contains the// number of command line arguments.// The default value is 1 movl (%ebp), %ecx// Exit if arguments are more than 10 cmpl $10, %ecx jg _exit movl $1, %esi movl $0, %edi// Store the command line arguments// in the command table store_loop: movl (%ebp, %esi, 4), %eax movl %eax, cmd_tbl( , %edi, 4) incl %esi incl %edi loop store_loop movl %edi, %ecx movl $0, %esi print_loop:// Make some local space subl $4, %esp// puts functions corrupts ecx movl %ecx, -4(%ebp) movl cmd_tbl( , %esi, 4), %eax pushl %eax call puts addl $4, %esp movl -4(%ebp), %ecx incl %esi loop print_loop jmp _exit _exit: movl $1, %eax movl $0, %ebx int $0x80
清单 5 演示在汇编程序中重复执行指令的方法。很自然,这种结构称为重复结构。在 GAS 中,重复结构以 .rept 指令开头(第 6 行)。用一个 .endr 指令结束这个指令(第 8 行)。.rept 后面是一个数字,它指定 .rept/.endr 结构中表达式重复执行的次数。这个结构中的任何指令都相当于编写这个指令 count 次,每次重复占据单独的一行。
例如,如果次数是 3:
.rept 3
? ? ?movl $2, %eax
.endr
就相当于:
movl $2, %eax
movl $2, %eax
movl $2, %eax
在 NASM 中,在预处理器级使用相似的结构。它以 %rep 指令开头,以 %endrep 结尾。%rep 指令后面是一个表达式(在 GAS 中 .rept 指令后面是一个数字):
%rep <expression>
? ? ?nop
%endrep
在 NASM 中还有另一种结构,times 指令。与 %rep 相似,它也在汇编级起作用,后面也是一个表达式。例如,上面的 %rep 结构相当于:
times <expression> nop
以下代码:
%rep 3
? ? ?mov eax, 2
%endrep
相当于:
times 3 mov eax, 2
它们都相当于:
mov eax, 2
mov eax, 2
mov eax, 2
在清单 5 中,使用 .rept(或 %rep)指令为 10 个双字创建内存数据区。然后,从堆栈一个个地访问命令行参数,并将它们存储在内存区中,直到命令表填满。
在这两种汇编器中,访问命令行参数的方法是相似的。ESP(堆栈顶部)存储传递给程序的命令行参数数量,默认值是 1(表示没有命令行参数)。esp + 4 存储第一个命令行参数,这总是从命令行调用的程序的名称。esp + 8、esp + 12 等存储后续命令行参数。
还要注意清单 5 中从两边访问内存命令表的方法。这里使用内存间接寻址模式(第 31 行)访问命令表,还使用了 ESI(和 EDI)中的偏移量和一个乘数。因此,NASM 中的 [cmd_tbl + esi * 4] 相当于 GAS 中的 cmd_tbl(, %esi, 4)。
?
结束语
尽管在这两种汇编器之间存在实质性的差异,但是在这两种形式之间进行转换并不困难。您最初可能觉得 AT&T 语法难以理解,但是掌握了它之后,它其实和 Intel 语法同样简单。