Linux延迟绑定机制过程总览ELF文件的延迟绑定Lazy Binding是一种优化动态链接性能的技术。其核心思想是将函数符号的查找与重定位工作从程序启动时推迟到该函数第一次被实际调用时进行。这样做可以避免在程序启动时解析所有动态库函数从而显著加快程序的启动速度。这项技术主要通过PLT过程链接表Procedure Linkage Table和GOT全局偏移表Global Offset Table两个表协同完成。PLT与GOT在了解具体过程前需要先认识这两个表PLT (过程链接表)位于代码段.plt节可执行但不可写。它包含一系列小的代码片段桩代码每个外部函数如printf都对应一个函数名plt的入口。GOT (全局偏移表)位于数据段.got.plt节可写但不可执行。它包含一个地址数组用于存储外部函数的实际内存地址。.got主要用于存储全局变量和静态变量的地址。结构相对简单就是一个纯粹的地址数组按需存放变量地址。延迟绑定的详细流程整个过程可以分为首次调用和后续调用两个阶段。1. 首次调用触发绑定当程序第一次调用一个外部函数例如foo时流程如下进入PLT程序中的call fooplt指令将控制权转移到PLT中对应的桩代码。间接跳转fooplt的第一条指令是jmp *(foogot)它尝试跳转到GOT中foo对应的条目所存储的地址。转向解析由于是首次调用foogot中存储的并不是foo的真实地址而是PLT中的下一条指令地址。因此程序会继续执行fooplt中的下一条指令push n将foo在重定位表中的索引ID压入栈。准备信息接着执行push moduleID将当前模块的ID压入栈。进入解析器最后跳转到PLT的固定入口PLT[0]。调用动态链接器PLT[0]的代码会进一步调用动态链接器中的核心函数_dl_runtime_resolve。此时栈上已经有了刚才压入的模块ID和函数索引。完成绑定_dl_runtime_resolve利用栈上的信息在指定的动态库中查找foo函数的实际地址并将其填入foo对应的GOT条目中。最后它将控制权转移到foo函数程序得以正常执行。2. 后续调用直接使用当程序再次调用foo函数时再次进入fooplt。执行jmp *(foogot)指令。此时foogot中存储的已经是foo函数的真实地址。程序直接跳转到foo函数执行不再经过动态链接器。关键数据结构一览GOT[0]: 存放.dynamic段的地址。GOT[1]: 存放link_map结构的地址用于管理已加载的共享库。GOT[2]: 存放_dl_runtime_resolve函数的地址。PLT[0]: PLT的固定入口负责调用_dl_runtime_resolve。函数名plt: 每个外部函数对应的桩代码包含跳转、压栈和跳转到PLT[0]的指令。示例程序#includestdio.hintmain(intargc,char*argv[]){printf(hello world\n);getchar();return0;}.plt过程链接表包含定义好的存根代码objdump -M intel --section .plt -d demo1.out demo1.out 文件格式 elf64-x86-64 Disassembly of section .plt: 0000000000001020 putsplt-0x10: 1020: ff 35 ca 2f 00 00 push QWORD PTR [rip0x2fca] # 3ff0 _GLOBAL_OFFSET_TABLE_0x8 1026: ff 25 cc 2f 00 00 jmp QWORD PTR [rip0x2fcc] # 3ff8 _GLOBAL_OFFSET_TABLE_0x10 102c: 0f 1f 40 00 nop DWORD PTR [rax0x0] 0000000000001030 putsplt: 1030: ff 25 ca 2f 00 00 jmp QWORD PTR [rip0x2fca] # 4000 putsGLIBC_2.2.5 1036: 68 00 00 00 00 push 0x0 // 在重定位表中索引 103b: e9 e0 ff ff ff jmp 1020 _init0x20 0000000000001040 getcharplt: 1040: ff 25 c2 2f 00 00 jmp QWORD PTR [rip0x2fc2] # 4008 getcharGLIBC_2.2.5 1046: 68 01 00 00 00 push 0x1 104b: e9 d0 ff ff ff jmp 1020 _init0x20每个外部函数对应一个条目其中还有一个默认存根0000000000001020 putsplt-0x10。在存根代码中push的数值并不是函数ID而是 .rela.plt 节中的偏移量reloc_index。动态链接器只需拿到这个数值就能在 .rela.plt 中找到对应的条目。条目中存储的地址就是.got.plt中的条目。动态链接器通过.rela.plt拿到需要修改的地址然后修改.got.plt中对应的条目。.got.plt它存储着外部函数在内存中的实际地址。.got.plt表的前几项有特殊用途而每个外部函数在.got.plt中都对应一个表项。详细分析1. 触发PLT 传递参数在延迟绑定的第一次调用时比如call printfpltprintfplt的指令会执行printfplt: jmp *printfgot.plt ; 跳转到 GOT初始存的是下一条地址 push 0x3 ; 将函数在 .rela.plt 中的“偏移量索引”压栈 jmp common_resolver ; 跳转到动态链接器入口 _dl_runtime_resolve注意这里压入的0x3并不是函数ID而是.rela.plt节中的偏移量reloc_index。动态链接器只需拿到这个数值就能在.rela.plt中找到对应的条目。2. 动态链接器定位当控制权交给_dl_runtime_resolve时它会接收到两个关键参数在 x86-64 中通过寄存器传递rdi存放link_map的地址指向当前模块的链接信息由 GOT[1] 提供。rsi存放 PLT 压入的偏移量0x3reloc_index。动态链接器的定位步骤分为三层第一层定位.rela.plt中的条目拿到“改哪里”动态链接器通过link_map找到.rela.plt节在内存中的起始地址虚拟地址 VA。然后用起始地址加上传入的偏移量0x3计算出对应的Elf64_Rela结构体内存地址。// 伪代码Elf64_Rela*reloc_entry(Elf64_Rela*)(link_map-l_info[DT_JMPREL]-d_un.d_ptrreloc_index);这个Elf64_Rela结构体大小通常为 24 字节包含三个核心字段r_offset 它存放着该函数对应的.got.plt条目的绝对虚拟内存地址例如0x601020。动态链接器拿到这个地址就知道待会要把真实函数地址写入这里。r_info编码了符号表索引和重定位类型。通过解析它链接器能知道要去.dynsym找哪个符号。r_addend修正加数通常用于变量函数重定位时多为 0。第二层定位.dynsym中的符号拿到“改什么”通过r_info的高位部分动态链接器计算出该函数在.dynsym动态符号表中的索引定位到对应的Elf64_Sym结构体。这个结构体里存储着符号在.dynstr动态字符串表中的名字偏移量。第三层解析真实地址动态链接器根据r_info的类型如R_X86_64_JUMP_SLOT结合link_map去遍历共享库如libc.so.6通过字符串如printf找到该函数的实际内存地址。3. 写入完成修改拿到printf的真实内存地址比如0x7f1234567890后动态链接器执行最后一步// 通过 r_offset 找到 .got.plt 槽位直接写入真实地址*(Elf64_Addr*)(reloc_entry-r_offset)real_function_addr;写入完成后.got.plt中printf对应的槽位就变成了0x7f1234567890。后续再调用printfplt时jmp *printfgot.plt就会直接跳转到libc中的真实代码。补充说明.rela.plt与.rela.dyn的区别表名服务对象包含的重定位条目.rela.plt专用于PLT 延迟绑定函数类型多为JUMP_SLOT对应.got.plt中的函数槽位.rela.dyn用于立即绑定变量/数据类型多为GLOB_DAT或RELATIVE对应.got中的变量地址或用于模块基址修正总结延迟绑定机制通过将地址解析推迟到函数首次调用时进行优化了程序的启动性能。在此过程中PLT作为“代理”负责发起解析请求而GOT则作为“缓存”存储解析结果。首次调用时两者协同工作通过动态链接器完成地址绑定后续调用则直接通过GOT跳转实现了效率与性能的平衡。