:PLT 与延迟绑定:第一次调用动态库函数时发生了什么)
文章目录一、动态链接的性能问题二、PLT 是什么三、GOT 和 PLT 如何配合四、第一次调用动态库函数时发生什么五、第二次调用为什么更快六、为什么叫延迟绑定七、GOT 默认指向什么八、用 objdump 查看 PLT九、用 readelf 查看重定位项十、用 LD_DEBUG 观察绑定过程十一、LD_BIND_NOW关闭延迟绑定十二、延迟绑定的优缺点十三、GOT、PLT、PIC 三者关系十四、为什么这对理解动态库很重要十五、总结上一篇我们讲了 GOT 和 PIC。GOT 让动态链接器可以把运行时确定的函数地址、变量地址填到一张可写表中代码通过查表完成访问。但还有一个性能问题如果程序启动时就把所有动态库函数的真实地址都解析好会不会太慢毕竟一个程序依赖的动态库可能很多库里的函数更多其中很多函数可能整个运行期间都不会被调用。为了解决这个问题动态链接引入了 PLT 和延迟绑定。一、动态链接的性能问题动态链接把一部分链接工作推迟到了程序加载和运行阶段。这带来了灵活性可执行文件更小 动态库可以共享 库可以独立升级但也带来成本程序启动时需要加载库 需要解析符号 需要做重定位如果启动时把所有外部函数地址都解析完程序启动可能变慢。例如一个程序链接了 libc但它不一定会调用 libc 中的所有函数。提前解析所有函数没有必要。所以更合理的思路是函数真正第一次被调用时再解析它的真实地址。这就是延迟绑定。二、PLT 是什么PLT 是 Procedure Linkage Table过程链接表。它可以理解为动态函数调用的“中转站”或“跳板”。当程序调用外部动态库函数时往往不是直接跳到真实函数地址而是先跳到对应的 PLT 项。例如反汇编时经常看到call putsplt call printfplt这里的putsplt、printfplt就是 PLT 中的入口。三、GOT 和 PLT 如何配合GOT 保存运行时地址PLT 负责调用过程中的跳转和第一次解析。可以先用一句话理解GOT 负责存地址PLT 负责第一次调用时去解析地址。更具体一点第一次调用函数 call 函数plt - PLT 发现 GOT 里还不是最终函数地址 - 跳到动态链接器的解析逻辑 - 动态链接器找到真实函数地址 - 更新 GOT 表项 - 跳转到真实函数 第二次调用函数 call 函数plt - PLT 通过 GOT 直接跳到真实函数这就是延迟绑定的核心。四、第一次调用动态库函数时发生什么假设程序调用printf。第一次调用printf(hello\n);底层大致流程1. 程序执行 call printfplt 2. 进入 printf 对应的 PLT 项 3. PLT 通过 GOT 表项发现还没有绑定真实 printf 地址 4. 跳到动态链接器的符号解析逻辑 5. 动态链接器在 libc.so 中找到 printf 的真实地址 6. 动态链接器把真实地址写入 GOT 表项 7. 跳转到 libc.so 中的 printf 执行这一步完成后GOT 中已经保存了printf的真实地址。五、第二次调用为什么更快第二次再调用printf(world\n);流程就简化了1. 程序执行 call printfplt 2. PLT 通过 GOT 表项直接拿到 printf 真实地址 3. 直接跳转到 libc.so 中的 printf不需要再次让动态链接器解析符号。所以延迟绑定的优势是没有调用过的函数不解析 调用过的函数只在第一次付出解析成本。六、为什么叫延迟绑定绑定可以理解为把一个符号引用和它的真实函数地址建立关系。普通立即绑定程序启动时绑定所有需要的函数延迟绑定函数第一次被调用时才绑定所以它叫 lazy binding也就是延迟绑定。七、GOT 默认指向什么在延迟绑定机制下程序刚启动时某些 GOT 表项里并不是最终函数地址而是指向一段辅助解析代码。这段辅助代码有时也被称为桩代码。第一次调用函数时流程会绕到这段代码再进入动态链接器解析符号。解析完成后动态链接器会把 GOT 表项改成真实函数地址。因此 GOT 表项的内容会发生变化第一次调用前指向解析辅助逻辑 第一次调用后指向真实函数地址这就是为什么 GOT 必须位于可写区域。八、用 objdump 查看 PLT可以反汇编程序objdump-dmain|grep-A10plt可能看到0000000000001030 putsplt: ...或者callq putsplt这些就是动态库函数调用的 PLT 入口。不需要一开始完全看懂每条汇编但要抓住外部函数调用先进入 xxxplt而不是直接写死真实地址。九、用 readelf 查看重定位项可以查看动态重定位信息readelf-rmain你可能看到和 PLT/GOT 相关的重定位项例如R_X86_64_JUMP_SLOT这类重定位项通常和函数调用的延迟绑定有关。动态链接器会根据这些重定位信息知道哪些 GOT 表项需要在运行时处理。十、用 LD_DEBUG 观察绑定过程LD_DEBUG可以让动态链接器输出调试信息。例如LD_DEBUGlibs,bindings ./main可能会输出大量库加载和符号绑定信息。学习时可以重点观察加载了哪些库 查找了哪些符号 绑定了哪些函数输出可能比较多可以配合LD_DEBUGbindings ./main21|grepprintf不同系统输出略有差异但能帮助你直观看到动态链接器确实在运行时参与符号解析。十一、LD_BIND_NOW关闭延迟绑定如果希望程序启动时就完成所有动态符号绑定可以设置LD_BIND_NOW1./main这会让动态链接器倾向于立即绑定而不是等第一次调用再解析。这在某些调试、安全或性能分析场景中有用。也可以在链接时使用相关选项控制绑定策略但初学阶段知道LD_BIND_NOW即可。十二、延迟绑定的优缺点优点降低程序启动时的符号解析成本。没用到的函数不必解析。大型程序和大型动态库中收益明显。缺点第一次调用某个函数时会多一次解析开销。调试动态链接问题时链路更复杂。某些安全机制需要额外处理 GOT/PLT 可写带来的风险。真实系统中还会涉及 RELRO、符号劫持、ASLR 等安全机制。本文先不展开后续可作为拓展内容。十三、GOT、PLT、PIC 三者关系现在可以把三个概念放在一起PIC让代码不依赖固定加载地址 GOT保存运行时需要修正的真实地址 PLT外部函数调用的跳板支持第一次调用时解析动态库能工作依赖的是一整套机制而不是单独某个概念。简化版调用流程程序调用外部函数 - 进入 PLT - PLT 查 GOT - 第一次调用时进入动态链接器解析 - 动态链接器更新 GOT - 后续调用通过 GOT 直接跳转真实函数十四、为什么这对理解动态库很重要前面制作动态库时我们只是记住gcc-fPIC-cxxx.c gcc-shared-olibxxx.so xxx.o现在我们知道背后的原因-fPIC 是为了让动态库适应不同加载地址 GOT 是为了把可变地址放到可写表中 PLT 是为了让函数调用可以延迟解析 动态链接器负责在运行时填表和绑定。这就把“命令怎么写”和“系统为什么需要这样做”连起来了。十五、总结PLT 和延迟绑定解决的是动态链接的性能问题。如果启动时解析所有动态库函数开销可能很大延迟绑定把解析推迟到函数第一次调用时。第一次调用通过 PLT 进入动态链接器解析真实地址后更新 GOT后续调用直接通过 GOT 跳转。到这里我们已经从库的制作使用一路讲到了 ELF、重定位、动态库映射、GOT/PIC、PLT。下一篇作为主线收束把这些知识落到真实工程如何组织库工程、如何发布、如何排查常见错误。