静态链接
当有两个目标文件时,如何将它们连接起来形成一个可执行文件?其中发生了什么?
使用两个源代码文件作为研究例子:
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;
}
使用gcc将两个文件编译成目标文件a.o和b.o
gcc -c a.c b.c
图示:
b.c里面定义了两个全局符号,
-
shared
-
函数swap
a.c里面定义了一个全局符号
- main
a.c引用到了b.c里面的swao与shared,接下来就是把这两个目标文件链接在一起最终形成一个可执行文件
空间与地址分配
可执行文件中代码段和数据段都是输入的目标文件合并而来的,那么链接过程产生的第一个问题:对于多个输入目标文件,链接器如何将它们的各个段合并到输出文件?
按序叠加
一个方案就是按照输入次序叠加起来:
这种做法很简单,但是有一个问题:
在很多输入文件的时候,输出文件将会有很多零散的段,这种做法很浪费空间,因为每个段都需要有一定的地址和空间对齐要求,而且还会造成碎片问题,所以这不是一个好的方案
相似段合并
一个更实际的方法是将相同性质的段合并到一起,比如将所有输入文件的.text合并到输出文件的.text,接着是.data,.bss
图示:
.bss段在目标文件和可执行文件并不占用文件空间,但是在装载时占用地址空间,所以链接器在合并各个段的时候,需要将.bss也合并,并且分配虚拟空间
链接器为目标文件分配地址和空间中,地址和空间有两个含义:
-
一个是在输出的可执行文件中的空间
-
一个是在装载后的虚拟地址中的虚拟地址空间
对于有实际数据的段,如.text和.data,它们在文件中和虚拟地址中都有分配空间,因为它们在这两者中都存在,而对于.bss这样的段来说,分配空间的意义只局限于虚拟地址空间,因为它在文件中并没有内容
现在链接器空间分配策略采用第二种方法,使用这种方法的链接器一般采用两步链接的方法:
- 空间和地址分配
扫描所有的输入目标文件,并且获得它们的各个段的长度,属性,位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放在一个全局符号表。这一步,链接器能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度和位置,并建立映射关系。
- 符合解析与重定位
使用上面的第一步中收集到的所有信息,读取输入文件中段的数据,重定位信息,并且进行符合解析与重定位,调整代码中的地址等。
用链接器将a.o和b.o链接起来:
ld a.o b.o -e main -o ab
接下来使用objdump来查看链接前后地址的分配情况:
objdump -h a.o
输出:
a.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .text 00000051 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000091 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000091 2**0
ALLOC
3 .comment 0000002a 0000000000000000 0000000000000000 00000091 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000bb 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 000000c0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
objdump -h b.o
输出
b.o: 文件格式 elf64-x86-64
节:
Idx Name S