前缀
在Intel汇编中没有寄存器前缀或者立即数前缀。而在AT&T汇编中寄存器有一个“%”前缀,立即数
有一个“$”前缀。Intel语句中十六进制和二进制数据分别带有“h”和“b”后缀,并且如果十六
进制
数字的第一位是字母的话,那么数值的前面要加一个“0”前缀。
例如,
Intel Syntax
mov eax,1
mov ebx,0ffh
int 80h
AT&T Syntax
movl $1,%eax
movl $0xff,%ebx
int $0x80
就像你看到的,AT&T非常难懂。[base+index*scale+disp]
看起来比disp(base,index,scale)更好理解。
操作数的用法
intel语句中操作数的用法和AT&T中的用法相反。在Intel语句中,第一个操作数表示目的,第二个
操作数表示源。然而在AT&T语句中第一个操作数表示源而第二个操作数表示目的。在这种情形下AT
&T语法的好处是显而易见的。我们从左向右读,也从左向右写,这样比较自然。
例如,
Intex Syntax
instr dest,source
mov eax,[ecx]
AT&T Syntax
instr source,dest
movl (%ecx),%eax
存储器操作数
如同上面所看到的,存储器操作数的用法也不相同。在Intel语句中基址寄存器用“[”和“]”括?br>鹄炊贏T&T语句中是用“(”和“)”括起来的。
例如,
Intex Syntax
mov eax,[ebx]
mov eax,[ebx+3]
AT&T Syntax
movl (%ebx),%eax
movl 3(%ebx),%eax
AT&T语法中用来处理复杂的操作的指令的形式和Intel语法中的形式比较起来要难懂得多。在Intel
语句中这样的形式是segreg:[base+index*scale+disp]。在AT&T语句中这样的形式是%segreg:disp
(base,index,scale)。
Index/scale/disp/segreg
都是可选并且可以去掉的。Scale在本身没有说明而index已指定的情况下缺省值为1。segreg的确?br>ㄒ览涤谥噶畋旧硪约俺绦蛟诵性谑的J交故莗mode。在实模式下它依赖于指令本身而pmode模式下?br>遣恍枰摹T贏T&T语句中用作scale/disp的立即数不要加“$”前缀。
例如
Intel Syntax
instr foo,segreg:[base+index*scale+disp]
mov eax,[ebx+20h]
add eax,[ebx+ecx*2h]
lea eax,[ebx+ecx]
sub eax,[ebx+ecx*4h-20h]
AT&T Syntax
instr %segreg:disp(base,index,scale),foo
movl 0x20(%ebx),%eax
addl (%ebx,%ecx,0x2),%eax
leal (%ebx,%ecx),%eax
subl -0x20(%ebx,%ecx,0x4),%eax
后缀
就像你已经注意到的,AT&T语法中有一个后缀,它的意义是表示操作数的大小。“l”代表long,?br>皐”代表word,“b”代表byte。Intel语法中在处理存储器操作数时也有类似的表示,如byte
ptr, word ptr, dword ptr。"dword"
显然对应于“long”。这有点类似于C语言中定义的类型,但是既然使用的寄存器的大小对应着假?br>ǖ氖堇嘈停庋拖缘貌槐匾恕?
例子:
Intel Syntax
mov al,bl
mov ax,bx
mov eax,ebx
mov eax, dword ptr [ebx]
AT&T Syntax
movb %bl,%al
movw %bx,%ax
movl %ebx,%eax
movl (%ebx),%eax
注意:从此开始所有的例子都使用AT&T语法
系统调用
本节将介绍linux中汇编语言系统调用的用法。系统调用包括位于/usr/man/man2的手册里第二部分
所有的函数。这些函数也在/usr/include/sys/syscall.h中列出来了。一个重要的关于这些函数的
列表是在http://www.linuxassembly.org/syscall.html里。这些函数通过linux中断服务:int
$0x80来被执行
小于六个参数的系统调用
对于所有的系统调用,系统调用号在%eax中。对于小于六个参数的系统调用,参数依次存放在%ebx
,%ecx,%edx,%esi,%edi中,系统调用的返回值保存在%eax中。
系统调用号可以在/usr/include/sys/syscall.h中找到。宏被定义成SYS_name>的形式,如SYS_exit, SYS_close等。
例子:(hello world 程序)
参照write(2)的帮助手册,写操作被声明为ssize_t write(int fd, const void *buf, size_t
count);
这样,fd应存放在%ebx中,buf放在 %ecx, count 放在 %edx , SYS_write 放在
%eax中,紧跟着是int $0x80语句来执行系统调用。系统调用的返回值保嬖?eax中。
$ cat write.s
.include "defines.h"
.data
hello:
.string "hello world/n"
.globl main
main:
movl $SYS_write,%eax
movl $STDOUT,%ebx
movl $hello,%ecx
movl $12,%edx
int $0x80
ret
$
少于5个参数的系统调用的处理也是这样的。只是没有用到的寄存器保持不变罢了。象open或者fcn
tl这样带有一个可选的额外参数的系统调用也就知道怎么用了。
大于5个参数的系统调用
参数个数大于五个的系统调用仍然把系统调用号保存在%eax中,但是参数存放在内存中,并且指向
第一个参数的指针保存在%ebx中。
如果你使用栈,参数必须被逆序压进栈里,即按最后一个参数到第一个参数的顺序。然后将栈的指
针拷贝到%ebx中。或者将参数拷贝到一块分配的内存区域,然后把第一个参数的地址保存在%ebx中
。
例子:(使用mmap作为系统调用的例子)。在C中使用mmap():
#define STDOUT 1
void main(void) {
char file[]="mmap.s";
char *mappedptr;
int fd,filelen;
fd=fopen(file, O_RDONLY);
filelen=lseek(fd,0,SEEK_END);
mappedptr=mmap(NULL,filelen,PROT_READ,MAP_SHARED,fd,0);
write(STDOUT, mappedptr, filelen);
munmap(mappedptr, filelen);
close(fd);
}
mmap()参数在内存中的排列:
%esp %esp+4 %esp+8 %esp+12 %esp+16 %esp+20
00000000 filelen 00000001 00000001 fd 00000000
等价的汇编程序:
$ cat mmap.s
.include "defines.h"
.data
file: .string "mmap.s"
fd: .long 0
filelen: .long 0
mappedptr: .long 0
.globl main
main:
push %ebp
movl %esp,%ebp
subl $24,%esp
// open($file, $O_RDONLY);
movl $fd,%ebx // save fd
movl %eax,(%ebx)
// lseek($fd,0,$SEEK_END);
movl $filelen,%ebx // save file length
movl %eax,(%ebx)
xorl %edx,%edx
// mmap(NULL,$filelen,PROT_READ,MAP_SHARED,$fd,0);
movl %edx,(%esp)
movl %eax,4(%esp) // file length still in %eax
movl $PROT_READ,8(%esp)
movl $MAP_SHARED,12(%esp)
movl $fd,%ebx // load file descriptor
movl (%ebx),%eax
movl %eax,16(%esp)
movl %edx,20(%esp)
movl $SYS_mmap,%eax
movl %esp,%ebx
int $0x80
movl $mappedptr,%ebx // save ptr
movl %eax,(%ebx)
// write($stdout, $mappedptr, $filelen);
// munmap($mappedptr, $filelen);
// close($fd);
movl %ebp,%esp
popl %ebp
ret
$
注意:上面所列出的源代码和本文结束部分的例子的源代码不同。上面列出的代码中没有说明其它
的系统调用,因为这不是本节的重点,上面列出的源代码仅仅打开mmap.s文件,而例子的源代码要
读命令行的参数。这个mmap的例子还用到lseek来获取文件大小。
Socket系统调用
Socket系统调用使用唯一的系统调用号:SYS_socketcall,它保存在%eax中。Socket函数是通过位
于/usr/include/linux/net.h的一个子函数号来确定的,并且它们被保存在%ebx中。指向系统调用
参数的一个指针存放在%ecx中。Socket系统调用也是通过int $0x80来执行的。
$ cat socket.s
.include "defines.h"
.globl _start
_start:
pushl %ebp
movl %esp,%ebp
sub $12,%esp
// socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
movl $AF_INET,(%esp)
movl $SOCK_STREAM,4(%esp)
movl $IPPROTO_TCP,8(%esp)
movl $SYS_socketcall,%eax
movl $SYS_socketcall_socket,%ebx
movl %esp,%ecx
int $0x80
movl $SYS_exit,%eax
xorl %ebx,%ebx
int $0x80
movl %ebp,%esp
popl %ebp
ret
$
命令行参数
在linux中执行的时候命令行参数是放在栈上的。先是argc,跟着是一个由指向命令行中各字符串并以空指针结束。接下来是一个由指向环境变量的指针组成的数组(**envp)。这些东西在asm中都可以很容易的获得,并且在例子代码(args.s)中有示范。