ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

ELF加载、动态链接、GOT与PLT

ELF加载、动态链接、GOT与PLT 前面已经弄清楚了.o、ELF、Section、Segment 和静态链接。程序编译链接完成以后还差最后一步它到底是怎么跑起来的比如我在终端输入./main程序显然不会凭空出现在内存里。操作系统需要先读取 ELF 文件建立进程的地址空间把程序需要的内容加载进去。如果程序还依赖了libc.so libmystdio.so这些动态库也得处理。这一篇主要把这部分串起来。一、ELF还没加载进内存的时候有地址吗刚开始接触这个问题时我会觉得程序都还在磁盘里没有进入内存怎么可能已经有地址但 ELF 文件本身已经有自己的地址组织方式了。在现代计算机的平坦模式下程序中的代码和数据会进行统一编址。所以即使程序此时还在磁盘上这些代码和数据也已经拥有对应的逻辑地址关系。使用objdump-Smain或者查看 ELF Headerreadelf-hmain都可以看到这些信息。二、程序从哪里开始执行一个 ELF 可执行文件里面有一个很重要的字段Entry point address也就是程序入口地址。比如Entry point address: 0x1060程序启动的时候并不是直接跑到我们写的main()而是先从入口位置开始。Linux 下通常会先进入_start然后经过 C 运行时环境的初始化最后才进入main所以程序启动 ↓ _start ↓ 初始化 ↓ 动态链接等处理 ↓ __libc_start_main ↓ main这个过程平时写 C/C 的时候基本看不到但真正运行的时候确实存在。三、Segment和进程地址空间前面研究 ELF 的时候已经知道Section 最终会根据属性组合成 Segment。程序加载的时候真正和内存布局关系比较密切的就是 Segment。一个 Segment 会有自己的起始地址 长度 权限这些信息可以拿来初始化进程地址空间中的对应区域。可以粗略地理解成ELF ↓ Program Header Table ↓ 找到各个需要加载的 Segment ↓ 建立进程地址空间 ↓ 把对应内容映射进去所以 Program Header Table 对运行时加载非常重要。四、是不是所有Section都会加载到内存不是。ELF 里有很多 Section 是为了链接、调试或者描述文件结构准备的并不是每一个都需要作为程序内容加载到内存。真正和加载有关的是 Program Header Table 里面的 Segment。例如readelf-lmain可以看到LOAD LOAD DYNAMIC ...其中LOAD表示这一部分后面要被加载。所以Section ↓ ELF文件内部怎么组织 Segment ↓ 运行时怎么加载这个区别到这里就比较好理解了。五、静态链接和动态链接最大的区别静态链接的时候.o 静态库中的.o ↓ 一起链接 ↓ 独立可执行文件库里的代码已经进入最终程序。动态链接则不一样。比如ldd main.exe可能看到libc.so.6 /lib64/ld-linux-x86-64.so.2这些动态库并没有在编译链接时把完整代码直接塞进可执行文件。程序真正启动以后动态链接器才会去处理这些库。所以可以把动态链接理解成把一部分链接工作推迟到程序运行的时候。六、动态库为什么不会每个进程都复制一份假设机器上同时运行程序A 程序B 程序C它们都需要libc.so如果每个进程都在内存里完整复制一份那肯定浪费。动态链接的一个重要特点就是多个进程可以共享同一份动态库代码。动态库加载到内存以后不同进程都可以通过自己的地址空间去访问它从而减少内存和磁盘空间的浪费。七、动态库加载以后地址一定一样吗不一定。一个进程和另一个进程的地址空间并不相同。而且一个动态库每次加载到内存的位置也可能不同。所以动态库不能简单地写死我永远在 0x12345678这种绝对地址。它更适合使用相对地址。也就是动态库起始地址 函数在库中的偏移量 函数真正的地址这样动态库换一个位置以后只要起始地址变化内部的偏移关系仍然有效。八、动态库到底怎么和当前进程对应起来可以把过程想成这样进程 ↓ 加载动态库 ↓ 动态库映射到进程地址空间 ↓ 知道动态库起始地址 ↓ 知道某个函数在库里的偏移量 ↓ 找到函数比如某个函数在动态库中的偏移是0x500当前动态库加载到0x70000000那么函数地址就可以理解成0x70000000 0x500核心思想就是起始地址 偏移量。九、问题来了代码区不是只读的吗这里就出现一个很有意思的问题。程序中的.text一般是代码区而代码区不能随便修改。但是动态链接又需要在运行时确定函数地址。那怎么办总不能直接把.text里面的指令改来改去吧。于是就出现了GOTGlobal Offset Table全局偏移量表十、GOT到底是干什么的GOT 可以理解成程序专门准备的一张“地址表”。它通常位于可读写的数据区域因此运行时可以修改。里面保存的是程序当前需要访问的外部函数或者全局变量的地址。所以原来的思路代码区 ↓ 直接写死外部函数地址变成代码区 ↓ 查GOT ↓ 找到真正地址 ↓ 跳过去这样就不用直接修改代码区。十一、为什么每个进程都需要自己的GOT假设进程A里面的libc.so加载到了0x70000000而进程B里的libc.so可能加载到了另一个位置。那么进程A里的GOT和进程B里的GOT当然不能完全一样。所以动态库代码本身可以共享但 GOT 不能简单地让所有进程共用。每个进程都要根据自己的地址空间建立对应关系。十二、PIC为什么和GOT有关动态库制作的时候我们执行过gcc-fPIC-cmy_stdio.c以前只知道-fPIC就是生成位置无关代码。现在再看就容易理解很多了。动态库需要能够加载到不同的地址还要正常访问其中的函数和数据这就需要相对寻址和 GOT 一起工作。可以简单记成PIC 相对编址 GOT这也是为什么生成动态库的时候需要-fPIC十三、PLT又是干什么的如果一个程序依赖很多动态库函数那么程序启动的时候把所有函数地址全部处理一遍会比较耗时间。假设程序依赖printf write close strlen ...但是实际运行的时候可能只调用其中几个。那一开始把所有函数都重定位一遍就有点浪费了。所以又出现了一种优化延迟绑定。对应的就是PLTProcedure Linkage Table过程链接表十四、延迟绑定是怎么工作的一开始GOT中的函数地址 ↓ 辅助代码第一次调用某个函数的时候调用函数 ↓ 进入PLT ↓ 查找真正函数地址 ↓ 修改GOT ↓ 跳转到真正函数等第二次再调用调用函数 ↓ PLT ↓ GOT里已经有真正地址 ↓ 直接跳转也就是说第一次调用的时候麻烦一点后面就可以直接用了。这样就避免了程序启动时把大量暂时用不到的函数全部处理一遍。十五、把整个动态链接过程串起来到这里可以把动态链接整个过程串起来了。程序运行./main↓读取 ELF↓找到程序需要的 Segment↓建立进程地址空间↓加载程序本身↓根据依赖关系找到动态库↓把动态库映射到当前进程地址空间↓确定动态库的加载地址↓处理 GOT↓调用动态库函数↓通过 PLT/GOT 找到真正地址↓进入动态库函数执行这样整个过程就顺起来了。十六、静态链接和动态链接放在一起最后再对比一下。静态链接hello.o code.o 静态库 ↓ 链接 ↓ 地址重定位 ↓ 独立可执行程序程序生成以后静态库本身不需要再参与运行。动态链接.o ↓ 生成可执行程序 ↓ 程序运行 ↓ 加载动态库 ↓ 建立地址映射 ↓ GOT / PLT ↓ 调用动态库函数动态链接把一部分工作放到了程序运行的时候完成因此需要处理库加载和运行时地址重定位。十七、这部分知识终于串起来了以前看到.a .so ELF GOT PLT Segment Section感觉一个比一个陌生。现在把它们按照程序运行的顺序排起来就比较容易理解源代码 ↓ 编译 ↓ .o ↓ 链接 ↓ ELF可执行文件 ↓ 加载 ↓ 进程地址空间 ↓ 动态库 ↓ GOT ↓ PLT ↓ 真正的库函数而静态库和动态库的区别也可以放在这里一起看静态库 ↓ 链接阶段进入程序 动态库 ↓ 运行阶段加载和处理这样再去看ldd readelf objdump这些命令就不只是记它们的用法了而是知道自己到底在看什么。这一部分对我来说比较重要的一点就是开始慢慢把“编译出来的文件”和“真正运行起来的程序”联系到了一起。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表