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中。
- .file "write.s"
-
.section .rodata
- hello: .string "hello, world!\n"
-
-
.section .text
-
.global _start
-
-
_start:
-
movl $4, %eax # syscall number for write function
-
movl $1, %ebx # 1 standand for stdout
-
movl $hello, %ecx # the second argument of write function
-
movl $14, %edx # the third argument of write function
-
int $0x80 # interrupt to call write
-
-
movl $1, %eax # syscall number for sys_exit function
-
xorl %ebx, %ebx # the argument for sys_exit function
-
int $0x80 # interrupt to call sys_exit
-
-
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
- .section .data
- msg: .string "Hello, world!\n"
- len = . - msg
-
.section .text
-
.global _start
-
.type _start, @function
-
_start:
-
movl $len, %edx
-
movl $msg, %ecx
-
movl $1, %ebx
-
movl $4, %eax
-
int $0x80
-
-
movl $0, %ebx
-
movl $1, %eax
-
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:
- #include <sys/types.h>
-
#include <sys/stat.h>
-
#include <sys/mman.h>
-
#include <fcntl.h>
-
#include <unistd.h>
-
-
int main(void)
-
{
-
char *file = "mmap.s";
-
char *mappedptr;
-
int fd, filelen;
-
-
fd = open(file, O_RDONLY);
-
filelen = lseek(fd, 0, SEEK_END);
-
mappedptr = mmap(NULL, filelen, PROT_READ, MAP_SHARED, fd, 0);
-
write(STDOUT_FILENO, mappedptr, filelen);
-
munmap(mappedptr, filelen);
-
close(fd);
-
return 0;
-
}
Arrangement of mmap() args in memory:%esp | %esp+4 | %esp+8 | %esp+12 | %esp+16 | %esp+20 |
00000000 | filelen | 00000001 | 00000001 | fd | 00000000
|
我们用命令:gcc -S mmap.c
得到mmap.c的汇编代码如下:
- .file "mmap.c"
-
.section .rodata
-
.LC0:
-
.string "mmap.s"
-
.text
-
.globl main
-
.type main, @function
-
main:
-
pushl %ebp # 先保护栈的基指针
-
movl %esp, %ebp # 为了执行本函数,准备从栈上开辟出一块内存
-
andl $-16, %esp # 执行内存访问的对齐
-
subl $48, %esp # 在栈上分配出一块内存
-
movl $.LC0, 44(%esp) # 为 char *file指针在栈上分配内存
-
movl $0, 4(%esp) # 将open函数的参数O_RDONLY = 0分配内存
-
movl 44(%esp), %eax # 利用寄存器%eax作为中转站将内存44(%esp)的值转到(%esp)中
-
movl %eax, (%esp) # 因为不能直接将一个内存中的数据mov到另一个内存中,必须由CPU来控制
-
call open # 调用open函数
-
movl %eax, 40(%esp) # 为int fd 分配内存,并将open的返回值复制给fd
-
movl $2, 8(%esp) # 为lseek的参数SEEK_END=2分配内存
-
movl $0, 4(%esp) # 为lseek的参数 0 分配内存
-
movl 40(%esp), %eax # 将lseek的参数fd通过寄存器%eax作为中转站转到(%esp)中
-
movl %eax, (%esp)
-
call lseek # 调用lseek函数
-
movl %eax, 36(%esp) # 为filelen分配内存,并将lseek的返回值赋值给filelen
-
movl 36(%esp), %eax # 该句似乎可以优化掉?
-
movl $0, 20(%esp) # 为mmap的第6个参数0分配内存
-
movl 40(%esp), %edx # 将mmap的第5个参数fd通过寄存器%edx中转到16(%esp)中
-
movl %edx, 16(%esp) # 为mmap的第4个参数MAP_SHARED=1分配内存
-
movl $1, 12(%esp) # 为mmap的第3个参数PROT_READ=1分配内存
-
movl $1, 8(%esp)
-
movl %eax, 4(%esp) # 为mmap的第2个参数filelen转到4(%esp)中
-
movl $0, (%esp) # 为mmap的第1个参数NULL=0分配内存
-
call mmap # 调用mmap函数
-
movl %eax, 32(%esp) # 为mappedptr分配内存,并将mmap的返回值赋值给mappedptr
-
movl 36(%esp), %eax # 将filelen通过寄存器%eax中转到8(%esp)来作为write的第3个参数
-
movl %eax, 8(%esp)
-
movl 32(%esp), %eax # 将mappedptr通过寄存器%eax中转到4(%esp)来作为write的第2个参数
-
movl %eax, 4(%esp)
-
movl $1, (%esp) # 为write的第1个参数STDOUT_FILENO=1分配内存
-
call write # 调用write函数
-
movl 36(%esp), %eax # 将filelen通过寄存器%eax中转到4(%esp)来作为munmap的第2个参数
-
movl %eax, 4(%esp)
-
movl 32(%esp), %eax # 将mappedptr通过寄存器%eax中转到(%esp)来作为munmap的第1个参数
-
movl %eax, (%esp)
-
call munmap # 调用munmap函数
-
movl 40(%esp), %eax # 将fd通过寄存器%eax中转到(%esp)来作为close的参数
-
movl %eax, (%esp)
-
call close # 调用close函数
-
movl $0, %eax # return 0 对应的汇编代码
-
leave # leave指令会清理刚才使用了的堆栈,并恢复原来的堆栈基指针%ebp
- # leave指令相当于:movl %ebp,%esp 和 popl %ebp 这两条指令
- ret # ret 指令所做的操作相当于POP EIP,因为在调用call的时候,会将call指令之后 # 的一条指令的地址执行PUSH EIP,所以当call执行完之后,
- # ret指令会将调用call时压栈的地址POP给EIP
-
.size main, .-main
-
.ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
上面的注释中对mmap.c对应的汇编代码进行了十分详细的解释。
可以知道:
每一个函数的调用执行,都是先在栈上分配出一块足够大的内存来使用,在这块内存的高地址部分,为函数中的局部变量分配内存,对于函数内部的函数调用,会把在这块内存的低地址部分当作一个新的栈,根据内部函数参数的大小和多少将参数保存在栈上,第一个参数保存在新栈的栈顶,然后执行内部的函数调用。
关于socket的系统调用,仅仅使用一个系统调用号:SYS_socketcall保存到%eax中。不同的socket函数是通过子函数号来区别的,我们要将子函数号保存到%ebx中,然后将指向系统调用的参数的指针保存到%ecx中,最后通过%0x80号中断来执行socket调用。
- .include "defines.h"
- .globl _start
-
_start:
-
pushl %ebp # 保存堆栈基指针%ebp
-
movl %esp, %ebp # 准备开辟出一个堆栈供本函数的执行来使用
-
sub $12, %esp # 开辟出一个12字节的堆栈供本函数的执行来使用
-
-
# socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
-
movl $AF_INET, (%esp) # socket函数的第1个参数
-
movl $SOCK_STREAM, 4(%esp) # socket函数的第2个参数
-
movl $IPPROTO_TCP, 8(%esp) # socket函数的第3个参数
-
-
movl $SYS_socketcall,%eax # socket函数的调用号
-
movl $SYS_socketcall_socket,%ebx # socket函数的子调用号
-
movl %esp,%ecx # 将socket函数的的参数的指针放到%ecx中
-
int $0x80 # 利用中断调用socket函数
-
-
movl $SYS_exit,%eax # exit函数的调用号
-
xorl %ebx,%ebx # exit函数的参数0
-
int $0x80 # 利用中断调用exit函数
-
-
movl %ebp,%esp # 清理刚才使用了的堆栈
-
popl %ebp # 恢复原来的堆栈基指针%ebp
-
ret # return 相当于POP EIP
上面代码中的头问件defines.h请参见Using Assembly Language in Linux--(2) 编译、运行及结果:
- digdeep@ubuntu:~/assembly$ as -o socket.o socket.s
-
digdeep@ubuntu:~/assembly$ ld -o socket socket.o
-
digdeep@ubuntu:~/assembly$ ./socket
-
digdeep@ubuntu:~/assembly$ echo $?
-
0
-
digdeep@ubuntu:~/assembly$
阅读(2355) | 评论(0) | 转发(1) |