本篇文章承接上一篇动态链接1
单靠上面的解释, 读者可能会有疑惑,既然数据a, p和printf语句在模块内部的相对地址是不变的,为什么不直接用相对pc的寻址方式拿到a, p地址呢,而非要借助.got
表来绕一下弯子呢?
这其实涉及到一个全局符号介入(global symbol interpose)的问题。在静态链接中,我们不允许符号冲突。但是在动态链接中,默认是允许的(也可以修改链接参数改变链接器这一行为)。
以之前的weakref.c
文件为基础,再引入下面的havea.c
文件
// havea.c
int y();
int a = 10;
int b = 13;
int *p = &b;
int main(){
y();
return 0;
}
// gcc -fPIC -shared -o weakref.so weakref.c
// gcc -o havea havea.c ./weakref.so
按照上面的指令编译链接后,运行得到的结果是怎样的呢?事实上,我们得到的结果是a = 13
。
动态链接器对于符号的处理是这样,加载可执行文件后,以广度或者深度优先搜索的顺序加载相应的动态库依赖,若发生符号冲突,以先加载上来的符号为准。
因此最终weakref.so
中.got
表填入p的地址是havea.c
中数据p的地址。如果在weakref.so
中采用pc相对寻址获得p的地址,那么最终得到的是动态库内部的数据p的地址,与上面的处理原则相违。
当然读者可能觉得有些别扭,因为命名冲突本来就是一个不好的习惯。那我们修改一下havea.c
。
// havea.c
#include
extern int a;
//int b = 13;
//extern int *p;
int main(){
printf(".c a = %d\n", a);
return 0;
}
// gcc -o havea havea.c ./weakref.so
这次在havea.c
中显式地引用weakref.so
中的数据a,编译运行后得到自然的结果.c a = 5
。我们来猜猜在可执行文件havea
中数据a应该位于哪个段。
直观上来讲,a应该位于UND
中,因为a的实际定义位于weakref.so
中,实际上如何呢?
$ readelf -s havea
Num: Value Size Type Bind Vis Ndx Name
57: 0000000000201010 4 OBJECT GLOBAL DEFAULT 24 a
Ndx=24
对应的是bss
段中。这意味着数据a现在有两份副本了,一份在heava
中,一份在weakref.so
中,这难道不会出问题吗?
造成这个现象的根本原因在于havea.c
在编译为havea.o
的时候并不清楚最终会以静态链接的方式得到数据a还是以动态链接的方式得到数据a,于是默认以静态链接的方式引用数据a,反汇编havea.o
可以看出来。
$ objdump -d havea.o
havea.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # a
a: 89 c6 mov %eax,%esi
c: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 13
13: b8 00 00 00 00 mov $0x0,%eax
18: e8 00 00 00 00 callq 1d <main+0x1d>
1d: b8 00 00 00 00 mov $0x0,%eax
22: 5d pop %rbp
23: c3 retq
注意地址4处的指令,它是采用了静态链接下的访问全局变量的方法(如果是动态链接,会先访问.got
表拿到变量地址,多一次访存)。
因此链接器需要单独处理这种情况,书上讲链接器的解决办法就是把所有引用该变量的语句都指向可执行文件中的符号,也就是说,如果可执行文件的.dynsym
节中出现了该符号,那么就把动态库中该符号的.got
表项指向源文件中该符号的地址,如果动态库中该符号被初始化了,还需要把初始值也copy到加载后源文件的内存镜像对应的.bss
段中。
但是我测试的时候遇到一个奇怪的问题, 如果我把havea.c
写成这样
// havea.c
#include
int a;
//int b = 13;
extern int *p;
int main(){
printf(".c a = %d, *p = %d\n", a, *p);
return 0;
}
最终链接后输出是a = 5
,说明链接器把共享库中的值copy到了源文件加载后的.bss
段,但是如果我把上面的代码从int a;
改为int a = 0;
,最终输出的结果是a = 0
。说明链接器没有把共享库中的值copy过来。但是两种情况下符号a都是位于.bss
段的,不太理解链接器是如何把这两种情况区分开的。
留下疑惑。
为了解决全局符号介入,最好的办法是在动态库中尽可能多地使用static
关键字,这样在link editor阶段就可以自信地用PC相对寻址的方法访问该变量,而不用使用.got
表,也不会额外增加一个重定位条目。
与全局符号介入相关的一个概念是copy relocation
,也就是上面提到的把动态库中的符号copy到可执行文件中,这是通过link editor
在.rela.dyn
生成copy relocation
条目辅助实现的。上面int a=0
没有copy,而int a
时copy了动态库的值,只是因为前者没有生成copy重定位条目,而后者生成了。
.plt.got
section之前有提到,对应实际机器代码的除了.plt
节之外,还有一个.plt.got
节,这个东西是干嘛的呢?
仔细想想会发现,其实并不是所有的函数符号都可以使用延迟绑定的方法,如果一个函数符号在调用前需要使用它的地址(参见弱符号与弱引用的weakref示例),那么该函数符号是不能够延迟绑定的。
0000000000000560 <_foo@plt>:
560: ff 25 82 0a 20 00 jmpq *0x200a82(%rip) # 200fe8 <_foo>
566: 66 90 xchg %ax,%ax
.plt.got
节的函数都长上面这样,它们对应的跳转地址不再保存到.got.plt
表中,而是保存到.got
表中,因为它们不会延迟绑定,不再需要之前提到的额外的push
指令这些。对应的重定位条目位于.rela.dyn
中。
这时候可以发现, .rela,dyn
节中的重定位条目对应都是.got
表中的项,且对应的条目都是非延迟绑定的。而.rela.plt
节中的重定位条目对应的都是.got.plt
节中的项,且都是延迟绑定的。