Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1786161
  • 博文数量: 413
  • 博客积分: 8399
  • 博客等级: 中将
  • 技术积分: 4325
  • 用 户 组: 普通用户
  • 注册时间: 2011-06-09 10:44
文章分类

全部博文(413)

文章存档

2015年(1)

2014年(18)

2013年(39)

2012年(163)

2011年(192)

分类: LINUX

2011-09-20 21:12:06

Using Assembly Language in Linux.
 (同时进行了较大的修改!)

本文我们来学习Linux下的汇编语言。当然也包含了一个关于Intel与AT&T汇编语法的比较,一个关于使用系统调用和在C语言中使用嵌入汇编的指导。

写作本文是由于缺少关于这个方面的好的资料(特别是关于C语言中使用嵌入汇编的资料),因此我要提醒你本文不是一个关于如何写恶意代码的指南,因为不缺少这一方面资料。

本文的各个部分都是我通过实验学到的知识,因此也许会存在者错误。如果你发现了某处有错误,请一定不要怀疑给我发电子邮件通知我。

阅读本文只有一个唯一的前提条件,很显然必须对x86汇编和C语言有意个基本的了解。

Intel和AT&T汇编在语法上看起来十分的不同,当从一种转向另一种时,可能让人感到很困惑。所以我们从最简单的开始。


在Intel汇编语法中寄存器没有前缀,立即数也没有前缀。而在AT&T汇编语法中寄存器必须带上%的前缀,立即数必须具有前最$. Intel汇编16进制和二进制立即数必须分别带上后缀'h'和'b'。当然,如果16进制的第一个数字是字母时,最后带上前缀'0'。

Example:

Intex Syntax
mov eax,1
mov ebx,0ffh
int 80h
AT&T Syntax
movl $1,%eax
movl $0xff,%ebx
int $0x80

两种汇编中,操作数的方向是相反的。在Intel语法中第一个是目的操作数,第二个是源操作数,而在AT&T语法中,第一个源操作数,第二个是目的操作数。AT&T语法具有很明显的优势,因为我们是从左向右读和写的,这种方式是唯一自然的方法。

Example:

Intex Syntax
instr dest,source
mov eax,[ecx]
AT&T Syntax
instr source,dest
movl (%ecx),%eax

从上面可以看出内存的访问方式也不同。在Intel语法中寄存器被总括号包着的,而在AT&T语法中是被小括号包着的。

Example:

Intex Syntax
mov eax,[ebx]
mov eax,[ebx+3]
AT&T Syntax
movl (%ebx),%eax
movl 3(%ebx),%eax

The AT&T form for instructions involving complex operations is very obscure compared to Intel syntax. The Intel syntax form of these is segreg:[base+index*scale+disp]. The AT&T syntax form is %segreg:disp(base,index,scale).

AT&T涉及到复杂操作时,它的语法形式是非冲难以理解的,相比于Intel的语法而言。
Intel的语法形式是:[base+index*scale+disp]. 
而AT&T的语法形式是:%segreg:disp(base,index,scale)

Index/scale/disp/segreg都是可选的,可以简单的忽略。当指定了index, 而scale没有指定则默认为1.

segreg(段寄存器)取决于指令和程序是执行在实模式还是保护模式。在实模式它取决于指令,而在保护模式是多余的。在AT&T中,当scale/disp是立即数时,不应该带有'$'的前缀。

Example:

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是非常难以理解的。[base+index*scale+disp] 看一眼就能理解,而disp(base,index,scale)却不是这样。


就像你也许已经注意到的,AT&T语法的指令有一个后缀。这个后缀表示的是操作数的大小。'l'表示操作数是长整形,'w'表示操作数是字,'b'表示操作数是字节。Intel语法中也有相似功能的指令,比如:byte ptr, word ptr, dword ptr。

Example:

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

**NOTE: ALL EXAMPLES FROM HERE WILL BE IN AT&T SYNTAX**

从这里开始,所以的例子都是用AT&T语法的形式。

这一节我们大致的看如何在汇编语言中使用系统调用。系统调用在manual手册的第二册。同时也列在文件/usr/include/sys/syscall.h头文件中。一个更好的系统调用的列表在 。这些函数都是通过Linux的中断调用服务:int $0x80来实现的。


对于所以的系统调用,系统的调用号保存在%eax中。对于所以小于6个参数的系统的调用,参数安装顺序保存到%ebx,%ecx,%edx,%esi,%edi中。系统调用的返回值保存在%eax中。

系统调用号可以在头文件/usr/include/sys/syscall.h中找到。那些宏被定义成:SYS_的形式,也就是形如:SYS_exit,SYS_exit等。

Example:
(Hello world program - it had to be done)

根据man 2 write可以知道write函数的原型:

ssize_t write(int fd, const void *buf, size_t count);

因此参数fd保存到%ebx中,buf保存在%ecx中,count保存到%edx中,系统调用号保存到%eax中。然后利用$0x80号中断来执行write调用。返回值保存在%eax中。

  1. .file "write.s"
  2. .section .rodata
  3.           hello: .string "hello, world!\n"
  4. .section .text
  5. .global _start
  6. _start:
  7. movl $4,     %eax  # syscall number for write function
  8. movl $1,     %ebx  # 1 standand for stdout
  9. movl $hello, %ecx  # the second argument of write function 
  10. movl $14,    %edx  # the third argument of write function
  11. int  $0x80         # interrupt to call write
  12. movl $1,     %eax  # syscall number for sys_exit function
  13. xorl %ebx,   %ebx  # the argument for sys_exit function
  14. int  $0x80         # interrupt to call sys_exit
  15. ret                # return to the caller of the function

编译、运行、及结果:
digdeep@ubuntu:~/assembly$ as -o write.o write.s
digdeep@ubuntu:~/assembly$ ld -o write write.o
digdeep@ubuntu:~/assembly$ ./write
hello, world!
helloworld.s
  1. .section .data
  2.           msg: .string "Hello, world!\n"
  3.           len = . - msg
  4. .section .text
  5. .global _start
  6. .type _start, @function

  7. _start:
  8. movl $len, %edx
  9. movl $msg, %ecx
  10. movl $1, %ebx
  11. movl $4, %eax
  12. int $0x80
  13. movl $0, %ebx
  14. movl $1, %eax
  15. int $0x80
编译、运行、及结果:
digdeep@ubuntu:~/assembly$ as -o helloworld.o helloworld.s
digdeep@ubuntu:~/assembly$ ld -o helloworld helloworld.o
digdeep@ubuntu:~/assembly$ ./helloworld
Hello, world!

Syscalls with > 5 args. 系统调用的参数多余5个

当系统调用的参数多于5个时,我们仍然将系统调用号保存在%eax中,但是参数存储在内存中,同时将第一个参数的内存地址保存在%ebx中。

《1》如果你用栈保存参数,那么参数必须逆序进栈,也就是最后一个参数先进栈,第一个参数最后进栈。然后将栈指针复制到%ebx中
《2》另外也可以将参数复制到一块分配的内存中,然后将该第一个参数地址保存在%ebx中
Example: 
  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <sys/mman.h>
  4. #include <fcntl.h>
  5. #include <unistd.h>

  6. int main(void)
  7. {
  8.         char *file = "mmap.s";
  9.         char *mappedptr;
  10.         int fd, filelen;

  11.         fd = open(file, O_RDONLY);
  12.         filelen = lseek(fd, 0, SEEK_END);
  13.         mappedptr = mmap(NULL, filelen, PROT_READ, MAP_SHARED, fd, 0);
  14.         write(STDOUT_FILENO, mappedptr, filelen);
  15.         munmap(mappedptr, filelen);
  16.         close(fd);
  17.         return 0;
  18. }
Arrangement of mmap() args in memory:
%esp%esp+4%esp+8%esp+12%esp+16%esp+20
00000000filelen0000000100000001fd00000000

我们用命令:gcc -S mmap.c
得到mmap.c的汇编代码如下:
  1. .file "mmap.c"
  2.         .section .rodata
  3. .LC0:
  4.         .string "mmap.s"
  5.         .text
  6. .globl main
  7.         .type main, @function
  8. main:
  9.         pushl %ebp           # 先保护栈的基指针
  10.         movl %esp, %ebp      # 为了执行本函数,准备从栈上开辟出一块内存
  11.         andl $-16, %esp      # 执行内存访问的对齐
  12.         subl $48, %esp       # 在栈上分配出一块内存
  13.         movl $.LC0, 44(%esp# 为 char *file指针在栈上分配内存
  14.         movl $0, 4(%esp)     # 将open函数的参数O_RDONLY = 0分配内存
  15.         movl 44(%esp), %eax  # 利用寄存器%eax作为中转站将内存44(%esp)的值转到(%esp)
  16.         movl %eax, (%esp)    # 因为不能直接将一个内存中的数据mov到另一个内存中,必须由CPU来控制
  17.         call open            # 调用open函数

  18.         movl %eax, 40(%esp)  # 为int fd 分配内存,并将open的返回值复制给fd
  19.         movl $2, 8(%esp)     # 为lseek的参数SEEK_END=2分配内存
  20.         movl $0, 4(%esp)     lseek的参数 0 分配内存
  21.         movl 40(%esp), %eax  # 将lseek的参数fd通过寄存器%eax作为中转站转到(%esp)
  22.         movl %eax, (%esp
  23.         call lseek           # 调用lseek函数

  24.         movl %eax, 36(%esp)  # 为filelen分配内存,并将lseek的返回值赋值给filelen
  25.         movl 36(%esp), %eax  # 该句似乎可以优化掉?
  26.         movl $0, 20(%esp)    # 为mmap的第6个参数0分配内存
  27.         movl 40(%esp), %edx  # 将mmap的第5个参数fd通过寄存器%edx中转到16(%esp)
  28.         movl %edx, 16(%esp)  # 为mmap的第4个参数MAP_SHARED=1分配内存
  29.         movl $1, 12(%esp)    # 为mmap的第3个参数PROT_READ=1分配内存
  30.         movl $1, 8(%esp)
  31.         movl %eax, 4(%esp)   # 为mmap的第2个参数filelen转到4(%esp)
  32.         movl $0, (%esp)      # 为mmap的第1个参数NULL=0分配内存
  33.         call mmap            # 调用mmap函数

  34.         movl %eax, 32(%esp)  # 为mappedptr分配内存,并将mmap的返回值赋值给mappedptr
  35.         movl 36(%esp), %eax  # 将filelen通过寄存器%eax中转到8(%esp)来作为write的第3个参数
  36.         movl %eax, 8(%esp)
  37.         movl 32(%esp), %eax  # 将mappedptr通过寄存器%eax中转到4(%esp)来作为write的第2个参数
  38.         movl %eax, 4(%esp)  
  39.         movl $1, (%esp)      # 为write的第1个参数STDOUT_FILENO=1分配内存
  40.         call write           # 调用write函数

  41.         movl 36(%esp), %eax  # 将filelen通过寄存器%eax中转到4(%esp)来作为munmap的第2个参数
  42.         movl %eax, 4(%esp)
  43.         movl 32(%esp), %eax  # 将mappedptr通过寄存器%eax中转到(%esp)来作为munmap的第1个参数
  44.         movl %eax, (%esp)
  45.         call munmap          # 调用munmap函数

  46.         movl 40(%esp), %eax  # 将fd通过寄存器%eax中转到(%esp)来作为close的参数
  47.         movl %eax, (%esp)
  48.         call close           # 调用close函数

  49.         movl $0, %eax        # return 0 对应的汇编代码
  50.         leave                # leave指令会清理刚才使用了的堆栈,并恢复原来的堆栈基指针%ebp
  51.                              # leave指令相当于:movl %ebp,%esp 和 popl %ebp  这两条指令

  52.               ret                               # ret 指令所做的操作相当于POP EIP,因为在调用call的时候,会将call指令之后                                                                    # 的一条指令的地址执行PUSH EIP,所以当call执行完之后,
  53.                                                                    # ret指令会将调用call时压栈的地址POP给EIP

  54.         .size main, .-main
  55.         .ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
上面的注释中对mmap.c对应的汇编代码进行了十分详细的解释。
可以知道:
每一个函数的调用执行,都是先在栈上分配出一块足够大的内存来使用,在这块内存的高地址部分,为函数中的局部变量分配内存,对于函数内部的函数调用,会把在这块内存的低地址部分当作一个新的栈,根据内部函数参数的大小和多少将参数保存在栈上,第一个参数保存在新栈的栈顶,然后执行内部的函数调用

关于socket的系统调用,仅仅使用一个系统调用号:SYS_socketcall保存到%eax中。不同的socket函数是通过子函数号来区别的,我们要将子函数号保存到%ebx中,然后将指向系统调用的参数的指针保存到%ecx中,最后通过%0x80号中断来执行socket调用。

  1. .include "defines.h"
  2. .globl _start
  3. _start:

  4. pushl %ebp                          # 保存堆栈基指针%ebp
  5. movl  %esp, %ebp                    # 准备开辟出一个堆栈供本函数的执行来使用
  6. sub   $12, %esp                     # 开辟出一个12字节的堆栈供本函数的执行来使用
  7. # socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
  8. movl $AF_INET, (%esp)               # socket函数的第1个参数
  9. movl $SOCK_STREAM, 4(%esp)          # socket函数的第2个参数
  10. movl $IPPROTO_TCP, 8(%esp)          # socket函数的第3个参数
  11. movl $SYS_socketcall,%eax           # socket函数的调用号
  12. movl $SYS_socketcall_socket,%ebx    # socket函数的子调用号
  13. movl %esp,%ecx                      # 将socket函数的的参数的指针放到%ecx中
  14. int $0x80                           # 利用中断调用socket函数
  15. movl $SYS_exit,%eax                 # exit函数的调用号
  16. xorl %ebx,%ebx                      # exit函数的参数0
  17. int $0x80                           # 利用中断调用exit函数
  18. movl %ebp,%esp                      # 清理刚才使用了的堆栈
  19. popl %ebp                           # 恢复原来的堆栈基指针%ebp
  20. ret                                 # return 相当于POP EIP

上面代码中的头问件defines.h请参见Using Assembly Language in Linux--(2)
编译、运行及结果:
  1. digdeep@ubuntu:~/assembly$ as -o socket.o socket.s
  2. digdeep@ubuntu:~/assembly$ ld -o socket socket.o
  3. digdeep@ubuntu:~/assembly$ ./socket
  4. digdeep@ubuntu:~/assembly$ echo $?
  5. 0
  6. digdeep@ubuntu:~/assembly$
阅读(2355) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~