汇编语言虽然运用不像高级语言那么广泛,但是却很重要。特别是在一些执行速度要求很高的场合,如Linux这样的操作系统。Linux中引导程序、启动程序及内核程序中都有很多汇编程序或嵌入式汇编程序。
汇编语言具有如下优点:
能够直接访问与硬件相关的存储器和I/O端口
不受编译器限制,对生成的二进制代码进行完全控制
能够对关键代码进行控制,避免因线程共同范围或硬件设备共享引起的死锁
对代码进行优化,提高执行速度
同时,汇编语言也有不容忽视的缺点:
代码难懂,不易维护
只能针对特定的体系结构和处理器进行优化
Linux下用汇编语言编写的代码有两种形式:一种是完全的汇编代码,另外一种是内嵌的汇编代码,即可以内嵌到C语言的汇编代码片段,这主要依赖于编译器在这方面的扩展。
Linux汇编语言格式
不同于DOS/Windows下的Intel风格的汇编语言,Unix/Linux下的汇编语法风格主要是AT&T模式的。
1.
AT&T格式 |
Intel格式 |
push %eax |
push eax |
2.
AT&T格式 |
Intel格式 |
pushl $1 |
push 1 |
3.
AT&T格式 |
Intel格式 |
addl $1, %eax |
add eax, 1 |
4.
AT&T格式 |
Intel格式 |
movb val, %al |
mov al, byte ptr val |
1.
最为前缀,而在Intel格式中则不需要。
2.
AT&T格式 |
Intel格式 |
ljump $section, $offset |
jmp far section:offset |
lcall $section, $offset |
call far section:offset |
AT&T格式 |
Intel格式 |
lret $stack_adjust |
ret far stack_adjust |
3.
section:disp(base, index, scale) |
section:[base + index* scale + disp] |
由于Linux工作在保护模式下,用的是32位线性地址,所以在计算地址时不用考虑段基址和偏移量,采用如下的计算方法:
disp + base + index * scale |
下面是一些内存操作数的例子:
AT&T格式 |
Intel格式 |
movl –4(%ebp), %eax |
mov eax, [ebp-4] |
movl array(,%eax, 4), %eax |
mov eax,[eax*4 + array] |
movw array(%ebx, %eax, 4), %cx |
mov cx,[ebx + 4*eax + array] |
movb $4, %fs:(%eax) |
mov fs:eax, 4 |
Hello World!
Linux下有很多方法用于在屏幕上显示一个字符串,但最简洁的方式是使用Linux内核提供的系统调用。这种方法可以直接和操作系统内核进行通讯,不需要链接如libc这样的函数库,也不需要ELF解释器。Linux是一个运行于保护模式下的32位操作系统,采用平坦内存模式,最常用到的ELF二进制代码格式包含.text, .data和.bss等section。其中.text为只读代码区,.data为可读可写的数据区,而.bss为可读可写且没有初始化的数据区。一个ELF可执行程序最少应该包含.text部分。
AT&T格式的Hello, World
#hello.s
.data
msg
len
.text
.
_start:
movl
movl
movl
movl
movl
movl
;
section
msg
len
section
_start:
mov
mov
mov
mov
int
mov
mov
int
Linux汇编工具
Linux平台下汇编工具仍然是汇编器、链接器和调试器
汇编器
汇编器(assembler)作用是将汇编语言编写的源程序转换成二进制形式的目标代码。Linux平台的标准汇编器是GAS,使用AT&T汇编语法。
Linux平台下另一个常用到的汇编器是NASM,提供了很好的宏指令功能,并支持相当多的目标代码格式,包括bin, a.out, coff, elf, rdf等。NASM使用Intel汇编语法。
链接器
链接器用来将多个目标代码连接成一个可执行代码。Linux下使用ld作为标准的链接程序。
调试器
Linux下汇编代码既可以用GDB,GDD等通用调试器,也可以用专门用来调试汇编代
码的ALD(Assembly Language Debugger)。
系统调用
程序中一般都会用到输入、输出及退出等操作。这些操作可通过系统调用来完成,即通过调用操作系统提供的服务来完成。Linux下可通过封装的C库(libc)或者汇编直接调用。通过系统调用的方法高效,因为最终生成的程序不需要与任何库进行链接,而是直接和内核通信。与DOS一样,Linux下系统调用也是通过中断(int 0x80)来完成。系统功能号在eax中,传递的参数使用寄存器参数传递,按顺序存放在ebx, ecx, edx, esi, edi中,调用完毕,返回值在eax中。系统功能号在/usr/includes/bits/syscall.h中,可用SYS_<name>宏来定义。当系统调用的参数个数大于5时,系统调用功能号仍然保存在eax中,全部参数依次保存在一块连续的内存区中,同时寄存器ebx保存指向该内存区域的指针。返回值保存在寄存器eax中。可以像普通函数调用一样使用栈(stack)来传递系统调用的参数。Linux采用c语言的调用模式,所有的参数必须以相反的顺序进栈,即最后一个参数先入栈,第一个参数最后进栈。如果采用栈来传递系统调用的所需的参数,执行int 0x80时将栈指针复制到寄存器ebx中。
命令行参数
Linux操作系统中,一个可执行程序通过命令行启动时,所需的参数保存在栈中:
首先是argc,然后是指向各个命令行参数的指针数组argv,最后是指向环境变量的
指针数据envp。
#args.s
.text
.global
_start:
popl
vnext:
popl
test
jz
movl
xorl
strlen:
movb
inc
inc
test
jnz
movb
movl
movl
int
jmp
exit:
movl
xorl
int
ret
Linux嵌入式汇编
嵌入式汇编的基本格式是:
:输出寄存器
:输入寄存器
:会被修改的寄存器);
其中,汇编语句是放汇编语言的地方;输出寄存器表示那些寄存器存放数据,这些寄存器分别对应一个C语言表达式或一个内存地址;输入寄存器表示开始执行汇编代码时,指定的一些寄存器中应存放的输入值,也分别对应一个C变量或常数值。
#define
({
res;})
嵌入汇编语言宏函数常作为一个宏,用圆括号括住的组合语句(花括号中的语句)可以作为表达式使用。:”=a”(__res)表示代码运行结束后将eax所代表的寄存器的值放入__res变量中,作为本函数的输出值,”=a”中的”a”叫加载码,”=”表示这是一个输出寄存器。”””(seg), “m”(*(addr)))为输入寄存器,其中”””表示使用与上面同个位置的输出寄存器。而”m”(*(addr))表示一个内存偏移地址值。为了在汇编语句中使用该地址值,嵌入式汇编规定把输入和输出寄存器统一按顺序编号,顺序是从输出寄存器从左到右从上到下以”%0”开始,依次记为%0, %1, %2,…。
常用寄存器加载代码说明
代码 |
说明 |
代码 |
说明 |
a |
eax |
m |
使用内存地址 |
b |
ebx |
o |
使用内存地址并加偏移值 |
c |
ecx |
I |
使用常数0-31 |
d |
edx |
J |
使用常数0-63 |
S |
使用esi |
K |
使用常数0-255 |
D |
使用edi |
L |
使用常数0-65535 |
q |
使用动态分配字节可寻址寄存器(eax, ebx, ecx或edx) |
M |
使用常数0-3 |
r |
使用任意动态分配的寄存器 |
N |
使用1字节常数(0-255) |
g |
使用通用有效的地址即可(eax, ebx, ecx, edx或内存变量) |
O |
使用常数0-31 |
A |
使用eax与edx联合(64位) |