最小的程序
最近在读《程序员的自我修养——链接、装载与库》收获匪浅。在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可看到汇编代码与我们写的一样。
