绪论
大家好,欢迎来到【程序员的自我修养】专栏。正如其专栏名,本专栏主要分享学习《程序员的自我修养——链接、装载与库》的知识点以及结合自己的工作经验以及思考。编译原理相关知识本身就比较有难度,我会尽自己最大的努力,争取深入浅出。若你希望与一群志同道合的朋友一起学习,也希望加入到我们的学习群中。文末有加入方式。
简介
在前两章的内容中,我们已经了解目标文件的基本布局以及部分局部细节,单独的可执行文件并没有什么实际作用。我们需要多个目标文件链接成可执行文件,或生成动态库。接下来的几章,我会详细介绍目标文件链接成可执行文件的过程,希望大家能够坚持,坚持必有所获。
)
示例代码
本文的示例代码如下:
//a.c
extern int shared;
extern void swap(int* a, int* b);
int main()
{
int a = 100;
swap(&a,&shared);
return 0;
}
//b.c
int shared = 1;
void swap(int* a, int* b)
{
*a ^= *b ^= *a = ^= b;
}
编译:
gcc -c a.c b.c
得到a.o
和b.o
两个目标文件。
如何合并目标文件
我们知道ELF文件格式的目标文件中,有.text
、.data
、.bss
等多个段。如果是你,会如何将多个目标文件按照什么规则合并成一个可执行文件呢?
略加思考,我们应该也会想到两个方式:
一、按序叠加。这种没有什么特别的规则,来一个目标文件,就将其依次叠加起来。如下图。
分析:由图可知,这种方式的确很简单,大大减轻了链接器的工作和复杂度。但是存在两个致命的问题。
- 浪费内存空间。段的装载地址和空间是由对齐要求的。比如x86的硬件来说,段的装载地址和空间的对齐单位是页,即4096字节。稍微规模大些的应用程序可能由几百,上千的目标文件组成,那么最终生成的可执行程序的段就会非常多。而每一个段都有内存对齐要求,则会造成很多的内存浪费。
- 访问效率不高。我们前面说过,计算机喜欢利用局部性原理,增加cache命中率。提高访问效率。若是这种组合方式,很明显,并不能有效利用该特性。
综上所述,该方式虽然简单,但不是一个好的方式。
二、相似段合并。将相同性质的段合并到一起,比如将所有输入文件的.text
段合并输出到文件的.text
段。如下图所示:
现在的链接器基本都是采用上述方式,因为它避免了方式一的缺陷。
链接步骤
采用方式二合并的链接器,整个链接过程可以分为两个步骤。空间与地址分配、符号解析与重定位。
空间与地址分配
扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。并将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系 。
这里的**“地址和空间”**有两层含义:
- 输出可执行文件中的空间分布。即各目标文件中的段在可执行文件中分布,如上如。
- 可执行文件在装载后的虚拟地址中的虚拟地址空间。即确定程序加载后,各个段在虚拟空间中的地址。也就是说,像
.text
、.data
等在内存中实际存在的段,其虚拟地址,在链接成可执行程序时,就已经确定了。这也是可以通过pc指针,定位到代码行数的原因。
yihua@ubuntu:~/test/static-linker$ readelf -S a.o
There are 12 section headers, starting at offset 0x2c8:
Section Headers:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040 0000000000000029 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000220 0000000000000030 0000000000000018 I 9 1 8
[ 3] .data PROGBITS 0000000000000000 00000069 0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 00000069 0000000000000000 0000000000000000 WA 0 0 1
[ 5] .comment PROGBITS 0000000000000000 00000069 000000000000002a 0000000000000001 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 0000000000000000 00000093 0000000000000000 0000000000000000 0 0 1
[ 7] .eh_frame PROGBITS 0000000000000000 00000098 0000000000000038 0000000000000000 A 0 0 8
[ 8] .rela.eh_frame RELA 0000000000000000 00000250 0000000000000018 0000000000000018 I 9 7 8
[ 9] .symtab SYMTAB 0000000000000000 000000d0 0000000000000120 0000000000000018 10 8 8
[10] .strtab STRTAB 0000000000000000 000001f0 000000000000002c 0000000000000000 0 0 1
[11] .shstrtab STRTAB 0000000000000000 00000268 0000000000000059 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
yihua@ubuntu:~/test/static-linker$ readelf -S b.o
There are 11 section headers, starting at offset 0x270:
yihua@ubuntu:~/test/static-linker$ readelf -S b.o
There are 11 section headers, starting at offset 0x270:
Section Headers:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040 000000000000004b 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 0000008c 0000000000000004 0000000000000000 WA 0 0 4
[ 3] .bss NOBITS 0000000000000000 00000090 0000000000000000 0000000000000000 WA 0 0 1
[ 4] .comment PROGBITS 0000000000000000 00000090 000000000000002a 0000000000000001 MS 0 0 1
[ 5] .note.GNU-stack PROGBITS 0000000000000000 000000ba 0000000000000000 0000000000000000 0 0 1
[ 6] .eh_frame PROGBITS 0000000000000000 000000c0 0000000000000038 0000000000000000 A 0 0 8
[ 7] .rela.eh_frame RELA 0000000000000000 00000200 0000000000000018 0000000000000018 I 8 6 8
[ 8] .symtab SYMTAB 0000000000000000 000000f8 00000000000000f0 0000000000000018 9 8 8
[ 9] .strtab STRTAB 0000000000000000 000001e8 0000000000000011 0000000000000000 0 0 1
[10] .shstrtab STRTAB 0000000000000000 00000218 0000000000000054 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
生成可执行程序ab
,可能会出现以下错误提示。
yihua@ubuntu:~/test/static-linker$ ld a.o b.o -e main -o ab
a.o: In function `main':
a.c:(.text+0x46): undefined reference to `__stack_chk_fail'
yihua@ubuntu:~/test/static-linker$
若出现如上错误,那是因为编译目标文件时,默认增加了栈保护。关闭即可。如下:
yihua@ubuntu:~/test/static-linker$ gcc -c -fno-stack-protector a.c b.c
yihua@ubuntu:~/test/static-linker$ ld a.o b.o -e main -o ab
yihua@ubuntu:~/test/static-linker$
可执行程序ab
的段信息如下:
yihua@ubuntu:~/test/static-linker$ readelf -S ab
There are 9 section headers, starting at offset 0x1258:
Section Headers:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 00000000004000e8 000000e8 0000000000000074 0000000000000000 AX 0 0 1
[ 2] .eh_frame PROGBITS 0000000000400160 00000160 0000000000000058 0000000000000000 A 0 0 8
[ 3] .got.plt PROGBITS 0000000000601000 00001000 0000000000000018 0000000000000008 WA 0 0 8
[ 4] .data PROGBITS 0000000000601018 00001018 0000000000000004 0000000000000000 WA 0 0 4
[ 5] .comment PROGBITS 0000000000000000 0000101c 0000000000000029 0000000000000001 MS 0 0 1
[ 6] .symtab SYMTAB 0000000000000000 00001048 0000000000000180 0000000000000018 7 10 8
[ 7] .strtab STRTAB 0000000000000000 000011c8 0000000000000048 0000000000000000 0 0 1
[ 8] .shstrtab STRTAB 0000000000000000 00001210 0000000000000043 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
通过对比a.o
、b.o
、ab
的段信息,我们可以得出两点信息。
- 只有可执行程序才会确定虚拟地址。在链接之前,目标文件中的所有段的VMA都是0,因为虚拟空间并没有被分配,只有当链接为可执行程序
ab
后,才会分配虚拟地址。比如:ab
程序运行时,.text
段会加载到虚拟地址0x00000000004000e8
中;.data
段,加载到虚拟地址0x0000000000601018
中。 - 并不是所有的段都会加载到内存中。即使是可执行程序
ab
,也并不是所有的段,都设置了虚拟地址。仅仅.text
、eh_fream
、.got.plt
、.data
段设置了虚拟地址,其它段并没有。那是其他段在程序真正运行时,并不需要了。 比如.comment
段,记录调试信息的,运行时并不需要。.symtab
段,记录所有的符号,用于链接阶段的符号解析和重定位。当程序运行时,也不再需要了。
空间和地址分配流程大致如下:
符号解析和重定位
通过空间与地址分配后,各段在可执行程序中的虚拟地址是确认的。也就是说.text
段内函数符号,.data
段内的变量符号。其虚拟地址都是已经确认的(段的基地址加上符号在本段的offset)。
比如:a.o
中的main
函数相对于a.o
的.text
段偏移X。但经过空间地址分配之后,a.o
的.text
位于虚拟地址0x00000000004000e8
中,那么main
的虚拟地址为0x00000000004000e8
+X。实际上X=0,那么main
的虚拟地址为0x00000000004000e8
。通过命令查看,确实如此。
yihua@ubuntu:~/test/static-linker$ readelf -s ab
Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000004000e8 0 SECTION LOCAL DEFAULT 1
2: 0000000000400160 0 SECTION LOCAL DEFAULT 2
3: 0000000000601000 0 SECTION LOCAL DEFAULT 3
4: 0000000000601018 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c
7: 0000000000000000 0 FILE LOCAL DEFAULT ABS b.c
8: 0000000000000000 0 FILE LOCAL DEFAULT ABS
9: 0000000000601000 0 OBJECT LOCAL DEFAULT 3 _GLOBAL_OFFSET_TABLE_
10: 0000000000400111 75 FUNC GLOBAL DEFAULT 1 swap
11: 0000000000601018 4 OBJECT GLOBAL DEFAULT 4 shared
12: 000000000060101c 0 NOTYPE GLOBAL DEFAULT 4 __bss_start
13: 00000000004000e8 41 FUNC GLOBAL DEFAULT 1 main
14: 000000000060101c 0 NOTYPE GLOBAL DEFAULT 4 _edata
15: 0000000000601020 0 NOTYPE GLOBAL DEFAULT 4 _end
上述的方式就是符号解析。通过完全一样的计算方式,我们可以得知所有符号的地址。
我们知道a.c
中引用b.c
中的swap
函数和shared
变量。实际上a.o
是不可能知道swap
和shared
的虚拟地址的。那么a.o
是如何使用这两个外部符号的呢?可通过反汇编查看。
由反汇编可知, a.o
对外部符号的引用,暂时将地址设置为0。当符号解析完成,获取符号的虚拟地址,再进行修改。这个过程就是符号重定位。
总结
本章节概述了目标文件链接成可执行文件的过程。由如何合并多个目标文件开始,了解到相似段合并的优点。
进而介绍了链接过程的主要两个步骤:空间与地址分配、符号解析和重定位。并用示例分析其过程。内容较多,希望读者能够自己本地操作一遍,认真思考,推敲。若有疑问,也可与我沟通。
最后,大家可以思考一个问题:符号重定位是非常重要的步骤,那么链接器是如何知道哪些符号需要修正的呢?
若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途