共享库,在Linux称为shared object
,即.so
文件,在Windows中成为dynamic linked library
,即.dll
文件。使用共享库的主要目的就是“共享”,
即内存中只有一份代码,而多个进程都可以使用它,当然使用时涉及到内存的分页机制,
这里不作展开。共享库的一种很常用的实现方式就是位置无关代码
(position independent
code),该方式生成的共享库,其代码部分在动态载入并链接到内存中时是不会有任何修改的,
这些代码放到内存的任何位置都可以正常工作,所以叫做与位置无关的代码
。
这里我们关注位置无关代码
(PIC)是如何实现的。毕竟共享库映射到进程内存空间时,其
位置是不固定的,程序代码是如何知道共享库中函数的具体地址的,共享库中函数之间
相互调用时又是如何确定具体内存地址的,共享库中的全局变量(或者static)变量地址
又是如何确定的?
这些问题其实很简单,魔力之源就是全局偏移表
(Global Offset Table),它被放置
在.so
文件的.GOT
section中。当调用函数或者引用数据(.so中的数据)时,不是
直接使用函数或者数据的绝对地址,而是使用GOT中相关索引中保存的地址。也就是说所有
函数、全局变量的绝对内存地址都放到GOT中,GOT其实就是个unsigned int
数组。
.so
文件载入内存之后的样子大致是这个样子的,
在链接阶段,code section的大小是可以知道的,而每条指令在code section中的偏移
也是已知的,所以从某一条指令到GOT的偏移量也就是可知的了,例如
1 | ; 1. 取得GOT的真实地址,也就是当前指令的真实地址加上偏移量 lea ebx, ADDR_OF_GOT ; 2. 取得变量的真实地址,0x10为数据在GOT中索引,这在链接时即可确定 mov edx, DWORD PTR [ebx + 0x10] ; 3. 取得变量的值 mov edx, DWORD PTR [edx] |
上面是对变量的重定位,对于函数呢,函数之间的相对地址早在链接阶段就是已知的了,
所以.so
内部的函数调用不需要重定位,至于外部调用.so
内部函数,同样只需查阅
GOT即可。但是GOT中的真实地址又是怎么来的呢?答案是动态连接器,在Linux中就是ld.xxx.so
,它在加载.so
文件到内存时会知道.so
代码在内存中的真实地址,然后
根据重定位表(.dynamic
)即可知道每个函数、全局变量的偏移量及其在GOT中的索引,
进而就可以填充GOT表了。
看似很完美,但是实际上函数的处理方式并非如此,函数采用一种叫做延迟加载
的技术,
即在函数未被调用之前,其在GOT中的真实地址并没有得出,而是在第一次调用时,调用
动态加载器计算函数的真实地址,并将真实地址填入GOT的相关条目中,下次再调用该函数
时就直接使用GOT中记录的真实地址,而无需再次调用动态加载器了。
延迟加载
技术其实也蛮简单,关键点就是过程链接表
(procedure linkage table, PLT),
编译阶段,编译器会为每个函数生成一个代码片段,叫做stub
,不论什么函数,其对应
的stub都是16字节,而且这个stub的格式也都是一样的,这些stub构成过程链接表,
1 | PLT[0]: call 'linker' ; ... PLT[n]: jmp *GOT[m] push 'index of func' jmp PLT[0] |
而在GOT中,函数对应的条目默认值为该函数的stub中push
指令的地址,所以第一次
调用某一个函数的流程大致是这样的:
- call ‘plt item’,这个是在链接时就被修改了的,PLT中的item其实也是一个小函数,
其相对地址在链接阶段就是已知的(链接器创建了这些小函数) - jmp *GOT[m],m是该函数在GOT中对应的索引,链接阶段已知,这句话就是跳转到
该函数在GOT中的对应项保存的地址处,而GOT默认保存的是该函数对应的PLT项的push指令
地址,所以这一句的意思其实就是接着执行push指令
- push ‘index of func’,把该函数在动态链接表(.dynamic)中的索引入栈,它其实
就是调用linker的参数,看下一条 - jmp PLT[0],PLT[0]不同于其他项,它保存的指令就是调用链接器,根据3中给出参数
计算函数的真实地址,然后修改函数对应的GOT项,(即GOT[m])
以后再次调用该函数时,情况发生了戏剧性的变化:
- 同第一次调用一样,仍然执行其对应的PLT项
- 同第一次调用一样,仍然跳转到其对应的GOT项保存的地址处,但是由于第一次调用时
动态链接器已经修改了该GOT项(上面的步骤4),所以这次直接跳转到该函数的真实地址处,
不再调用linker了
于是,延迟加载
过程就这样完成了。
以上就是位置无关代码的实现方式,只是描述思想,不必计较细节。
(over)