往期地址:
本期主题:
静态链接详解
从往期的目标文件详解(链接在上面)我们已经大概了解ELF文件的结构以及一些相关信息,现在的问题是,这些ELF文件怎么链接在一起,变成一个可执行文件呢?
以下面的两个源码 “a.c” 和 “b.c” 为例:
gcc -c -fno-stack-protector a.c b.c
经过gcc -c 编译后,生成了a.o和b.o两个文件,模块 a.c 中引用了 b.c 模块中的 "shared"变量 以及 “swap函数” ,接下来,我们来看这两个目标文件怎么最终链接成一个可执行文件。
静态链接分为两步:
1.空间和地址的分配
2.符号解析与重定位
总结: 静态链接第一步,多个目标文件链接时,会进行相似段的合并,并且会计算输出文件中的各个段合并后的长度与位置,建立映射关系。例如,a.o的.data段 会与 b.o的.data段合并。
jason@ubuntu:~/WorkSpace/3.OS_study/2.static_link$ objdump -h a.o b.o ab
a.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000032 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000072 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000072 2**0
ALLOC
...
b.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000031 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 00000074 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000078 2**0
ALLOC
...
ab: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
...
1 .text 00000063 0000000000401000 0000000000401000 00001000 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
2 .eh_frame 00000058 0000000000402000 0000000000402000 00002000 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .data 00000004 0000000000404000 0000000000404000 00003000 2**2
CONTENTS, ALLOC, LOAD, DATA
从最后生成的ab文件可以看出,.text、.data段的大小是把a.o和b.o的对应段合并相加了。
还可以由 a.o、b.o、ab 这三个文件来确认符号地址
原则就是:各个符号在段内的相对位置是固定的。例如 main函数符号在a.o中的偏移位置和ab文件中是一样的。
然后我们就可以通过 objdump -d 来确认各个符号的位置:
结果:
main函数的位置: 在a.o的起始位置,(0000位置)
shared变量的位置: 看指令偏移量为17的指令,其中 48 8d 35是lea的指令码,00 00 00 00 是shared变量的位置
swap函数的位置: 看指令偏移量为26的指令,callq是一条相对跳转指令,指的是目的地址相对于下一条指令的偏移,26的下一条是2b,所以就是swap函数位置在 2b
重点: 编译器把这两条指令(shared变量和swap函数)是暂时用地址’0x0000’和地址’0x2b’代替着,真正的地址计算在链接的时候做。(那么链接器怎么知道哪个符号需要重新计算地址,哪个符号不需要呢?
)
解答前面的疑问,链接器是通过重定位表来知道哪个符号需要重定位,重定位表会描述需要进行重定位的符号。
还有使用readelf读符号表
对于c/c++语言来说,编译器默认函数和初始化了的全局变量是强符号,未初始化的全局变量为弱符号。
强弱符号的相关规则:
同样的也有强弱引用的说法:
弱引用的常见形式,使用" attribute((weakref)) "
__attribute__((weakref)) void test();
int main()
{
test();
}
在讲解目标文件时,我们就发现 未初始化的全局变量在链接之前处于COMMON段,这里我们来详细讲解。
现在的编译器和链接器都支持一种COMM块的机制,当不同目标文件所需要的COMMON块大小不一致时,以最大块为准
看一个实际的例子:
a.c 和 b.c都定义未初始化的全局变量,c.c去引用,通过符号表看大小
$ gcc -c -fno-stack-protector a.c b.c c.c
$ ld a.o b.o c.o -e main -o abc
$ readelf -s a.o b.o abc
结果:最终使用了b.c中的 a_comm,占4个字节
总结:
1.把未初始化的全局变量分配在COMM空间,而不直接放在bss段的原因是:未初始化的全局变量是弱符号,假如其他目标文件也有该弱符号,在链接之前不能确定该弱符号的大小,所以大小是未知的,只能先放在COMMON块;
2.总体来看,链接之后,未初始化的全局变量最终还是放在bss段;