文档的目的是为了能够入门x86_64 GNU Assembly 和 能够看懂 Linux Kernel 的启动代码。涉及的汇编知识未必是深入和最优的,如发现错误或者对实际的汇编程序编写的影响,请评论指出。
平台:Intel x86_64
编译器: gas
汇编程序是由定义好的段组成的,每个段的意义都不一样。最常用的由以下几个段:
对于最简单一个程序而言,text段是必须的,其他都是可选的。
那么text 段又是由什么组成的呢?对,它是由各种指令组成,而指令又是由操作码,寄存器,立即数和内存地址组成。
但是操作码都是一堆16进制字符,不太人性化,所以就就产生了助记符来方便程序员来编写汇编代码。
例如:
55 对应 push
0f 05 对应 syscall
在让我们看看 x86_64 平台提供了哪些寄存器给我们使用,
更多的寄存器请参见Intel Technical RM. 这里就不再详述了。
x86_64 平台规定立即数的最大值不能超过32位。
接下来我们来看看我们学习任何一种编程语言会写的第一个程序 - helloworld。
.section .data
message:
.ascii "hello world!\n"
length = . - message
.section .text
.global _start # must be declared for linker
_start:
movq $1, %rax # 'write' syscall number
movq $1, %rdi # file descriptor, stdout
lea message(%rip), %rsi # relative addressing string message
movq $length, %rdx
syscall
movq $60, %rax # 'exit' syscall number
xor %rdi, %rdi # set rdi to zero
syscall
➜ learning-asm git:(master) ✗ as hello.s -o hello.o
➜ learning-asm git:(master) ✗ ld hello.o -o hello
➜ learning-asm git:(master) ✗ ./hello
hello world!
其中 .section .data
和 .section .text
定义个数据段 和代码段。
message 只是一个label 方便我们来引用 hello world 字符串。
length = . - message
用来计算字符串的长度。.
用来表示当前的地址
_start
是程序的入口
然后程序调用 write syscall 来讲hello world 字符串输出。首先我们要知道syscall 的号码才能调用对用的方法。其中32位的syscall 号码与64位是不同的,你可以从/usr/include/asm/unistd_64.h
Linux的这个文件中查看,如果你要编写32位的汇编程序请查看/usr/include/asm/unistd_32.h
。
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5
#define __NR_lstat 6
#define __NR_poll 7
#define __NR_lseek 8
#define __NR_mmap 9
#define __NR_mprotect 10
#define __NR_munmap 11
#define __NR_brk 12
#define __NR_rt_sigaction 13
#define __NR_rt_sigprocmask 14
#define __NR_rt_sigreturn 15
#define __NR_ioctl 16
#define __NR_pread64 17
#define __NR_pwrite64 18
#define __NR_readv 19
......
这里我们是通过write 直接将字符串写到 stdout 描述符来进行输出。那么我们来看看sys_write 的原型。
long sys_write(unsigned int fd, const char __user *buf, size_t count);
从中我们可以看到需要传入3个参数,第一个fd,这里是stdout,所以就是1;第二个参数就是“hello world\n” 字符串的地址;第三个就是这个字符串的长度。
但是System V ABI 规定了对64位程序的接口,同时也规定了函数参数的传递规则。
所以文件描述符1需要加载到rdi寄存器,“hello world\n” 字符串的地址需要加载到 %rsi,字符串的长度加载到 rdx。
lea message(%rip), %rsi # relative addressing string message
这里为什么没有用mov 而是使用lea,这是为了变成地址无关的代码。如果你编写的不是地址无关的代码,那么可以使用mov 来取代。
这样这个helloworld 程序我们就基本讲解完了。如果下次再调用什么别的syscall,我相信应该也是没有问题的。