dl_runtime_resolve 动态链接重定位
参考资料:https://xz.aliyun.com/news/17612
ELF 文件格式:
ELF(Executable and Linkable Format)是一种常见的可执行文件和可链接文件格式,主要用于Linux和类Unix系统。ELF 文件可以包含不同的类型,常见的 ELF 文件类型包括:
- 可执行文件(
ET_EXEC):这种类型的 ELF 文件是可直接执行的程序,可以在操作系统上运行。 - 共享目标文件(
ET_DYN):这种类型的 ELF 文件是可被动态链接的共享库,可以在运行时与其他程序动态链接。该类型文件后缀名为.so。 - 可重定位文件(
ET_REL):这种类型的 ELF 文件是编译器生成的目标文件,通常用于将多个目标文件链接到一个可执行文件或共享库中。该类型文件后缀名为.o,静态链接库(.a)也可以归为这一类。 - 核心转储文件(
ET_CORE):这种类型的 ELF 文件是操作系统在程序崩溃或发生错误时生成的核心转储文件,用于调试和分析程序崩溃的原因。
ELF 文件结构及相关常数被定义在 /usr/include/elf.h 里,因为 ELF 文件在各种平台下都通用,ELF文件有 32 位版本和 64 位版本。32 位版本与 64 位版本的 ELF 文件的格式基本是一样的(部分结构体为了优化对齐后大小调整了成员的顺序),只不过有些成员的大小不一样。
elf.h 使用 typedef 定义了一套自己的变量体系:
| 自定义类型 | 描述 | 原始类型 | 长度(字节) |
|---|---|---|---|
Elf32_Addr |
32 位版本程序地址 | uint32_t |
4 |
Elf32_Half |
32 位版本的无符号短整型 | uint16_t |
2 |
Elf32_Off |
32 位版本的偏移地址 | uint32_t |
4 |
Elf32_Sword |
32 位版本有符号整型 | uint32_t |
4 |
Elf32_Word |
32 位版本无符号整型 | int32_t |
4 |
Elf64_Addr |
64 位版本程序地址 | uint64_t |
8 |
Elf64_Half |
64 位版本的无符号短整型 | uint16_t |
2 |
Elf64_Off |
64 位版本的偏移地址 | uint64_t |
8 |
Elf64_Sword |
64 位版本有符号整型 | uint32_t |
4 |
Elf64_Word |
64 位版本无符号整型 | int32_t |
4 |
Elf64_Section |
64 位版本符号所在段(section)表的索引 | uint16_t |
2 |
Elf64_Xword |
64 位版本符号所占内存大小 | uint64_t |
8 |
ELF 主要管理结构为文件头,程序头表(可重定位文件没有)和节表,其他部分有一个个节组成,多个属性相同的节构成一个段。对于节的介绍这里按照静态链接相关和动态链接相关分别介绍。
文件头
我们这里以 32 位版本的文件头结构 Elf32_Ehdr 作为例子来描述,它的定义如下:
1 | /* The ELF file header. This appears at the start of every ELF file. */ |
sh_name:表示节的名称在字符串表中的索引。字符串表节存储了所有节的名称,sh_name指定了节的名称在字符串表中的位置。sh_type:表示节的类型,指定了节的用途和属性。常见的类型包括代码段(SHT_PROGBITS(1))、数据段(SHT_PROGBITS(1))、符号表(SHT_SYMTAB(2))、字符串表(SHT_STRTAB(3))等。sh_flags:表示节的标志,用于描述节的特性和属性。标志的具体含义取决于节的类型和上下文。sh_addr:表示节的虚拟地址,只在可执行文件中有意义。对于可执行文件,sh_addr指定了节在内存中的加载地址,如果该节不可被加载,则该值为 0 。sh_offset:表示节在文件中的偏移量,指定了节在文件中的位置。对于 bss 段来说该值没有意义。sh_size:表示节的大小,指定了节所占据的字节数。sh_link:表示链接到的其他节的索引,用于建立节之间的关联关系,具体含义依赖于节的类型。sh_info:附加信息,具体含义依赖于节的类型。sh_addralign:表示节的地址对齐要求,指定了节在内存中的对齐方式。即sh_addr需要满足 sh_addrmod 2sh_addralign=0sh_addrmod2sh_addralign=0 。如果sh_addralign为 0 或 1 表示该段没有对齐要求。sh_entsize:表示节中每个项的大小,如果该字段为 0 说明节中不包含固定大小的项。
ELF 当中常见的节如下:
.text:代码段(Code Section),用于存储程序的可执行指令。.rodata:只读数据段(Read-Only Data Section),用于存储只读的常量数据,例如字符串常量。.data:数据段(Data Section),用于存储已初始化的全局变量和静态变量。.bss:未初始化的数据段(Block Started by Symbol),用于存储未初始化的全局变量和静态变量。它不占用实际的文件空间,而是在运行时由系统自动初始化为零。.symtab:符号表节(Symbol Table Section),用于存储程序的符号表信息,包括函数、变量和其他符号的名称、类型和地址等。.strtab:字符串表节(String Table Section),用于存储字符串数据,如节名称、符号名称等。字符串表节被多个其他节引用,通过偏移量和索引来访问具体的字符串。.rel.text或.rela.text:代码重定位节(Relocation Section),用于存储代码段中的重定位信息,以便在链接时修正代码中的符号引用。.rel.data或.rela.data:数据重定位节(Relocation Section),用于存储数据段中的重定位信息,以便在链接时修正数据段中的符号引用。.dynamic:动态节(Dynamic Section),用于存储程序的动态链接信息,包括动态链接器需要的重定位表、共享对象的名称、版本信息等。.note:注释节(Note Section),用于存储与程序或库相关的注释或调试信息。
静态链接相关
符号表(.symtab):
注意:符号表除了静态链接外没有用,但是程序为了方便调试会保留符号表,我们可以通过 strip + 程序名 的方式将符号表去除,这就是为什么有的 pwn 题的附件没有函数和变量名而有的却有。
ELF 文件中的符号表往往是文件中的一个段,段名一般叫 .symtab 。符号表是一个 Elf*_Sym 结构(32 位 ELF 文件)的数组,每个 Elf*_Sym 结构对应一个符号。
1 | /* Symbol table entry. */ |
st_name:符号名称在字符串表中的偏移量。st_value:符号的值,即符号的地址或偏移量。- 如果该符号在目标文件中,如果是符号的定义并且该符号不是
COMMON块类型的则st_value表示该符号在段中的偏移。 - 在目标文件中,如果符号是
COMMON块类型的则st_value表示该符号的对齐属性。 - 在可执行文件中,
st_value表示符号的虚拟地址。
- 如果该符号在目标文件中,如果是符号的定义并且该符号不是
st_size:符号的大小,如果符号是一个函数,则表示函数的大小。如果该值为 0 表示符号的大小为 0 或未知。st_info:该字段是一个字节,包含符号的类型和绑定信息。符号类型包括函数、数据、对象等,符号绑定包括局部符号、全局符号、弱符号等。该字段的高 4 位表示符号的类型,低 4 位表示符号的绑定信息。st_other:保留字段,通常为 0 。st_shndx:通常为符号所在节的索引。- 如果符号是一个常量,该字段为
SHN_ABS(初始值不为 0 的全局变量) 或SHN_COMMON(初始值为 0 的全局变量)。 - 如果该符号未定义但是在该文件中被引用到,说明该符号可能定义在其他目标文件中,则该字段为
SHN_UNDEF。
- 如果符号是一个常量,该字段为
重定位表(.rel.text/.rel.data)
重定位表是一个 Elf*_Rel 结构的数组,每个数组元素对应一个重定位入口。重定位表主要有.rel.text 或 .rela.text,即代码重定位节(Relocation Section)和 .rel.data 或 .rela.data:数据重定位节(Relocation Section)。
1 | /* Relocation table entry without addend (in section of type SHT_REL). */ |
r_offset:需要进行重定位的位置的偏移量或地址。这个位置通常是指令中的某个操作数或数据的地址,需要在链接时进行修正,以便正确地引用目标符号。对于可执行文件或共享库,
r_offset表示需要修改的位置在内存中的位置(用于动态链接)。对于可重定位文件,
r_offset表示需要修改的位置相对于段起始位置的偏移(用于静态链接)。
r_info:低 8 位表示符号的重定位类型,重定位类型指定了进行何种类型的修正,例如绝对重定位、PC 相对重定位等。高 24 位表示该符号在符号表中的索引,用于解析重定位所引用的符号。
字符串表(.strtab)
ELF 文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。一种很常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。
通过这种方法,在ELF文件中引用字符串只须给出一个数字下标即可,不用考虑字符串长度的问题。一般字符串表在ELF文件中也以段的形式保存,常见的段名为“.strtab”或“.shstrtab”。这两个字符串表分别为字符串表(String Table)和段表字符串表(Section Header String Table)。顾名思义,字符串表用来保存普通的字符串,比如符号的名字;段表字符串表用来保存段表中用到的字符串,最常见的就是段名(sh_name )。
注意,在字符串表中的每个字符串的开头和结尾都有一个 \x00 填充。
例如:fake_dynstr = '\x00libc.so.6\x00_IO_stdin_used\x00stdin\x00strlen\x00read\x00stdout\x00setbuf\x00__libc_start_main\x00system\x00'
动态链接相关:
.interp 段
在动态链接的 ELF 可执行文件中,有一个专门的段叫做 .interp 段“interp”是“interpreter”解释器的缩写
.interp 的内容很简单,里面保存的就是一个字符串 /lib64/ld-linux-x86-64.so.2 ,这个字符串就是可执行文件所需要的动态链接器的路径。
通常系统通过判断一个 ELF 程序是否有 .interp 来判断该 ELF 文件是否为动态链接程序。
.dynamic 段
动态链接 ELF 中最重要的结构是 .dynamic 段,这个段里面保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。.dynamic 段是由Elf*_Dyn 构成的结构体数组。
1 | /* Dynamic section entry. */ |
Elf32_Dyn 结构由一个类型值加上一个附加的数值或指针,对于不同的类型,后面附加的数值或者指针有着不同的含义。我们这里列举几个比较常见的类型值(这些值都是定义在 elf.h 里面的宏),
DT_SYMTAB:指定了符号表的地址,d_ptr表示.dynsym的地址。DT_STRTAB:指定了字符串表的地址,d_ptr表示.synstr的地址。DT_STRSZ:指定了字符串表的大小,d_val表示大小。DT_HASH:指定了符号哈希表的地址,用于加快符号查找的速度,d_ptr表示.hash的地址。DT_SONAME:指定了共享库的名称。DT_RPATH:指定了库搜索路径(已废弃,不推荐使用)。DT_INIT:指定了初始化函数的地址,动态链接器在加载可执行文件或共享库时会调用该函数。DT_FINI:指定了终止函数的地址,动态链接器在程序结束时会调用该函数。DT_NEEDED:指定了需要的共享库的名称。DT_REL/DT_RELA:指定了重定位表的地址。
动态符号表(.dynsym)
为了完成动态链接,最关键的还是所依赖的符号和相关文件的信息。我们知道在静态链接中,有一个专门的段叫做符号表 .symtab(Symbol Table),里面保存了所有关于该目标文件的符号的定义和引用。为了表示动态链接这些模块之间的符号导入导出关系,ELF 专门有一个叫做动态符号表(Dynamic Symbol Table)的段用来保存这些信息,这个段的段名通常叫做 .dynsym(Dynamic Symbol),同样也是由 Elf*_Sym 构成的结构体数组
与 .symtab 不同的是,.dynsym 只保存了与动态链接相关的符号,对于那些模块内部的符号,比如模块私有变量则不保存。很多时候动态链接的模块同时拥有 .dynsym 和 .symtab 两个表,.symtab 中往往保存了所有符号,包括 .dynsym 中的符号。
与 .symtab 类似,动态符号表也需要一些辅助的表,比如用于保存符号名的字符串表。静态链接时叫做符号字符串表 .strtab(String Table),在这里就是动态符号字符串表 .dynstr(Dynamic String Table);由于动态链接下,我们需要在程序运行时查找符号,为了加快符号的查找过程,往往还有辅助的符号哈希表.hash
动态链接重定位表(.rel.dyn/.rel.data)
共享对象需要重定位的主要原因是导入符号的存在。动态链接下,无论是可执行文件或共享对象,一旦它依赖于其他共享对象,也就是说有导入的符号时,那么它的代码或数据中就会有对于导入符号的引用。在编译时这些导入符号的地址未知,在静态链接中,这些未知的地址引用在最终链接时被修正。但是在动态链接中,导入符号的地址在运行时才确定,所以需要在运行时将这些导入符号的引用修正,即需要重定位。
共享对象的重定位与我们在前面“静态链接”中分析过的目标文件的重定位十分类似,唯一有区别的是目标文件的重定位是在静态链接时完成的,而共享对象的重定位是在装载时完成的。在静态链接中,目标文件里面包含有专门用于表示重定位信息的重定位表,比如 .rel.text 表示是代码段的重定位表,.rel.data 是数据段的重定位表。
动态链接的文件中,也有类似的重定位表分别叫做 .rel.dyn 和 .rel.plt ,它们分别相当于 .rel.data 和 .rel.text 。.rel.dyn 实际上是对数据引用的修正,它所修正的位置位于 .got 以及数据段;而 .rel.plt 是对函数引用的修正,它所修正的位置位于 .got.plt 。
PLT 表(.plt)
在未开启 FULL RELRO 的情况下 PLT 表的结构如下图所示, PLT 表在 .plt(有的还包括 .plt.got) 中。
PLT 表的形式如下所示:
其中 n 为函数 bar 在 GOT 表中的值的索引,bar@GOT 中初始值为 jmp *(bar@GOT) 指令的下一条指令,也就是说第一次调用 bar 函数的时候会继续执行跳转至 PLT0 进行 bar@GOT 的重定位并调用 bar 函数;第二次调用 bar 函数的时候由于 bar@GOT 已完成重定位因此会直接跳转至 bar 函数。
第一次调用 bar 函数(解析地址并调用)
当程序中第一次执行到调用 bar 函数的代码时,实际上是跳转到了 bar 函数在 PLT 表中的桩代码 bar@PLT。
jmp *(bar@GOT):程序首先尝试跳转到 GOT 表中为bar预留的位置。- 由于是第一次调用,动态链接器还没有解析过
bar的地址。此时bar@GOT里面存放的不是真实的函数地址,而是紧接着的下一条指令的地址(即push n所在的内存地址)。
- 由于是第一次调用,动态链接器还没有解析过
push n:因为上一步的跳转实际上只是顺延到了下一条指令,所以程序接着执行push n。- 这里的
n是bar函数在重定位表中的索引号)。把它压入栈中,是为了告诉接下来的动态链接器要解析第n号函数(也就是bar)
- 这里的
jmp PLT0:跳转到 PLT 表的头部PLT0。这里存放着调用动态链接器的公共代码。- 进入
PLT0:push *(GOT+8):把当前模块的信息(通常是指向link_map结构的指针)压入栈中。动态链接器需要这个信息来知道是哪个模块发出的解析请求。jmp *(GOT+16):跳转到GOT+16所指向的地址。这个地址存放的正是动态链接器的解析函数(如_dl_runtime_resolve)的入口。
- 重定位与执行:动态链接器根据栈里的
n和模块信息,找到bar函数真正的内存地址,将这个真地址覆盖写入到bar@GOT中,然后直接把控制权交给真正的bar函数去执行。disassemble main
这个下面也会再次说明
2. 第二次及以后调用 bar 函数(直接调用)
当程序第二次(或以后无数次)调用 bar 函数时,情况就完全不同了。
- 程序依然首先跳转到
bar@PLT。 jmp *(bar@GOT):程序再次读取bar@GOT里面的值并跳转。- 因为在第一次调用时,动态链接器已经把
bar函数的真实内存地址写进了bar@GOT。
- 因为在第一次调用时,动态链接器已经把
- 直接执行:程序直接飞跃到了真正的
bar函数内部去执行。后面的push n和jmp PLT0被直接跳过,再也不会被执行到了。
GOT 表(.got/.got.plt)
ELF 将 GOT 拆分成了两个表叫做 .got 和 .got.plt 。其中 .got 用来保存全局变量引用的地址,.got.plt 用来保存函数引用的地址,也就是说,所有对于外部函数的引用全部被分离出来放到了 .got.plt 中(当然有的 ELF 文件可能吧这两个表合并为一个 .got 表,结构等同于后面提到的 .got.plt)。另外 .got.plt 还有一个特殊的地方是它的前三项是有特殊意义的,分别含义如下:
第一项保存的是
.dynamic段的偏移(也有可能是.dynamic段的地址)。 0x08第二项是一个
link_map的结构体指针,里面保存着动态链接的一些相关信息,是重定位函数_dl_runtime_resolve的第一个参数。 0x10第三项保存的是
_dl_runtime_resolve的地址。 0x18之后就是 got 表的内容 0x20 +
延迟绑定流程梳理
其中在第一次调用 puts 函数时调用的 _dl_runtime_resolve 函数的具体实现为:
用第一个参数
link_map访问.dynamic,取出.dynstr,.dynsym,.rel.plt的指针。.rel.plt + 第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作rel。rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym。.dynstr + sym->st_name得出符号名字符串指针。在动态链接库查找这个函数的地址,并且把地址赋值给
*rel->r_offset,即 GOT 表。调用这个函数。
ret2dlresolve
相关结构
主要有 .dynamic 、.dynstr 、.dynsym 和 .rel.plt 四个重要的 section 。
结构及关系如下如图(以 32 位为例):
Dyn
1 | /* Dynamic section entry. */ |
Dyn 结构体用于描述动态链接时需要使用到的信息,其成员含义如下:
d_tag表示标记值,指明了该结构体的具体类型。比如,DT_NEEDED表示需要链接的库名,DT_PLTRELSZ表示 PLT 重定位表的大小等。d_un是一个联合体,用于存储不同类型的信息。具体含义取决于d_tag的值。如果
d_tag的值是一个整数类型,则用d_val存储它的值。如果
d_tag的值是一个指针类型,则用d_ptr存储它的值
| d_tag类型 | d_un定义 |
|---|---|
#define DT_STRTAB 5 |
动态链接字符串表的地址,d_ptr表示.dynstr的地址 (Address of string table) |
#define DT_SYMTAB 6 |
动态链接符号表的地址,d_ptr表示.dynsym的地址 (Address of symbol table) |
#define DT_JMPREL 23 |
动态链接重定位表的地址,d_ptr表示.rel.plt的地址 (Address of PLT relocs) |
#define DT_RELENT 19 |
单个重定位项的大小,d_val表示单个重定位项大小 (Size of one Rel reloc) |
#define DT_SYMENT 11 |
单个符号表项的大小,d_val表示单个符号表项大小 (Size of one symbol table entry) |
Sym
1 | /* Symbol table entry. */ |
Sym 结构体用于描述 ELF 文件中的符号(Symbol)信息,其成员含义如下:
st_name:指向一个存储符号名称的字符串表的索引,即字符串相对于字符串表起始地址的偏移。st_info:如果st_other** 为 0** 则设置成 0x12 即可。st_other:决定函数参数link_map参数是否有效。如果该值不为 0 则直接通过link_map中的信息计算出目标函数地址。否则需要调用_dl_lookup_symbol_x函数查询出新的link_map和sym来计算目标函数地址。st_value:符号地址相对于模块基址的偏移值。
Rel
1 | /* Relocation table entry without addend (in section of type SHT_REL). */ |
Rel 结构体用于描述重定位(Relocation)信息,其成员含义如下:
r_offset:加上传入的参数link_map->l_addr等于该函数对应 got 表地址。r_info:符号索引的低 8 位(32 位 ELF)或低 32 位(64 位 ELF)指示符号的类型这里设为 7 即可,高 24 位(32 位 ELF)或高 32 位(64 位 ELF)指示符号的索引即Sym构造的数组中的索引。
link_map_x86
1 | struct link_map |
link_map 是存储目标函数查询结果的一个结构体,我们主要关心 l_addr 和 l_info 两个成员即可。
l_addr:目标函数所在 lib 的基址。l_info:Dyn结构体指针,指向各种结构对应的Dyn。l_info[DT_STRTAB]:即l_info数组第 5 项,指向.dynstr对应的Dyn。l_info[DT_SYMTAB]:即l_info数组第 6 项,指向Sym对应的Dyn。l_info[DT_JMPREL]:即l_info数组第 23 项,指向Rel对应的Dyn。
1 | struct link_map { |
dynamic 中的地址对应着 link_map 中l_info 相应的指针,可从link_map 取到dynamic 结构中.rel.plt .dynsym .dynstr对应的指针,为后来程序的执行提供各个节的基地址
dl_runtime_resolve 函数
_dl_runtime_resolve 的核心函数位 _dl_fixup 函数,这里是为了避免 _dl_fixup 传参与目标函数传参干扰(_dl_runtime_resolve 函数通过栈传参然后转换成 _dl_fixup 的寄存器传参)以及调用目标函数才在 _dl_fixup 外面封装一个 _dl_runtime_resolve 函数。_dl_fixup 函数的定义如下:
1 | /* We use this macro to refer to ELF types independent of the native wordsize. |
1 |
|
需要注意的是 _dl_fixup 中会有如下判断,根据这个判断决定了重定位的策略。
1 | if (__builtin_expect(ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0) |
_dl_fixup 函数在计算出目标函数地址并更新 got 表之后会回到 _dl_runtime_resolve 函数,之后 _dl_runtime_resolve 函数会调用目标函数。
更新: 2026-04-28 13:41:42
原文: https://www.yuque.com/idcm/wnemg9/um60itlorlds12v8
