读书笔记--《程序员的自我修养》第4章:静态链接(1)

本章以 如何将a.c文件与b.c文件链接成一个可执行文件 来探讨如何进行静态链接

其中a.c和b.c文件如下:
a.c文件

extern int shared;
int main()
{
    int a = 100;
    swap(&a,&shared);
    }

b.c文件

int shared = 1;
void swap(int* a, int* b)
{
    *a ^= *b ^= *a ^= *b;
}

首先将他们编译成目标文件“a.o”和“b.o”
从代码中可以看到,b.c中定义了两个全局符号:变量shared和函数swap();a.c中定义了一个全局符号main;a.c引入了b.c中的shared和swap。接下来把a.o和b.o两个目标文件链接成一个可执行文件ab.

4.1 空间地址分配

对于多个输入目标文件,链接器如何将它们的各个段合并到一个输出文件?

方法一:按序叠加
缺点:由于地址和空间对齐的关系,会造成内存空间大量的碎片

方法二:相似段合并(现在链接器基本都采用这个方法)
”链接器为目标文件分配地址和空间“:这里既指在输出的可执行文件中的空间、也指装载后的虚拟地址的空间。
但对.bss段来说只限于后者。

一般分两步:
(1)空间与地址分配
扫描所有输入文件,并获得它们各个段的长度、属性和位置;将它们符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表中。
(2)符号解析与重定位
利用上一步获得的信息,读取输入文件中段的数据、重定位信息,并进行符号解析和重定位。

使用ld链接器,将目标文件链接起来:
$ ld a.o b.o -e main -o ab

  1. -e main 表示将main函数作为程序的入口;ld链接器默认程序入口是_start。
  2. -o ab 表示链接输出文件名是ab。

    objdump -h a.o 参数-h表示显示处a.o的段头信息
    这里写图片描述
    这里写图片描述
    可以看出,链接后.text的长度=两个目标文件中.text的长度之和;
    链接前所有的VMA都是0,因为他们还未被分配虚拟地址空间,默认为0.

    问题:为什么链接器要将可执行文件ab的.text分配到0x4000e8,将.data分配到0x6001b8,而不是从虚拟空间的0地址开始分配呢?答案:不知道!

4.1.3 符号地址的确定

链接器首先根据前面的空间分配方法,对各个段的虚拟地址进行确定。如.text段的起始位置是:0x4000e8,.data段的起始位置是:0x6001b8。然后根据各个符号在段内的偏移,确定其真正的虚拟地址。只需对段的基址加上偏移量即可。
用gdb打印出符号的位置:
这里写图片描述
说明main函数在.text段的偏移为0,shared在.data的偏移也为0.

4.2 符号解析与重定位

用objdump -d a.o 查看a.o文件的反汇编结果:
这里写图片描述

(1)a.o文件中对shared的引用为偏移为0x13的位置:be 00 00 00 00
它的作用是将shared的地址赋值到esi寄存器中,该指令一共5个字节,第1个字节是指令码,后4个字节是shared的地址。
由于源码在编译时,编译器不知道shared的地址,因此把0作为它们的地址。

(2)a.o文件中对swap的引用为偏移为0x20的位置:e8 00 00 00 00
该指令一共5个字节,前面的0xe8是操作码。根据Intel的IA-32体系软件开发者手册,这是一条近址相对位移调用指令。
后面4个字节是被调用的函数相对调用指令的下一条指令的偏移量。

用objdump -d ab命令查看ab的反汇编代码:
这里写图片描述
发现确实,shared的地址变为0x6001b8,而swap函数的地址为0x40010d+0x7 = 0x400114

4.2.2 重定位表

重定位表用来保存与重定位相关的信息。每个要被重定位的ELF段都有一个重定位表。一个重定位表往往是ELF文件中的一个段。如代码段有要被重定位的地方,那么就有’.ref.text’的段保存了代码段的重定位表。

objdump -r a.o 查看a.o文件的重定位表
这里写图片描述

每个要被重定位的地方叫一个重定位入口。重定位入口的偏移表示该入口在要被重定位段中的位置。根据前面的反汇编结果,这里的0x14和0x21分别是代码段中shared和swap的地址所在的位置。

对于32位的Intel x86系列处理器来说,重定位表是一个Elf32_Rel结构的数组,每个数组元素对应一个重定位入口。
typedef struct {
Elf32_Addr r_offset; //重定位入口的偏移。
Elf32_Word r_info; //重定位入口的类型和符号。
}Elf32_Rel;

r_offset:对可重定位文件来说,是该重定位入口所要修正的位置的第一个字节相对于段起始地偏移。
r_info:低8位表示重定位入口的类型;高24位表示重定位入口的符号在符号表中的下标。

4.2.3 符号解析
输入命令:$ ld a.o
这里写图片描述
发现找不到shared和swap的定义。这是因为我们没有链接b.o文件,当然找不到喽

输入命令:$readelf -s a.o
这里写图片描述
“GLOBAL”类型的符号,出来main函数定义了之外,其他shared和swap都是“UND”,未定义类型。这种未定义的符号是因为该目标文件中有关于它们的重定向项。所以链接器扫描完所有的输入目标文件后,所有这些未定义的符号都应该能在全局符号表中找到,否则连接器就报符号未定义错误。

4.2.4 指令修正方式

不同处理器指令对地址的格式和方式都不一样。寻址方式有多种,如:
近址寻址或远址寻址;绝对寻址或相对寻址;寻址长度有8、16、32、64位等区别。

但对于32位x86平台的ELF文件的重定位入口所修正的指令寻址方式,只有两种;
绝对近址32位寻址和相对近址32位寻址。

重定位入口的r_info成员低8位表示重定位入口类型,如表所示:
这里写图片描述
其中,A=保存在被修正位置的值;P=被修正的位置(相对于段开始的偏移量或虚拟地址);S=符号的实际地址(r_info的高24位)

对照前面的a.o的重定位信息,第一个重定位入口是对swap符号的引用,它是一条相对位移调用指令;而shared是R_386_32类型的,它修正的是一条传输指令的源,该传输指令的源是一个立即数,即shared的绝对地址。

假设将a.o和b.o链接成可执行文件后,main函数的虚拟地址是0x1000,swap是0x2000;shared的是0x3000。那么如何修正重定位入口呢?

(1)对于shared变量。是绝对地址修正。结果应该是S+A.
S是符号shared的实际地址,即0x3000,A是被修正位置的值,是0x0。因此修正后的地址是
0x3000+0x0 = 0x3000

(2)对于swap函数。是相对地址修正。结果应该是S+A-P.
S是符号swap的实际地址,即0x2000,A是被修正位置的值,是0x0。P为被修正的位置,当链接成可执行文件时,这个值应该是被修正位置的虚拟地址,即0x1000+0x21
因此这个重定位入口修正后地址为:0x2000+0-(0x1000+0x21)=0xfdf
**

这一步没法验证,因为不知道怎么打印出重定位过程信息。。。具体看书

**

4.3 COMMON块

由于弱符号机制允许同一个符号的定义存在多个文件中。但是如果一个弱符号定义在多个目标文件中,它们的类型又不同。而连接器本身并不支持符号的类型,那么链接器该如何处理呢?

考虑到三种情况:
(1)两个或两个以上强符号不一致;
(2)有一个强符号,其他都是弱符号,出现类型不一致;
(3)两个或两个以上弱符号类型不一致;

对于第(1)中情况,无须额外处理,链接器会报符号多重定义错误;链接器处理的是后两种情况。

事实上,现在的编译器和链接器支持一种叫做COMMON块的机制。当不同的目标文件需要的COMMON块空间大小不一致时,以最大的那块为准。

COMMON类型的链接规则是针对符号都是弱符号的情况,如果其中有一个符号是强符号,则输出结果中符号所占空间与强符号相同。如果链接过程又弱符号大小大于强符号,那么ld链接器会报错。

问题:为什么编译器不直接把未初始化的全局变量也当作未初始化的局部静态变量一样处理,在.bss段为其分配空间,而是将其标记为一个COMMON类型的变量?
回答:当编译器将一个编译单元编译成目标文件时,如果包含弱符号,则其所占空间是未知的,因为有可能在链接到其他文件中也定义了该弱符号,而弱符号以占用空间大的为准,因此无法确定在.bss段为其分配多少空间。而链接时,则大小就能确定了,所以它可以在最终输出文件的.bss段为其分配空间。所以总体来看,未初始化全局变量还是放在.bss段的。

GCC的“-fno-common”允许我们把未初始化的全局变量不以COMMON块的形式处理,或者使用“attribute”扩展:

int global __attribute__((nocommon));

一旦一个未初始化的全局变量不以COMMON块的形式存在,它就相当于一个强符号。如果其他目标文件中还有一个变量的强符号定义,链接时就会发生符号重复定义错误。

猜你喜欢

转载自blog.csdn.net/qq_15727809/article/details/82695673
今日推荐