1、Linux 汇编语言开发指南级别: 初级肖文鹏 (), 北京理工大学计算机系硕士研究生2003 年 7 月 03 日汇编语言的优点是速度快,可以直接对硬件进行操作,这对诸如图形处理等关键应用是非常重要的。Linux 是一个用 C 语言开发的操作系统,这使得很多程序员开始忘记在 Linux 中还可以直接使用汇编这一底层语言来优化程序的性能。本文为那些在 Linux 平台上编写汇编代码的程序员提供指南,介绍 Linux 汇编语言的语法格式和开发工具,并辅以具体的例子讲述如何开发实用的 Linux 汇编程序。一、简介作为最基本的编程语言之一,汇编语言虽然应用的范围不算很广,但重要性却勿庸置疑,因为它
2、能够完成许多其它语言所无法完成的功能。就拿 Linux 内核来讲,虽然绝大部分代码是用 C 语言编写的,但仍然不可避免地在某些关键地方使用了汇编代码,其中主要是在 Linux 的启动部分。由于这部分代码与硬件的关系非常密切,即使是 C 语言也会有些力不从心,而汇编语言则能够很好扬长避短,最大限度地发挥硬件的性能。大多数情况下 Linux 程序员不需要使用汇编语言,因为即便是硬件驱动这样的底层程序在 Linux 操作系统中也可以用完全用 C 语言来实现,再加上 GCC 这一优秀的编译器目前已经能够对最终生成的代码进行很好的优化,的确有足够的理由让我们可以暂时将汇编语言抛在一边了。但实现情况是 L
3、inux 程序员有时还是需要使用汇编,或者不得不使用汇编,理由很简单:精简、高效和 libc 无关性。假设要移植 Linux 到某一特定的嵌入式硬件环境下,首先必然面临如何减少系统大小、提高执行效率等问题,此时或许只有汇编语言能帮上忙了。汇编语言直接同计算机的底层软件甚至硬件进行交互,它具有如下一些优点: 能够直接访问与硬件相关的存储器或 I/O 端口; 能够不受编译器的限制,对生成的二进制代码进行完全的控制; 能够对关键代码进行更准确的控制,避免因线程共同访问或者硬件设备共享引起的死锁; 能够根据特定的应用对代码做最佳的优化,提高运行速度; 能够最大限度地发挥硬件的功能。 同时还应该认识到,
4、汇编语言是一种层次非常低的语言,它仅仅高于直接手工编写二进制的机器指令码,因此不可避免地存在一些缺点: 编写的代码非常难懂,不好维护; 很容易产生 bug,难于调试; 只能针对特定的体系结构和处理器进行优化; 开发效率很低,时间长且单调。 Linux 下用汇编语言编写的代码具有两种不同的形式。第一种是完全的汇编代码,指的是整个程序全部用汇编语言编写。尽管是完全的汇编代码,Linux 平台下的汇编工具也吸收了 C 语言的长处,使得程序员可以使用 #include、#ifdef 等预处理指令,并能够通过宏定义来简化代码。第二种是内嵌的汇编代码,指的是可以嵌入到 C 语言程序中的汇编代码片段。虽然
5、ANSI 的 C 语言标准中没有关于内嵌汇编代码的相应规定,但各种实际使用的 C 编译器都做了这方面的扩充,这其中当然就包括 Linux 平台下的 GCC。打印本页将此页作为电子邮件发送二、Linux 汇编语法格式绝大多数 Linux 程序员以前只接触过 DOS/Windows 下的汇编语言,这些汇编代码都是 Intel 风格的。但在 Unix 和 Linux 系统中,更多采用的还是 AT hello.asm section .data ; 数据段声明msg db “Hello, world!“, 0xA ; 要输出的字符串len equ $ - msg ; 字串长度section .text
6、 ; 代码段声明global _start ; 指定入口函数_start: ; 在屏幕上显示一个字符串mov edx, len ; 参数三:字符串长度mov ecx, msg ; 参数二:要显示的字符串mov ebx, 1 ; 参数一:文件描述符 (stdout) mov eax, 4 ; 系统调用号 (sys_write) int 0x80 ; 调用内核功能; 退出程序mov ebx, 0 ; 参数一:退出代码mov eax, 1 ; 系统调用号 (sys_exit) int 0x80 ; 调用内核功能上面两个汇编程序采用的语法虽然完全不同,但功能却都是调用 Linux 内核提供的 sys_
7、write 来显示一个字符串,然后再调用 sys_exit 退出程序。在 Linux 内核源文件 include/asm-i386/unistd.h 中,可以找到所有系统调用的定义。四、Linux 汇编工具Linux 平台下的汇编工具虽然种类很多,但同 DOS/Windows 一样,最基本的仍然是汇编器、连接器和调试器。1.汇编器 回页首汇编器(assembler)的作用是将用汇编语言编写的源程序转换成二进制形式的目标代码。Linux 平台的标准汇编器是 GAS,它是 GCC 所依赖的后台汇编工具,通常包含在 binutils 软件包中。GAS 使用标准的 AT该函数的功能最终是通过 SYS_
8、write 这一系统调用来实现的。根据上面的约定,参数 fb、buf 和 count 分别存在寄存器 ebx、ecx 和 edx 中,而系统调用号 SYS_write 则放在寄存器 eax 中,当 int 0x80 指令执行完毕后,返回值可以从寄存器 eax 中获得。或许你已经发现,在进行系统调用时至多只有 5 个寄存器能够用来保存参数,难道所有系统调用的参数个数都不超过 5 吗?当然不是,例如 mmap 函数就有 6 个参数,这些参数最后都需要传递给系统调用 SYS_mmap:void * mmap(void *start, size_t length, int prot , int fla
9、gs, int fd, off_t offset);当一个系统调用所需的参数个数大于 5 时,执行 int 0x80 指令时仍需将系统调用功能号保存在寄存器 eax 中,所不同的只是全部参数应该依次放在一块连续的内存区域里,同时在寄存器 ebx 中保存指向该内存区域的指针。系统调用完成之后,返回值仍将保存在寄存器 eax 中。由于只是需要一块连续的内存区域来保存系统调用的参数,因此完全可以像普通的函数调用一样使用栈(stack)来传递系统调用所需的参数。但要注意一点,Linux 采用的是 C 语言的调用模式,这就意味着所有参数必须以相反的顺序进栈,即最后一个参数先入栈,而第一个参数则最后入栈。
10、如果采用栈来传递系统调用所需的参数,在执行 int 0x80 指令时还应该将栈指针的当前值复制到寄存器 ebx 中。六、命令行参数在 Linux 操作系统中,当一个可执行程序通过命令行启动时,其所需的参数将被保存到栈中:首先是 argc,然后是指向各个命令行参数的指针数组 argv,最后是指向环境变量的指针数据 envp。在编写汇编语言程序时,很多时候需要对这些参数进行处理,下面的代码示范了如何在汇编代码中进行命令行参数的处理:例 3. 处理命令行参数# args.s回页首.text.globl _start_start:popl %ecx # argcvnext:popl %ecx # ar
11、gvtest %ecx, %ecx # 空指针表明结束jzexitmovl %ecx, %ebxxorl %edx, %edxstrlen:movb (%ebx), %alinc %edxinc %ebxtest %al, %aljnz strlenmovb $10, -1(%ebx)movl $4, %eax # 系统调用号(sys_write) movl $1, %ebx # 文件描述符(stdout) int $0x80jmp vnextexit:movl $1,%eax # 系统调用号(sys_exit) xorl %ebx, %ebx # 退出代码int $0x80ret七、GCC
12、内联汇编用汇编编写的程序虽然运行速度快,但开发速度非常慢,效率也很低。如果只是想对关键代码段进行优化,或许更好的办法是将汇编指令嵌入到 C 语言程序中,从而充分利用高级语言和汇编语言各自的特点。但一般来讲,在 C 代码中嵌入汇编语句要比“纯粹“的汇编语言代码复杂得多,因为需要解决如何分配寄存器,以及如何与 C 代码中的变量相结合等问题。GCC 提供了很好的内联汇编支持,最基本的格式是:_asm_(“asm statements“);回页首例如:_asm_(“nop“); 如果需要同时执行多条汇编语句,则应该用“nt“将各个语句分隔开,例如:_asm_( “pushl %eax nt“movl
13、$0, %eax nt“popl %eax“);通常嵌入到 C 代码中的汇编语句很难做到与其它部分没有任何关系,因此更多时候需要用到完整的内联汇编格式:_asm_(“asm statements“ : outputs : inputs : registers-modified);插入到 C 代码中的汇编语句是以“:“分隔的四个部分,其中第一部分就是汇编代码本身,通常称为指令部,其格式和在汇编语言中使用的格式基本相同。指令部分是必须的,而其它部分则可以根据实际情况而省略。在将汇编语句嵌入到 C 代码中时,操作数如何与 C 代码中的变量相结合是个很大的问题。GCC 采用如下方法来解决这个问题:程序
14、员提供具体的指令,而对寄存器的使用则只需给出“样板“和约束条件就可以了,具体如何将寄存器与变量结合起来完全由GCC 和 GAS 来负责。在 GCC 内联汇编语句的指令部中,加上前缀%的数字(如%0,%1)表示的就是需要使用寄存器的“样板“ 操作数。指令部中使用了几个样板操作数,就表明有几个变量需要与寄存器相结合,这样 GCC 和 GAS 在编译和汇编时会根据后面给定的约束条件进行恰当的处理。由于样板操作数也使用%作为前缀,因此在涉及到具体的寄存器时,寄存器名前面应该加上两个%,以免产生混淆。紧跟在指令部后面的是输出部,是规定输出变量如何与样板操作数进行结合的条件,每个条件称为一个“约束“ ,必
15、要时可以包含多个约束,相互之间用逗号分隔开就可以了。每个输出约束都以=号开始,然后紧跟一个对操作数类型进行说明的字后,最后是如何与变量相结合的约束。凡是与输出部中说明的操作数相结合的寄存器或操作数本身,在执行完嵌入的汇编代码后均不保留执行之前的内容,这是 GCC 在调度寄存器时所使用的依据。输出部后面是输入部,输入约束的格式和输出约束相似,但不带=号。如果一个输入约束要求使用寄存器,则 GCC 在预处理时就会为之分配一个寄存器,并插入必要的指令将操作数装入该寄存器。与输入部中说明的操作数结合的寄存器或操作数本身,在执行完嵌入的汇编代码后也不保留执行之前的内容。有时在进行某些操作时,除了要用到进行数据输入和输出的寄存器外,还要使用多个寄存器来保存中间计算结果,这样就难免会破坏原有寄存器的内容。在 GCC 内联汇编格式中的最后一个部分中,可以对将产生副作用的寄存器进行说明,以便 GCC能够采用相应的措施。下面是一个内联汇编的简单例子:例 4.内联汇编/* inline.c */int main()int a = 10, b = 0;_asm_ _volatile_(“movl %1, %eax;nr“movl %eax, %0;“:“=r“(b) /* 输出 */ :“r“(a) /* 输入 */:“%eax“); /* 不受影响的寄存器 */