什么是重定位

我们编译可执行程序的时候,调用的函数只有声明没有实现会出现编译错误。总之是要在编译期间解决的问题。

我们编译动态库的时候,调用的函数是可以没有实现的。那么加载动态库后如何正确的执行函数呢?毕竟编译期间根本没有解决这个函数符号。

编译时不解决,那就记录下来,留到以后解决吧。

objdump -C -R会打印出未解决的符号,和符号被调用时候的地址。比如有个未解决符号是f1

0x80abcde4———-f1

objdump -C -d查看调用f1的代码,可能如下:

0x80abcde0—–call 0x80abcde4

执行到0x80abcde0,代码就是call后4个字节的内容,我们假设call指令占用4个字节的话。正好是0x80abcde4。我们等着0x80abcde4被填上f1的真正地址。

程序启动后,会记录他所加载的符号和符号地址,无论是来自可执行文件还是动态链接库。

当它加载到那个还没有解决f1符号的动态链接库的时候,程序会尝试从所记录的符号表中查找f1,如果能找到,嗯,就把动态链接库中所有调用f1的位置都填上正确的值。比如0x80abcde4

我所说的只是重定位中的一种,称为动态链接。实际上静态链接的时候也有重定位过程。

什么是PIC

PIC是地址无关代码,由gcc编译时指定选项-fPIC开启。

首先说什么是地址相关代码,当程序调用函数f1的时候,实际汇编代码是这样的:

call 0x80abcde4

这个0x80abcde4就是f1的函数绝对地址。

为了调用f1而将它的绝对地址编译到调用中的做法,叫做地址相关代码。

地址无关代码恰好相反,编译器增加一个符号f1_ptr,存储函数地址。实际上是等价于增加了指向函数地址的全局指针变量。

那么调用f1的时候,是这么调用的:

call *(0x80123456)

0x80123456f1_ptr的地址,存储的是f1的函数绝对地址值。这个值在程序运行期间是可以修改的,那么调用的时候实际就跟f1的绝对地址无关了。

f1_ptr的地址是相对的,这就是地址无关代码。

当然编译器并没有真的增加一个名称是f1_ptr的符号,但确实增加了一个匿名全局变量,保存f1的函数地址,重定位的时候,只需要修改这个变量值。

地址无关代码的收益

当程序要为某个动态库重定位未解决的符号的时候,如果是地址相关代码,那么重定位需要修改动态库的代码。

若有多个程序同时使用这个动态库,系统不得不加载多份代码到内存中,因为每一份代码都要被修改。

地址无关的动态库在共享方面则不会有这个问题,系统只加载一份代码,每个程序都有一份f1_ptr变量,修改这份变量同样达到重定位的效果。

什么是导出符号

重定位过程中提到程序会记录所加载的符号和符号地址。那么什么符号才会被记录呢?是整个程序所有的符号吗?

符号是导出符号的超集,符号记录在.sym中,导出符号记录在.dynsmy中。只有导出符号才能用来供重定位过程查找。

可以通过objdump -C -T查看动态符号。

程序启动后,首先加载自身的导出符号,然后挨个加载动态库的导出符号,然后用链表串联起来。

重定位查找一个符号的时候,从链表头开始查找,第一次找到符号为止。因此即使后面的模块有定义这个符号,也等价于被隐藏起来了。

总是重定位,总是导出符号

事实上,动态链接库不仅仅需要重定位未解决的符号,他把所有调用过函数都编译成重定位的形式。这就形成了很有趣的用法,比如替换分配器,测试和性能收集。

动态链接库只导出他自身实现的符号,而可执行文件则不导出符号。

gcc编译时指定-rdynamic选项,可执行文件会导出所有他自身以及静态库所实现的符号。这样可执行程序本身就可以为它所依赖的动态链接库本身提供实现,真是挺奇葩的。

什么是PLT

PTL是.plt段,用来在重定位过程中对符号的真实地址进行惰性求值。仅限于PIC代码。

因为动态链接库将所有调用的函数都编译成重定位的形式,数量十分巨大。如果程序在加载动态链接库的时候就重定位所有符号,比较耗时间。

因此第一次调用的时候才去查找符号的绝对地址就比较划算。实际上PIC代码调用一个函数的时候,并不像上面所说的直接

call *(f1_ptr)

而是调用一个PLT函数

call f1@plt

f1@plt的第一条指令是

jmp f1_ptr

f1_ptr的默认值是f1@plt的第二条指令,因此继续执行下一条指令,这条指令就是去重定位f1符号,填写到f1_ptr中。

调用一次后,f1_ptr就存储了f1的绝对地址,不需要再次重定位了。