Linux汇编语言及嵌入式汇编
(2008-04-28 10:34:18)
汇编语言的优点是速度快,可以直接对硬件进行操作。Linux是用C语言开发的操作系统,可以在Linux中直接使用汇编这一底层语言来优化程序的性能。
汇编语言虽然运用不像高级语言那么广泛,但是却很重要。特别是在一些执行速度要求很高的场合,如Linux这样的操作系统。Linux中引导程序、启动程序及内核程序中都有很多汇编程序或嵌入式汇编程序。
汇编语言具有如下优点:
能够直接访问与硬件相关的存储器和I/O端口
不受编译器限制,对生成的二进制代码进行完全控制
能够对关键代码进行控制,避免因线程共同范围或硬件设备共享引起的死锁
对代码进行优化,提高执行速度
同时,汇编语言也有不容忽视的缺点:
代码难懂,不易维护
容易产生bug,调试困难
只能针对特定的体系结构和处理器进行优化
开放效率低,周期长
Linux下用汇编语言编写的代码有两种形式:一种是完全的汇编代码,另外一种是内嵌的汇编代码,即可以内嵌到C语言的汇编代码片段,这主要依赖于编译器在这方面的扩展。
Linux汇编语言格式
不同于DOS/Windows下的Intel风格的汇编语言,Unix/Linux下的汇编语法风格主要是AT&T模式的。
1.
在AT&T汇编格式中,寄存器名前要加上“%”作为前缀。
AT&T格式 |
Intel格式 |
push
%eax |
push
eax |
2.
在AT&T汇编格式中,用‘$’前缀表示一个立即操作数;在Intel汇编格式中,立即数的表示不用带任何前缀。
AT&T格式 |
Intel格式 |
pushl
$1 |
push
1 |
3.
AT&T和Intel格式中的源操作数和目的操作数的位置恰好相反。在Intel格式中,目的操作数在左边;而在AT&T格式中,目的操作数在右边。
AT&T格式 |
Intel格式 |
addl $1,
%eax |
add eax,
1 |
4.
在AT&T格式中,操作数的长度由操作符的最后一个字母决定,后缀‘b’,‘w’,‘l’分别表示操作数为字节(byte,
8bits),字(word, 16bits)和长字(long, 32bits);而在Intel格式中,操作数的字长由“byte ptr”和“word ptr”等前缀来表示。
AT&T格式 |
Intel格式 |
movb val,
%al |
mov al, byte ptr
val |
1.
在AT&T汇编格式中,绝对转移和调用指令(jump/call)的操作数前要加上‘*’
最为前缀,而在Intel格式中则不需要。
2.
远程转移指令和远程子调用指令的操作码,在AT&T汇编格式中为‘ljump’和‘lcall’;而在Intel汇编格式中为‘jmp
far’和‘call far’
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.
在AT&T汇编格式中,内存操作数的寻址方式是
section:disp(base, index,
scale) |
而在Intel汇编格式中,内存操作数的寻址方式是:
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 : .string “hello, world!” #要输出的字符串
len = . – msg #字符串长度
.text #代码段声明
.global _start #指定入口函数
_start:
movl $len, %edx
movl $msg, %ecx
movl $1, %ebx #文件描述符stdout
movl $4, %eax #系统调用号(sys_write)
int $0x80 #调用内核功能
#退出程序
movl $0, %ebx #退出代码
movl $1, %eax #系统调用号(sys_exit)
int $0x80

Intel格式的Hello, World!
; hello.asm
section .data ;数据段声明
msg db “Hello, world!”, 0xA
len equ $ -msg
section .text
global _start
_start:
mov edx, len
mov ecx, msg
mov ebx, 1
mov eax, 4
int 0x80

mov ebx, 0
mov eax, 1
int 0x80

上面两种汇编格式都是Linux内核提供的sys_write来显示一个字符串,然后再调用sys_exit退出程序。可以再include/asm-i386/unistd.h中,找到系统调用的定义。
Linux汇编工具
Linux平台下汇编工具仍然是汇编器、链接器和调试器
汇编器
汇编器(assembler)作用是将汇编语言编写的源程序转换成二进制形式的目标代码。Linux平台的标准汇编器是GAS,使用AT&T汇编语法。
as –o
hello.o hello.s
Linux平台下另一个常用到的汇编器是NASM,提供了很好的宏指令功能,并支持相当多的目标代码格式,包括bin,
a.out, coff, elf, rdf等。NASM使用Intel汇编语法。
链接器
链接器用来将多个目标代码连接成一个可执行代码。Linux下使用ld作为标准的链接程序。
ld –s –o hello hello.o
调试器
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
_start:
popl %ecx #argc
vnext:
popl %ecx #argv
test %ecx, %ecx #空指针表明结束
jz exit
movl %ecx, %ebx
xorl %edx, %edx
strlen:
movb (%ebx), %al
inc %edx
inc %ebx
test %al, %al
jnz strlen
movb $10, -1(%ebx)
movl $4, %eax #sys_write
movl $1, %ebx
int $0x80
jmp vnext
exit:
movl $1, %eax #系统调用号sys_exit
xorl %ebx, %ebx
int 0x80

ret

Linux嵌入式汇编
嵌入式汇编的基本格式是:
asm(“汇编语句”
:输出寄存器
:输入寄存器
:会被修改的寄存器);
其中,汇编语句是放汇编语言的地方;输出寄存器表示那些寄存器存放数据,这些寄存器分别对应一个C语言表达式或一个内存地址;输入寄存器表示开始执行汇编代码时,指定的一些寄存器中应存放的输入值,也分别对应一个C变量或常数值。
#define get_seg_byte(seg, addr)
({
register char __res;
__asm__(“push %%fs;
mov %%ax, %%fs;
movb %%fs:%2, %%al;
pop %%fs”
:”=a”(__res)
:”””(seg), “m”(*(addr)));
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位) |
|
|
|