【汇编】Linux 下汇编程序开发
一,简介
Linux 下用汇编语言编写的代码具有两种不同的形式。
1)完全的汇编代码
指的是整个程序全部用汇编语言编写。尽管是完全的汇编代码,Linux 平台下的汇编工具也吸收了 C 语言的长处,使得程序员可以使用 #include、#ifdef 等预处理指令,并能够通过宏定义来简化代码。
2)内嵌的汇编代码
指的是可以嵌入到C语言程序中的汇编代码片段。虽然 ANSI 的 C 语言标准中没有关于内嵌汇编代码的相应规定,但各种实际使用的 C 编译器都做了这方面的扩充,这其中当然就包括 Linux 平台下的 GCC。
二,Linux 环境下汇编语言的分类
绝大多数 Linux 程序员以前只接触过DOS/Windows 下的汇编语言,这些汇编代码都是 Intel 风格的。但在 Unix 和 Linux 系统中,更多采用的还是 AT&T 格式,两者在语法格式上有着很大的不同:
1)在 AT&T 汇编格式中,寄存器名要加上 '%' 作为前缀; pushl %eax
而在 Intel 汇编格式中,寄存器名不需要加前缀。 push eax
2)在 AT&T 汇编格式中,用 '$' 前缀表示一个立即操作数; pushl $1
而在 Intel 汇编格式中,立即数的表示不用带任何前缀。 push 1
3)AT&T 和 Intel 格式中的源操作数和目标操作数的位置正好相反。
在 Intel 汇编格式中,目标操作数在源操作数的左边; addl $1, %eax
而在 AT&T 汇编格式中,目标操作数在源操作数的右边。 add eax, 1
4)在 AT&T 汇编格式中,操作数的字长由操作符的最后一个字母决定,后缀'b'、'w'、'l'分别表示操作数为字节(byte,8 比特)、字(word,16 比特)和长字(long,32比特);
而在 Intel 汇编格式中,操作数的字长是用 "byte ptr" 和 "word ptr" 等前缀来表示的。例如:
AT&T 格式 : movb val,%al
Intel 格式 : mov al, byte ptr val
5)在 AT&T 汇编格式中,绝对转移和调用指令(jump/call)的操作数前要加上'*'作为前缀,而在 Intel 格式中则不需要。
6)远程转移指令和远程子调用指令的操作码,在 AT&T 汇编格式中为 "ljump" 和 "lcall",而在 Intel 汇编格式中则为 "jmp far" 和 "call far",即:
AT&T 格式 :ljump $section, $offset
lcall $section, $offset
Intel 格式: call far section:offset
jmp far section:offset
与之相应的远程返回指令则为:
AT&T 格式 :lret $stack_adjust
Intel 格式:ret far stack_adjust
7) 在 AT&T 汇编格式中,内存操作数的寻址方式是section:disp(base, index, scale)
而在 Intel 汇编格式中,内存操作数的寻址方式为:section:[base + index*scale + disp]
由于 Linux 工作在保护模式下,用的是 32 位线性地址,所以在计算地址时不用考虑段基址和偏移量,而是采用如下的地址计算方法:disp + base + index * scale
三,Hello World
Linux 是一个运行在保护模式下的 32 位操作系统,采用 flat memory 模式,目前最常用到的是 ELF 格式的二进制代码。一个 ELF 格式的可执行程序通常划分为如下几个部分:.text、.data 和 .bss,其中 .text 是只读的代码区,.data 是可读可写的数据区,而 .bss 则是可读可写且没有初始化的数据区。代码区和数据区在 ELF 中统称为 section,根据实际需要你可以使用其它标准的 section,也可以添加自定义 section,但一个 ELF 可执行程序至少应该有一个 .text 部分。 下面给出我们的第一个汇编程序,用的是 AT&T 汇编语言格式:
例1. AT&T 格式
/* inline.c */int main(){ int a = 10, b = 0; __asm__ __volatile__("movl %1, %%eax;\\n\\r" "movl %%eax, %0;" :"=r"(b) /* 输出 */ :"r"(a) /* 输入 */ :"%eax"); /* 不受影响的寄存器 */ printf("Result: %d, %d\\n", a, b);}上面的程序完成将变量a的值赋予变量b,有几点需要说明:
变量b是输出操作数,通过%0来引用,而变量a是输入操作数,通过%1来引用。
输入操作数和输出操作数都使用r进行约束,表示将变量a和变量b存储在寄存器中。输入约束和输出约束的不同点在于输出约束多一个约束修饰符'='。
在内联汇编语句中使用寄存器eax时,寄存器名前应该加两个'%',即%%eax。内联汇编中使用%0、%1等来标识变量,任何只带一个'%'的标识符都看成是操作数,而不是寄存器。
内联汇编语句的最后一个部分告诉GCC它将改变寄存器eax中的值,GCC在处理时不应使用该寄存器来存储任何其它的值。
由于变量b被指定成输出操作数,当内联汇编语句执行完毕后,它所保存的值将被更新。
在内联汇编中用到的操作数从输出部的第一个约束开始编号,序号从0开始,每个约束记数一次,指令部要引用这些操作数时,只需在序号前加上'%'作为前缀就可以了。需要注意的是,内联汇编语句的指令部在引用一个操作数时总是将其作为32位的长字使用,但实际情况可能需要的是字或字节,因此应该在约束中指明正确的限定符: