本文来自于网络。本人仅起整理的作用。
1/寄存器名要加上 '%' 作为前缀:
pushl %eax
80386有如下寄存器:
8个32-bit寄存器 %eax,%ebx,%ecx,%edx,%edi,%esi,%ebp,%esp;
8个16-bit寄存器,它们事实上是上面8个32-bit寄存器的低16位:%ax,%bx,%cx,%dx,%di,%si,%bp,%sp;
8个8-bit寄存器:%ah,%al,%bh,%bl,%ch,%cl,%dh,%dl。它们事实上 是寄存器%ax,%bx,%cx,%dx 的高8位和低8位;
6个段寄存器:%cs(code),%ds(data),%ss(stack),%es,%fs,%gs;
3个控制寄存器:%cr0,%cr2,%cr3;
6个debug寄存器:%db0,%db1,%db2,%db3,%db6,%db7;
2个测试寄存器:%tr6,%tr7;
8 个浮点寄存器栈:%st(0),%st(1),%st(2),%st(3),%st(4),%st(5),%st(6),%st(7)。
2/用 '$' 前缀表示一个立即操作数:
pushl $1
value:.long 0x12a3f2de
movl value,%ebx
指令执行的结果是将常数0x12a3f2de装入寄存器ebx。
引用符号地址在符号前加符号$,
如“movl $value,%ebx”则是将符号value的地址装入寄存器ebx。
3/在 AT&T 汇编格式中,源操作数在左边,目标操作数在右边:
mov $2,%ebp
将立即数2放入ebp寄存器中
4/操作数的字长由操作符的最后一个字母决定,后缀'b'、'w'、'l'、'q'分别表示操作数为字节(byte,8 比特)、字(word,16 比特)和长字(long,32比特)、四字(Quadword,64比特)。
movb val, %al
将一个val中长度为一个字节的值放入al寄存器中。
如果没有指定操作数长度的话,编译器将按照目标操作数的长度来设置。
比如指令“mov %ax,%bx”,由于目标操作数bx的长度为word,那么编译器将把此指令等同于
"movw %ax,%bx"。
同样道理,指令"mov $4,%ebx"等同于指令"movl $4,%ebx".
"push%al"等同于"pushb%al"。
对于没有指定操作数长度,但编译器又无法猜测的指令,编译器将会报错,比如指令“push$4”。
5/内存操作数的寻址方式是:
section:disp(base, index, scale)
其中,base和index是任意的32-bitbase和index寄存器。
scale可以取值1,2,4,8。 如果不指定scale值,则默认值为1。
section可以指定任意的段寄存器作为段前缀,默认的段寄存器在不同的情况下不一样。
保护模式下,计算方法如下:
disp + base + index * scale
下面是一些例子:
-4(%ebp):base=%ebp,disp=-4,section没有指定,由于base=%ebp,所以默认的section=%ss,index,scale没有指定,则index为0。
foo(,%eax,4):index=%eax,scale=4,disp=foo。其它域没有指定。这里默认 的section=%ds。
foo(,1):这个表达式引用的是指针foo指向的地址所存放的值。注意这个表达式中没有base和index,并且只有一个逗号,这是一种异常语法,但却合法。
%gs:foo:这个表达式引用的是放置于%gs段里变量foo的值。
如果call和jump操作在操作数前指定前缀“*”,则表示是一个绝对地址调用/跳转,也就是说jmp/call指令指定的是一个绝对地址。如果没有指定"*",则操作数是一个相对地址。
任何指令如果其操作数是一个内存操作,则指令必须指定它的操作尺寸(byte,word,long),也就是说必须带有指令后缀(b,w,l)。
例如:
movl -4(%ebp), %eax
movl array(, %eax, 4), %eax
movw array(%ebx, %eax, 4), %cx
movb $4, %fs:(%eax)
6/Sign and Zero Extension
符号扩展指令和零扩展指令需要指定源操作数长度和目的操作数长度,即使在某些指令中这些操作数是隐含的。
在AT&T语法中,符号扩展和零扩展指令的格式为,基本部分"movs"和"movz",后面跟上源操作数长度和目的操作数长度。movsbl意味着 movs(from)byte(to)long;movsbw意味着movs(from)byte(to)word;movswl 意味着movs (from)word (to)long。对于movz指令也一样。比如指令“movsbl %al,%edx”意味着将al寄存器的内容进行符号扩展后放置到edx寄存器中。
cbtw:convert byte to word
cwtl:convert word to double word in %eax register. %ax → %eax
cwtd:convert word to double word. %ax → %dx:%ax
cltd:convert doubleword to quadword. %eax → %edx:%eax
7/一个AT&T汇编的例子:
#hello.s
.data # 数据段声明
msg : .string "Hello, world!//n" #要输出的字符串
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 #调用内核功能
#hello.s file end.
a、把上面保存成hello.s文件。
b、用GAS汇编器(通常在binutils软件包中)编译下:
as -o hello.o hello.s
c、用ld链接器链接下:
ld -s -o hello hello.o
d、运行试下:
./hello
8/系统调用
本节将介绍linux中汇编语言系统调用的用法。系统调用包括位于/usr/man/man2的手册里第二部分所有
的函数。这些函数也在/usr/include/sys/syscall.h中列出来了。这些函数通过linux中断服务:int $0x80来被执行
a、小于六个参数的系统调用
对于所有的系统调用,系统调用号在%eax中。对于小于六个参数的系统调用,参数依次存放
在%ebx,%ecx,%edx,%esi,%edi中,系统调用的返回值保存在%eax中。
系统调用号可以在/usr/include/sys/syscall.h中找到。宏被定义成SYS_的形式,
如SYS_exit, SYS_close等。
参照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或者fcntl这样
带有一个可选的额外参数的系统调用也就知道怎么用了。
b、大于5个参数的系统调用
参数个数大于五个的系统调用仍然把系统调用号保存在%eax中,但是参数存放在内存中,并且指向第一个
参数的指针保存在%ebx中。
如果你使用栈,参数必须被逆序压进栈里,即按最后一个参数到第一个参数的顺序。然后将栈的指针拷贝
到%ebx中。或者将参数拷贝到一块分配的内存区域,然后把第一个参数的地址保存在%ebx中。
例子:(使用mmap作为系统调用的例子)。在C中使用mmap():
#include
#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
$
9/命令行参数
在linux中执行的时候命令行参数是放在栈上的。先是argc,跟着是一个由指向命令行中各字符串的
指针组成的数组(**argv)并以空指针结束。接下来是一个由指向环境变量的指针组成的
数组(**envp)。这些东西在asm中都可以很容易的获得。
10/GCC内联汇编
本节中GCC内联汇编仅涉及x86的应用程序。操作数约束会和其它处理器上的有所不同。关于这部分
的说明放在本文的最后。
gcc中基本的内联汇编非常易懂,如
__asm__("movl %esp,%eax"); // look familiar ?
或者是
__asm__("
movl $1,%eax // SYS_exit
xor %ebx,%ebx
int $0x80
");
如果指定了用作asm的输入、输出数据并指出哪一个寄存器会被修改,会使程序的执行效率提高。
input/output/modify都不是必需的。格式如下:
__asm__("" : output : input : modify);
output和input中必须包含一个操作数约束字符串,并紧跟一个用圆括号括起来的C语言表达式。
输出操作数约束的前面必须有一个“=”,表示这是一个输出。可能会有多个输出,多个输入和
多个修改过的寄存器。每个“入口”应该用“,”分隔开,并且入口的总数不多有10个。
操作数约束字符串可以是包含整个寄存器的名称也可以是简写。
a %eax/%ax/%al
b %ebx/%bx/%bl
c %ecx/%cx/%cl
d %edx/%dx/%dl
S %esi/%si
D %edi/%di
m memory
例如:
__asm__("test %%eax,%%eax", : /* no output */ : "a"(foo));
或者是
__asm__("test %%eax,%%eax", : /* no output */ : "eax"(foo));
可以利用在__asm__后使用关键字__volatile__的方法防止指令被优化。
$ cat inline1.c
#include
int main(void) {
int foo=10,bar=15;
__asm__ __volatile__ ("addl %%ebxx,%%eax"
: "=eax"(foo) // ouput
: "eax"(foo), "ebx"(bar)// input
: "eax" // modify
);
printf("foo+bar=%d/n", foo);
return 0;
}
$
你可能已经注意到现在寄存器使用“%%”前缀而不是“%”。这在使用output/input/modify域时是必要的。
你可以很简单的指定“a”而不是写“eax”或者强制使用一个特殊寄存器如"eax"、"ax"、"al"。
gcc提供了寄存器别名。最多有10个别名(%0—%9),这也是为什么只允许10个输入/输出的原因。
$ cat inline2.c
int main(void) {
long eax;
short bx;
char cl;
__asm__("nop;nop;nop"); // to separate inline asm from the rest of the code
__volatile__ __asm__("
test %0,%0
test %1,%1
test %2,%2"
: /* no outputs */
: "a"((long)eax), "b"((short)bx), "c"((char)cl)
);
__asm__("nop;nop;nop");
return 0;
}
编译:
$ gcc -o inline2 inline2.c
调试:
$ gdb ./inline2
Dump of assembler code for function main:
... start: inline asm ...
0x8048427 : nop
0x8048428 : nop
0x8048429 : nop
0x804842a : mov 0xfffffffc(%ebp),%eax
0x804842d : mov 0xfffffffa(%ebp),%bx
0x8048431 : mov 0xfffffff9(%ebp),%cl
0x8048434 : test %eax,%eax
0x8048436 : test %bx,%bx
0x8048439 : test %cl,%cl
0x804843b : nop
0x804843c : nop
0x804843d : nop
... end: inline asm ...
End of assembler dump.
///////////////
三 扩展的行内汇编
扩展的行内汇编类似于Watcom.
基本的格式是:
asm ( "statements" : output_regs : input_regs : clobbered_regs);
clobbered_regs指的是被改变的寄存器.
下面是一个例子(为方便起见,我使用全局变量):
int count=1;
int value=1;
int buf[10];
void main()
{
asm(
"cld /n/t"
"rep /n/t"
"stosl"
:
: "c" (count), "a" (value) , "D" (buf[0])
: "%ecx","%edi" );
}
得到的主要汇编代码为:
movl count,%ecx
movl value,%eax
movl buf,%edi
#APP
cld
rep
stosl
#NO_APP
cld,rep,stos就不用多解释了.
这几条语句的功能是向buf中写上count个value值.
其中符号"c"(count)指示要把count的值放入ecx寄存器
类似的还有:
a eax
b ebx
c ecx
d edx
S esi
D edi
I 常数值,(0 - 31)
q,r 动态分配的寄存器
g eax,ebx,ecx,edx或内存变量
A 把eax和edx合成一个64位的寄存器(use long longs)
我们也可以让gcc自己选择合适的寄存器.
如下面的例子:
asm("leal (%1,%1,4),%0"
: "=r" (x)
: "0" (x) );
这段代码实现5*x的快速乘法.
得到的主要汇编代码为:
movl x,%eax
#APP
leal (%eax,%eax,4),%eax
#NO_APP
movl %eax,x
几点说明:
1.使用q指示编译器从eax,ebx,ecx,edx分配寄存器.
使用r指示编译器从eax,ebx,ecx,edx,esi,edi分配寄存器.
2.我们不必把编译器分配的寄存器放入改变的寄存器列表,因为寄存器已经记住了它们.
3."="是标示输出寄存器,必须这样用.
4.数字%n的用法:
数字表示的寄存器是按照出现和从左到右的顺序映射到用"r"或"q"请求
的寄存器.如果我们要重用"r"或"q"请求的寄存器的话,就可以使用它们.
5.如果强制使用固定的寄存器的话,如不用%1,而用ebx,则
asm("leal (%%ebx,%%ebx,4),%0"
: "=r" (x)
: "0" (x) );
注意要使用两个%,因为一个%的语法已经被%n用掉了.
未完。由于找到了以下两个高人写的资料。不写了。
http://download.csdn.net/source/1436505
http://download.csdn.net/source/1436526