最小的程序
最近在读《程序员的自我修养——链接、装载与库》收获匪浅。在4.6节中介绍链接过程时,书中有一个最“小”的程序的例子故想尝试。但发现书中例子是32位系统的例子,对于64位系统的情况有些许不同,故尝试改写为64位版。
32位版本
原书代码:
1 |
|
代码注解
代码使用了内嵌汇编代码。nomain
我们在链接阶段可指定为程序的入口。其中调用了两个函数:print
和exit
。
在print
中实现了系统调用。通过调用0x80中断,eax
为调用号,ebx
,ecx
,edx
等通用寄存器来传递参数。在些函数中调用号(eax
的值)为4,对应为write
,原型为:
int write(int filedesc, char* buffer, int size)
在代码中ebx
,ecx
,edx
分别对应了这三项参数。
movl $13,%%edx
中将edx
设置为13,对应文本size为13;
movl %0,%%ecx
的占用符与后面"r"(str)
结合,将文本地址写入ecx
;
movl $1,%%ebx
将文件描述符写入ebx
。此处原书有误使用了$0
,在Linux中文件描述符0对应为标准输入(stdin),实际应该1对应为标准输出(stdout)。
movl $4,%%eax
将调用号4写入eax
,对应调用的是write
函数。
int $0x80
调用中断,系统会去查找中断向量表,0x80
对应的为中断服务程序(system_call),该程序查询系统调用表找到调用号(eax
的值为4的函数对应为write
对其进行调用,实现向标准输出写入字符串。
过程可见下图(例子并非write
而是fork
但原理类似),图片为书中第12章内容。
在exit
函数中则比print
简单一些,将系统调用的EXIT
的调用号1存入eax
,将EXIT
的参数,退出码,42存入ebx
之后调用中断实现对EXIT
系统调用的调用。
另外书中也介绍到,使用echo $?
命令可查看上一条bash命令执行的程序的退出码。另外自己调用EXIT
系统调用而不是return 0
是因为,普通程序的main()
结束后会将控制权返回给系统库,由系统库负责调用EXIT
退出进程。而这里的nomain()
结束后系统控制权不会返回,可能执行到nomain()
后面不正常的指令,最终导致进程异常退出。
编译、链接
使用命令
gcc -c -fno-builtin TinyHelloWorld.c
ld -static -e nomain -o TinyHelloWorld TinyHelloWorld.o
进行编译链接。
-fno-builtin
的作用是禁用GCC编译器的内置函数。GCC会将一些常用的C库函数替换成编译器的内置函数,以达优化目的。如GCC会次只有字符串参数的printf
替换成puts
,节省格式解析的时间。exit()
也是内置函数之一,故在些要将内置函数关闭。
-e nomain
表示该程序的入口函数为nomain
。
64位版本
代码如下:
1 |
|
64位系统与32位系统有些许不同。
首先是系统调用号,write
与exit
在64位Linux下对应的为1与60,而非32位系统下的4与1。另外在调用系统调用时会使用syscall
指令。
其他发现
使用objdump -s
可看到,段.text
为程序的指令;.rodata
为字符串"Hello world!";.data
保存的是str的全局变量的地址,也就是0x402000;.eh_frame
是编译器生成的与C++异常处理相关的内容;.comment
就是编译器版本信息。
使用objdump -S
可看到汇编代码与我们写的一样。