PIC位置无关代码

2019-04-15 12:46发布

什么是PLT,如何通过调用“function@PLT”来调用“函数”?
要了解过程链接表(PLT),先让我简要介绍一下共享库!
与静态库不同,共享库代码段在多个进程之间共享,而其数据段对于每个进程是唯一的。这有助于减少内存和磁盘空间。由于代码段在多个进程之间共享,所以应该只有read和execute权限,因此动态链接器不能重新定位代码段中存在的数据符号或函数地址(因为它没有写权限)。那么动态链接如何在运行时重新定位共享库符号而不修改其代码段?它使用PIC完成! 什么是PIC?
位置无关代码(PIC)是为了解决这个问题而开发的 - 它确保共享库代码段在多个进程之间共享,尽管在加载时执行重定位。PIC通过一级间接寻址实现这一点-共享库代码段不包含绝对虚拟地址来代替全局符号和函数引用,而是指向数据段中的特定表。该表是全局符号和函数绝对虚拟地址的占位符。动态链接器作为重定位的一部分来填充此表。因此,只有重定位数据段被修改,代码段保持不变!
动态链接器以两种不同的方式重新定位PIC中发现的全局符号和函数,如下所述:
全局偏移表(GOT):
全局偏移表包含每个全局变量的4字节条目,其中4字节条目包含全局变量的地址。当代码段中的指令引用全局变量时,而不是全局变量的绝对虚拟地址,指令指向GOT中条目。当加载共享库时,GOT条目由动态链接器重新定位。因此,PIC使用该表来重新定位具有单个间接级别的全局符号。
过程链接表(PLT):
过程链接表包含每个全局函数的存根代码。代码段中的调用指令不直接调用函数(’function’),而是调用存根代码(function @ PLT)。这个存根代码在动态链接器的帮助下解析了函数地址并将其复制到GOT(GOT [n])。这次解析仅在函数(’function’)的第一次调用期间发生,稍后当代码段中的调用指令调用存根代码(function @PLT)时,而不是调用动态链接器来解析函数地址(’function’)存根代码直接从GOT(GOT [n])获取功能地址并跳转到它。因此,PIC使用这个表来重新定位具有两级间接的功能地址。 很好,你阅读了关于PIC的内容,并了解了它有助于保持共享库代码段的完整,因此它有助于共享库代码段在许多进程之间真正的共享!但是你有没有想过,为什么当它不共享任何进程时,在可执行文件的代码段需要有一个GOT 条目或PLT存根代码?它的安全保护机制。现在默认情况下,代码段只能被赋予读取和执行权限,没有写入权限(R_X)。这种安全保护机制甚至不允许动态链接器写入代码段,因此它不能重新定位代码段中发现的数据符号或函数地址(原因、、、、、)。因此,为了允许动态链接器重定位,可执行文件也需要GOT 条目和PLT存根代码,就像共享库一样! 参考:
深入理解计算机系统第三版第七章 链接
程序员自我修养第七章
其中文pdf下载地址(百度云资源):https://download.csdn.net/download/hujiuding/10382661 把指令中那些需要修改的部分分离出来,跟数据部分放在一起,这样之类部分就可以保持不变,而数据部分就可以在每一个进程钟拥有一个副本。
共享库的一个关键目的是为了使多个进程能够共享内存中的同一份代码拷贝,已达到节约内存资源的目的。
一个更好的方法是将动态库编译成可以在任意位置加载而无需链接器进行修改。这样的代码被称作位置无关代码(PIC)。GNU编译系统可以通过指定-fPIC选项来生成PIC代码。
在IA32系统中,同一个模块中的过程调用无需特殊处理就是PIC的,因为其引用相对于PC地址的偏移量是已知的。但是,对外部过程的调用和对全局变量的引用一般却不是PIC的,因此需要在链接的时候进行重定位。 PIC背后的思想是简单的——对代码中访问的所有全局数据与函数添加一层额外的抽象。通过巧妙地利用链接与载入过程中的某些工件,使得共享库的代码节真正位置无关是可能的,从这个意义来说它可以不做任何改变而容易地映射到不同的内存地址。
https://blog.csdn.net/wuhui_gdnt/article/details/51094732(讲的很好)
https://eli.thegreenplace.net/2011/08/25/load-time-relocation-of-shared-libraries/ 关键的洞察#1——代码与数据节间的偏移
PIC所依赖的关键的洞察之一是代码与数据节间的偏移,在链接时刻为链接器所知。当链接器将几个目标文件合并起来时,它收集它们的节(例如,所有的代码节合并为一个大的代码节)。因此,链接器知道节的大小与它们的相对位置
这里写图片描述
在上图中,代码节被载入到某个地址(链接时刻未知)0xXXXX000(X表示无关紧要),数据节紧随其后,在0xXXXXF0000。那么,如果在代码节0x80偏移处的指令想访问数据节里的内容,链接器知道相对偏移(这个情形里是0xEF80)并将它写入该指令。
注意到是否有另一个节插在代码节及数据节之间,或者代码节跟在数据节后,是无关紧要的。因为链接器知道所有节的大小并决定何处放置它们,这个洞察成立。 X86上是这样
这里写图片描述
还是要在数据节里创建一个重定位 延迟(lazy)绑定方案,其工作被推迟直到它被真正需要的最后一刻,目的是如果在程序的一次特殊运行中不需要其结果,就可以避免执行之。延迟的好例子有写时拷贝以及延迟求值。
这个延迟绑定方案通过添加另一层的间接性——PLT(程序链接表)来实现。
在共享库第一次载入时,函数调用还没解析:
这里写图片描述
这里写图片描述 在第一次调用后,图看起来有点不一样:
这里写图片描述
这里写图片描述
为唯一使用绝对地址的地方是GOT,GOT位于代码节并且由动态载入器重定位