Chap-4 Section 4.6 链接控制过程

4.6 链接过程控制
前面我们在使用ld链接器的时候,没有指定链接控制脚本,其实ld在用户没有指定链接脚本的
时候会使用默认链接脚本,可以使用ld -verbose来查看ld默认的链接脚本。

当然,为了更加精确的控制链接过程,可以自己写一个链接控制脚本,然后指定该脚本为链接
控制脚本,可以使用-T参数:ld -T link.script

4.6.2 最小的程序
为了演示链接的控制过程,我们要写一个程序,该程序的功能是在终端输出"hello world"。但是
我们这里的程序与C语言教科书中的例子有所不同。
首先,经典的hello world使用了printf函数,该函数是C语言库的一部分,要使用该函数,必须将
C语言库与程序的目标文件链接产生最终的可执行文件,而这里的程序没用C语言运行库,使得它成
为一个独立于任何库的纯的程序。
其次,经典的hello world使用了库,所以必须有main函数,我们知道一般程序的入口点是_start,
由库负责初始化后调用main函数来执行程序的主题部分,我们这里使用另外一个nomain函数做为
程序的入口。
最后,经典的hello world会产生多个段,main程序的指令部分会产生.text段、字符串常量
"hello world! "会被放在数据段或者只读数据段,还有C库所包含的各种段。为了演示ld链接脚本
的控制过程,我们将这个程序的所有段都合并到一个叫"tinytext"的段。注意:这个段是我们任意
命名的。
该程序的源代码如下:
//TinyHelloWorld.c
char* str = "Hello World! ";

void print() {
asm("movl $13, %%edx "
"movl %0, %%ecx "
"movl $0, %%ebx "
"movl $4, %%eax "
"int $0x80 "
::"r"(str):"edx", "ecx", "ebx");
}

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

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

这里的printf函数使用了Linux的Write系统调用,exit函数使用了Exit系统调用。系统调用使用
0x80中断来实现,其中eax为调用号,ebx,ecx,edx等通过寄存器来传递参数。比如Write系统调用
往一个文件句柄中写入数据,原型为:
int write(int filedesc, char* buffer, int size);
Write系统调用的调用号为4,因此eax寄存器的值为4
filedesc表示被写入的文件句柄,使用ebx寄存器传递,我们这里是要往默认终端(stdout)输出,它
的文件句柄为0,即ebx=0
buffer表示要写入的缓冲区首址,使用ecx寄存器传递。
size表示要写入的字节数,使用edx传递,字符串"hello world! "长度为13字节,因此edx=13

同理,Exit系统调用中,ebx表示进程的退出码。比如我们平时的main程序中的return的数值会
返回给系统库,由系统库将该数值传递给Exit系统调用,这样父进程就可以接收到子进程的退出码,
Exit系统调用的调用号为1,即eax=1。

我们先用普通命令行的方式来编译和链接TinyHelloWorld.c,如图4.2.18所示:


***图4.2.18***
其中-fno-builtin GCC编译器提供了很多内置函数(Built-in Function),它会把一些常用的C库函数
替换成编译器的内置函数,以到达优化的功能。比如GCC会将只有字符串参数的printf函数替换成
puts,以节省格式解析时间,exit()函数也是GCC的内置参数之一,所以我们要使用-fno-builtin
参数来关闭GCC内置函数的功能。
-static 这个参数表示ld将以静态链接的方式来链接程序,而不是使用默认的动态链接方式。
-e nomain 表示该程序的入口函数为nomain。
我们用readelf -h test打印出了可执行文件的文件头,从图4.2.19可以看出,入口点地址为0x80480c4,
用readeld -s test打印出了可执行文件的符号表,从中可以看出nomain符号的地址为0x80480c4。


***图4.2.19***

用readelf -S test打印出了可执行文件的段表,如图4.2.20所示:


***图4.2.20***
其中可读数据段.rodata大小为0x00000e,正好为"Hello World! "13个字节的长度。而数据段.data的
大小为0x000004,正好是char* str指针的大小。

4.6.3 使用链接控制脚本
如果把链接过程比作一台计算机,那么ld链接器就是计算机的CPU,所有的目标文件、库文件就是输入,
链接结果输出的可执行文件就是输出,而链接控制脚本就是这台计算机的程序,它控制CPU的执行,以
“程序”要求的方式将输入加工成所需要的输出结果。链接控制脚本“程序”使用一种特殊的语言写成,即
ld的链接脚本语言。
简单来讲,控制链接过程无非是控制输入段如何变成输出段,比如哪些输入段要合并一个输出段,哪些
输入段要丢弃;指定输出段的名字、装载地址、属性的等等。下面是一个链接控制脚本:
ENTRY(nomain)

SECTIONS
{
. = 0x08048000 + SIZEOF_HEADERS;
tinytext : {*(.text) *(.data) *(.rodata)}
/DISCARD/ : {*(.comment)}
}
第一行的ENTRY(nomain)指定了程序的入口为nomain()函数,后面的SECTIONS命令一般是链接脚本的主体,
这个命令指定了各种输入段到输出段的变换,SECTIONS后面紧跟着一对大括号里面包含了SECTIONS变换规则,
其中三条语句,每条语句一行。其中第一条是赋值语句,后两条是段转换规则。
. = 0x08048000 + SIZEOF_HEADERS 是将当前虚拟地址设置成0x08048000 + SIZEOF_HEADERS, SIZEOF_HEADERS
为输出文件的文件头大小,“.”表示当前虚拟地址,因为这条语句后面紧跟着输出段"tinytext",所以"tinytext"
段的起始虚拟地址为0x08048000 + SIZEOF_HEADERS。它将当前虚拟地址设置成一个比较巧妙的值,以便于装载
时页映射方便。
tinytext : {*(.text) *(.data) *(.rodata)} 将输入文件中名字为.text .data .rodata的段以此合并到输出
文件的tinytext。
/DISCARD/:{*(.comment)} 将所有输出文件中名字为.comment的段丢弃,不保存到输出文件中。

通过在链接的时候指定上述脚本为链接控制脚本,我们可以实现将TinyHelloWorld.c输出的目标文件中的.text
.data .rodata合并为一个段:.tinytext,如图4.2.21所示:


***图4.2.21***

原文地址:https://www.cnblogs.com/miaoyong/p/3508089.html