深入glibc:图解_dl_fixup函数如何一步步解析动态链接函数
在Linux系统中,动态链接是一个至关重要的机制,它允许程序在运行时加载共享库中的函数。这个过程看似简单,但背后却隐藏着复杂的机制。本文将带你深入探索glibc中的_dl_fixup函数,揭示动态链接函数解析的全过程。
1. 动态链接的基本原理
当我们在程序中调用一个动态链接的函数时,实际上经历了一个精巧的"延迟绑定"过程。这个过程确保了只有在函数第一次被调用时才会进行解析,后续调用则直接跳转到解析后的地址。
让我们用一个简单的例子来说明这个过程:
#include <unistd.h> int main() { write(1, "Hello\n", 6); return 0; }编译这个程序后,如果我们查看它的反汇编代码,会发现write函数的调用实际上是通过PLT(Procedure Linkage Table)来实现的。PLT是动态链接机制中的关键组件,它充当了程序与共享库之间的桥梁。
动态链接的核心优势在于:
- 节省内存:多个程序可以共享同一个库的代码段
- 灵活性:库可以独立更新而不需要重新编译程序
- 加载效率:只有实际使用的函数才会被解析和加载
2. PLT和GOT的协作机制
PLT和GOT(Global Offset Table)共同构成了动态链接的基础设施。让我们详细看看它们是如何工作的:
第一次调用函数时:
- 程序跳转到PLT表项
- PLT表项跳转到GOT表项
- 由于是第一次调用,GOT表项指向PLT中的下一条指令
- 程序压入重定位偏移量(reloc_arg)
- 跳转到PLT[0],即动态链接器的入口
后续调用时:
- 程序跳转到PLT表项
- PLT表项跳转到GOT表项
- GOT表项此时已经保存了函数的真实地址
- 直接跳转到目标函数
这个过程中,_dl_runtime_resolve函数扮演了关键角色。它接收两个参数:
link_map:包含动态链接信息的结构体reloc_arg:重定位偏移量
_dl_runtime_resolve实际上会调用_dl_fixup函数来完成具体的解析工作。
3. _dl_fixup函数的详细解析
_dl_fixup函数是动态链接解析过程的核心,它位于glibc的elf/dl-runtime.c文件中。让我们逐步分析它的工作原理:
_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg) { // 获取重定位表项 const PLTREL *const reloc = (const void *)(D_PTR(l, l_info[DT_JMPREL]) + reloc_offset); // 获取符号表项 const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)]; // 检查重定位类型 assert(ELF(R_TYPE)(reloc->info) == ELF_MACHINE_JMP_SLOT); // 查找符号 result = _dl_lookup_symbol_x(strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL); // 计算函数地址 value = DL_FIXUP_MAKE_VALUE(result, sym ? (LOOKUP_VALUE_ADDRESS(result) + sym->st_value) : 0); // 更新GOT表 return elf_machine_fixup_plt(l, result, reloc, rel_addr, value); }这个过程可以分解为以下几个关键步骤:
定位重定位表项:
- 通过
reloc_arg在.rel.plt节中找到对应的重定位表项 - 这个表项包含了函数在符号表中的索引和重定位类型
- 通过
获取符号信息:
- 通过重定位表项中的
r_info字段找到符号表项 - 符号表项包含了函数名在字符串表中的偏移等信息
- 通过重定位表项中的
符号查找:
- 使用
_dl_lookup_symbol_x函数在加载的共享库中查找符号 - 这个函数会返回包含目标符号的库的基地址
- 使用
地址计算与更新:
- 计算函数的实际地址(库基地址 + 符号偏移)
- 将这个地址写入GOT表的对应条目中
4. 关键数据结构解析
理解动态链接机制需要熟悉几个关键的数据结构:
4.1 link_map结构
link_map是动态链接器的核心数据结构,它包含了加载对象的所有信息:
struct link_map { ElfW(Addr) l_addr; // 库的加载地址 char *l_name; // 库的绝对文件名 ElfW(Dyn) *l_ld; // 动态段地址 struct link_map *l_next, *l_prev; // 链表中前后对象 // ... 其他字段 ... ElfW(Dyn) *l_info[DT_NUM]; // 动态段信息数组 };其中l_info数组包含了指向各种动态段信息的指针,如:
DT_JMPREL:指向PLT重定位表DT_SYMTAB:指向动态符号表DT_STRTAB:指向字符串表
4.2 重定位表项
重定位表项(Elf32_Rel/Elf64_Rela)描述了如何修改指令或数据以正确引用符号:
typedef struct { Elf32_Addr r_offset; // 需要修改的地址(通常是GOT表项) Elf32_Word r_info; // 符号索引和重定位类型 } Elf32_Rel;r_info字段的高24位是符号索引,低8位是重定位类型。
4.3 符号表项
符号表项(Elf32_Sym/Elf64_Sym)描述了符号的各种属性:
typedef struct { Elf32_Word st_name; // 符号名在字符串表中的偏移 Elf32_Addr st_value; // 符号值(通常是偏移量) Elf32_Word st_size; // 符号大小 unsigned char st_info;// 符号类型和绑定属性 unsigned char st_other;// 符号可见性 Elf32_Section st_shndx;// 相关节索引 } Elf32_Sym;5. 动态链接的安全考量
理解动态链接机制不仅有助于深入理解Linux系统,也对系统安全有重要意义。动态链接过程中涉及多个数据结构的交互,如果这些数据结构被恶意篡改,就可能导致安全漏洞。
在安全研究中,有一种称为"ret2dlresolve"的技术就是利用了动态链接机制的特性。这种技术允许攻击者在特定条件下控制函数的解析过程,从而执行任意代码。理解_dl_fixup的工作原理是分析和防御这类攻击的基础。
6. 调试动态链接过程
为了更好地理解动态链接的实际工作过程,我们可以使用GDB进行调试。以下是一个简单的调试步骤:
在PLT表项设置断点:
break *write@plt跟踪第一次调用时的执行流程:
stepi观察GOT表的变化:
x/x &write@got跟踪
_dl_runtime_resolve的执行:finish
通过这样的调试,我们可以直观地看到动态链接的整个过程,从PLT跳转到GOT,再到_dl_runtime_resolve和_dl_fixup的调用,最后GOT表被更新为函数的真实地址。
7. 实际案例分析
让我们通过一个实际的例子来观察动态链接的过程。考虑以下简单的漏洞程序:
#include <unistd.h> #include <stdio.h> #include <string.h> void vuln() { char buf[100]; read(0, buf, 256); } int main() { char buf[100] = "Welcome!\n"; write(1, buf, strlen(buf)); vuln(); return 0; }编译这个程序时,我们可以观察到:
write和read都是动态链接的函数- 它们的PLT表项和GOT表项在二进制中是明确可见的
- 第一次调用这些函数时会触发解析过程
通过分析这个程序的动态链接行为,我们可以更深入地理解_dl_fixup的实际工作方式。
8. 总结与展望
动态链接是现代操作系统中的重要机制,而_dl_fixup函数则是这个机制的核心。通过本文的分析,我们了解了:
- PLT和GOT如何协作实现延迟绑定
_dl_runtime_resolve和_dl_fixup的调用关系- 动态链接过程中涉及的关键数据结构
- 如何调试和观察动态链接的实际过程
理解这些底层机制不仅有助于我们编写更高效的代码,也为深入分析系统安全提供了基础。随着技术的发展,动态链接机制也在不断演进,但基本原理仍然保持着一致性。