最小的程序

最近在读《程序员的自我修养——链接、装载与库》收获匪浅。在4.6节中介绍链接过程时,书中有一个最“小”的程序的例子故想尝试。但发现书中例子是32位系统的例子,对于64位系统的情况有些许不同,故尝试改写为64位版。

32位版本

原书代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
char *str = "Hello world!\n";

void print()
{
asm( "movl $13,%%edx \n\t"
"movl %0,%%ecx \n\t"
"movl $1,%%ebx \n\t" // 原书为$0,对应stdin;实际应为$1,对应标准输出stdout。
"movl $4,%%eax \n\t"
"int $0x80 \n\t"
::"r"(str):"edx","ecx",'"ebx");
}

void exit()
{
asm( "movl $42,%ebx \n\t"
"movl $1, %eax \n\t"
"int $0x80 \n\t" );
}

void nomain()
{
print();
exit();
}

代码注解

代码使用了内嵌汇编代码。nomain我们在链接阶段可指定为程序的入口。其中调用了两个函数:printexit

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
char* str = "Hello world!\n";

void print()
{
asm("movq $13, %%rdx \n\t"
"movq %0, %%rsi \n\t"
"movq $1, %%rdi \n\t"
"movq $1, %%rax \n\t" // 系统调用号 (sys_write)
"syscall \n\t" // 使用syscall指令
::"r"(str):"rdx","rsi","rdi");
}

void exit()
{
asm("movq $42, %rdi \n\t"
"movq $60, %rax \n\t" // 系统调用号 (sys_exit)
"syscall \n\t");
}

void nomain()
{
print();
exit();
}

64位系统与32位系统有些许不同。

首先是系统调用号,writeexit在64位Linux下对应的为1与60,而非32位系统下的4与1。另外在调用系统调用时会使用syscall指令。

其他发现

使用objdump -s可看到,段.text为程序的指令;.rodata为字符串"Hello world!";.data保存的是str的全局变量的地址,也就是0x402000;.eh_frame是编译器生成的与C++异常处理相关的内容;.comment就是编译器版本信息。

反汇编段信息

使用objdump -S可看到汇编代码与我们写的一样。

反汇编

最小的程序
https://noeliufz.github.io/2024/08/19/zui-xiao-de-cheng-xu/
作者
Fangzhou Liu
發布於
2024年8月19日
許可協議