ELF文件-程序的执行与动态链接
ELF文件 程序的执行与动态链接 .dynamic section 是 ELF(Executable and Linkable Format)文件中非常重要的一部分,它存储了与动态链接相关的信息。它提供了必要的信息以便在程序加载时能够解析符号、加载共享库以及执行重定位等操作。 dynamic section 通常被动态链接器(如 ld.so )使用。
本文目录 18 个章节
ELF文件-程序的执行与动态链接
.dynamic section 是 ELF(Executable and Linkable Format)文件中非常重要的一部分,它存储了与动态链接相关的信息。它提供了必要的信息以便在程序加载时能够解析符号、加载共享库以及执行重定位等操作。dynamic section 通常被动态链接器(如 ld.so)使用。
在 ELF 文件中,.dynamic section 以一系列的条目(entries)组成,每个条目包含一个 Tag 和与该 Tag 对应的值。不同的 Tag 用于描述不同类型的动态链接信息。
Elf32_Dyn 结构(32-bit ELF 文件)
typedef struct {
uint32_t d_tag; // 标记,描述这个条目的类型
union {
uint32_t d_val; // 整数值(针对一些类型的 Tag,如 DT_HASH、DT_PLTGOT 等)
uint32_t d_ptr; // 指针值(针对一些类型的 Tag,如 DT_SYMTAB、DT_STRTAB 等)
} d_un;
} Elf32_Dyn;
Elf64_Dyn 结构(64-bit ELF 文件)
typedef struct {
uint64_t d_tag; // 标记,描述这个条目的类型
union {
uint64_t d_val; // 整数值(针对一些类型的 Tag,如 DT_HASH、DT_PLTGOT 等)
uint64_t d_ptr; // 指针值(针对一些类型的 Tag,如 DT_SYMTAB、DT_STRTAB 等)
} d_un;
} Elf64_Dyn;
在执行视图中,总是会存在一个类型为 PT_DYNAMIC 的 segment,这个 segment 就包含了 .dynamic section 的内容。
- DT_NEEDED
作用:表示程序或共享库依赖的共享库名称。
示例:
DT_NEEDED libexample.so - DT_SYMTAB
作用:指向符号表(
.symtab)的内存地址,符号表包含了程序中所有符号的信息,如函数和变量的名称及地址。 示例:DT_SYMTAB 0x12345678 - DT_STRTAB
作用:指向字符串表(
.strtab)的内存地址,字符串表包含了所有符号的名称。 示例:DT_STRTAB 0x12345678 - DT_RELA
作用:指向
.rela重定位表的地址,.rela重定位表包含了程序中需要重定位的条目(如地址修正)。 示例:DT_RELA 0x12345678 - DT_RELASZ
作用:表示
.rela重定位表的大小,单位是字节。 示例:DT_RELASZ 0x100 - DT_PLTGOT
作用:指向全局偏移表(GOT)的内存地址,GOT 用于存储程序的外部符号地址。
示例:
DT_PLTGOT 0x12345678 - DT_HASH
作用:指向符号哈希表的内存地址,哈希表用于高效地查找符号。
示例:
DT_HASH 0x12345678 - DT_PLTRELSZ
作用:表示 PLT 重定位表的大小,单位是字节。
示例:
DT_PLTRELSZ 0x80 - DT_PLTREL
作用:指定 PLT 重定位表的条目类型,通常为
REL或RELA。 示例:DT_PLTREL REL - DT_VERNEED
作用:指向版本需求表的地址,记录了程序或共享库对特定版本的依赖。
示例:
DT_VERNEED 0x2468 - DT_VERNEEDNUM
作用:表示版本需求表中条目的数量。
示例:
DT_VERNEEDNUM 1 - DT_INIT
作用:指向程序的初始化函数的地址,初始化函数会在程序启动时被调用。
示例:
DT_INIT 0x98765432 - DT_FINI
作用:指向程序的终结函数的地址,终结函数会在程序退出时被调用。
示例:
DT_FINI 0x87654321
动态链接器解析外部符号过程概述
动态链接是指程序在执行时(而非编译时)将程序需要的外部库和符号连接到程序中。
1. 加载 ELF 文件
操作系统加载 ELF 可执行文件, 根据 Program Header Table (PHT) 中的描述,确定将 ELF 文件中的不同部分(segment)加载到内存的位置。
对于动态链接的 ELF 文件,通常会加载以下段(segments):
- 代码段(
PT_LOAD类型):包含程序的可执行代码。 - 数据段(
PT_LOAD类型):包含程序的初始化数据。 - 动态段(
PT_DYNAMIC类型):包含.dynamicsection ,与动态链接相关的信息。
2. 定位 .dynamic section
ELF 文件中有一个 PT_DYNAMIC 类型的 segment,它指向 .dynamic section。动态链接器使用该 segment 来定位 .dynamic section 的内存位置。
.dynamic section主要包含:
- 符号表:
DT_SYMTAB指向动态符号表,其中列出了程序需要的所有外部符号。 - 共享库依赖:
DT_NEEDED指定了main_program依赖的共享库(此例中是libmath.so)。 - 重定位表:
DT_RELA指向重定位表,用于记录需要进行地址修改的地方。
3. 符号解析(Symbol Resolution)
根据 .dynamic section 中的符号信息,解析程序所需的外部符号,并将它们与共享库中的实际地址进行关联。
- 符号查找(Symbol Lookup):动态链接器会检查动态符号表中的每个符号,并尝试在 ELF 文件中列出的共享库中查找这些符号。如果找到匹配的符号(在共享库的符号表中),则将该符号的地址绑定到程序中对应的符号位置。
4. 共享库加载(Shared Library Loading)
在 ELF 文件的 .dynamic section 中,包含了所有依赖的共享库名称(通过 DT_NEEDED 类型条目)。动态链接器根据这些信息加载共享库:
- 查找共享库:动态链接器会根据
DT_NEEDED条目中的库名称查找共享库。如果库已经被加载到内存中,链接器会直接使用该库。如果库尚未加载,操作系统会根据配置(如环境变量LD_LIBRARY_PATH或系统默认路径)查找并加载该库。 - 加载共享库:一旦找到共享库,动态链接器将该库的代码和数据段加载到程序的虚拟内存中。
5. 重定位(Relocation)
在动态链接过程中,某些符号的地址是无法在编译时确定的。为了在运行时正确地解析这些符号,动态链接器会执行 重定位 操作。
- 重定位表:
DT_RELA指向重定位表,该表记录了需要进行地址修改的条目。动态链接器会检查所有需要重定位的条目,并修改它们的地址,使其指向正确的符号(如add函数)。
在 main_program 中,第一次调用 add 函数时,PLT 会触发动态链接器查找该函数的实际地址。重定位表(RELA)将会更新 GOT 中的相关条目,确保后续的调用直接通过正确的地址跳转。
6. 符号绑定(Symbol Binding)
符号绑定是指在链接时将符号的地址与实际内存地址进行绑定。动态链接器将根据 .dynamic section 中的符号信息,将外部符号绑定到正确的库或内存位置。
动态链接器通常使用 全局符号表(global symbol table)来管理符号。每个符号可能通过不同的规则进行绑定:
- 动态符号绑定:通过
.dynamicsection 中的符号条目与共享库进行绑定。 - 符号查找与绑定:通过符号的名字、地址、符号表等信息,动态链接器将程序中需要的外部符号与库中的符号进行匹配,并更新程序中的地址。
7. PLT 和 GOT(Procedure Linkage Table 和 Global Offset Table)
动态链接器在符号解析和重定位过程中,可能会涉及到 PLT(Procedure Linkage Table) 和 GOT(Global Offset Table)。
PLT 表
PLT:PLT 是一组用来实现函数调用的跳转表,它通常用于动态链接库中的函数调用。调用一个函数时,程序会跳转到 PLT 中的入口,PLT 会进一步跳转到实际的函数地址。最初,PLT 会通过动态链接器来解析函数地址。
PLT 条目:在程序中,每个外部函数调用都会对应一个 PLT 条目。PLT 是一种延迟绑定的机制。最初,PLT 会跳转到一个 stub,该 stub 会通过 GOT 查找符号地址,然后跳转到实际的函数实现。
第一次调用外部函数:
- 程序第一次调用外部函数时,跳转到 PLT 表中的入口。PLT 会跳转到 GOT 表中的一个条目。
- 如果这是函数的第一次调用,程序会跳转到动态链接器(
ld-linux.so)动态链接器会根据 RELA 表的信息进行重定位(RELA 表记录了需要重定位的地址信息),并将解析出来的地址填入 GOT 中,下一次调用该函数时就可以直接跳转到正确的地址。
0000000000000600 <.plt>:
600: ff 35 b2 09 20 00 pushq 0x2009b2(%rip) # 200fb8 <_GLOBAL_OFFSET_TABLE_+0x8>
606: ff 25 b4 09 20 00 jmpq *0x2009b4(%rip) # 200fc0 <_GLOBAL_OFFSET_TABLE_+0x10>
60c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000000610 <add@plt>:
610: ff 25 b2 09 20 00 jmpq *0x2009b2(%rip) # 200fc8 <add>
616: 68 00 00 00 00 pushq $0x0
61b: e9 e0 ff ff ff jmpq 600 <.plt>
000000000000074a <main>:
775: e8 a6 fe ff ff callq 620
GOT 表
GOT 是一个用于存储动态链接符号地址的表。它的作用是保存外部符号(如共享库中的函数地址)的偏移量,供程序在执行时使用。 在 PLT 第一次调用外部函数时,GOT 中存储的地址是指向动态链接器的代码,动态链接器解析完符号地址后,会将解析结果写入 GOT。
8. 程序执行
一旦符号解析、重定位、共享库加载等操作完成,程序就可以正常执行,所有符号的地址都已经正确绑定,并且程序的外部依赖(共享库)也已经加载。
动态链接器解析外部符的一种实现
官方描述
Following the steps below, the dynamic linker and the program cooperate to resolve symbolic references through the procedure linkage table and the global offset table.


- When first creating the memory image of the program, the dynamic linker sets the second and the third entries in the global offset table to special values. Steps below explain more about these values.
- Each shared object file in the process image has its own procedure linkage table, and control transfers to a procedure linkage table entry only from within the same object file.
- For illustration, assume the program calls name1, which transfers control to the label .PLT1.
- The first instruction jumps to the address in the global offset table entry for name1. Initially the global offset table holds the address of the following pushl instruction, not the real address of name1.
- Now the program pushes a relocation index (index) on the stack. The relocation index is a 32-bit, non-negative index into the relocation table addressed by the DT_JMPREL dynamic section entry. The designated relocation entry will have type R_X86_64_JUMP_SLOT, and its offset will specify the global offset table entry used in the previous jmp instruction. The relocation entry contains a symbol table index that will reference the appropriate symbol, name1 in the example.
- After pushing the relocation index, the program then jumps to .PLT0, the first entry in the procedure linkage table. The pushl instruction places the value of the second global offset table entry (GOT+8) on the stack, thus giving the dynamic linker one word of identifying information. The program then jumps to the address in the third global offset table entry (GOT+16), which transfers control to the dynamic linker.
- When the dynamic linker receives control, it unwinds the stack, looks at the designated relocation entry, finds the symbol's value, stores the ``real'' address for name1 in its global offset table entry, and transfers control to the desired destination.
- Subsequent executions of the procedure linkage table entry will transfer directly to name1, without calling the dynamic linker a second time. That is, the jmp instruction at .PLT1 will transfer to name1, instead of ``falling through'' to the pushl instruction.
实现案例
程序
// main_program.c
#include <stdio.h>
extern int add(int, int); // 声明外部函数
int main() {
int result = add(3, 5); // 调用共享库中的函数
printf("Result: %d\n", result);
return 0;
}
// libadd.c
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
过程
在首次创建程序的内存映像时,动态链接器将全局偏移表(GOT)中的第二和第三个条目设置为特殊值。
进程映像中的每个共享对象文件都有其自己的过程链接表(PLT)
Disassembly of section .plt:
0000000000000600 <.plt>:
600: ff 35 b2 09 20 00 pushq 0x2009b2(%rip) # 200fb8 <_GLOBAL_OFFSET_TABLE_+0x8>
606: ff 25 b4 09 20 00 jmpq *0x2009b4(%rip) # 200fc0 <_GLOBAL_OFFSET_TABLE_+0x10>
60c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000000610 <add@plt>:
610: ff 25 b2 09 20 00 jmpq *0x2009b2(%rip) # 200fc8 <add>
616: 68 00 00 00 00 pushq $0x0
61b: e9 e0 ff ff ff jmpq 600 <.plt>
0000000000000620 :
620: ff 25 aa 09 20 00 jmpq *0x2009aa(%rip) # 200fd0
626: 68 01 00 00 00 pushq $0x1
62b: e9 d0 ff ff ff jmpq 600 <.plt>
从
600: ff 35 b2 09 20 00 pushq 0x2009b2(%rip) # 200fb8 <_GLOBAL_OFFSET_TABLE_+0x8>可得出: GOT的第二个条目GOT[1]为200fb8 <_GLOBAL_OFFSET_TABLE_+0x8>
- main函数调用
add,该调用将控制流转移到标签<add@plt>。
000000000000074a <main>:
74a: 55 push %rbp
74b: 48 89 e5 mov %rsp,%rbp
74e: 48 83 ec 10 sub $0x10,%rsp
752: be 05 00 00 00 mov $0x5,%esi
757: bf 03 00 00 00 mov $0x3,%edi
75c: e8 af fe ff ff callq 610 <add@plt>
761: 89 45 fc mov %eax,-0x4(%rbp)
764: 8b 45 fc mov -0x4(%rbp),%eax
767: 89 c6 mov %eax,%esi
769: 48 8d 3d a4 00 00 00 lea 0xa4(%rip),%rdi # 814 <_IO_stdin_used+0x4>
770: b8 00 00 00 00 mov $0x0,%eax
775: e8 a6 fe ff ff callq 620
77a: b8 00 00 00 00 mov $0x0,%eax
77f: c9 leaveq
780: c3 retq
781: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
788: 00 00 00
78b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
- 第一个指令跳转到全局偏移表(GOT)中
add对应条目的地址。610: ff 25 b2 09 20 00 jmpq *0x2009b2(%rip) # 200fc8 <add>
当前指令寄存器的值0x616与0x2009b2相加会得到0x200fc8 , 这个是add函数在GOT表动态链接符号地址的存储位置. 最初,全局偏移表中保存的是随后的 pushl 指令的地址 616,而不是 add 的实际地址。
pushq $0x0 将重定位索引(index)推入栈中。
Relocation表内容:
Relocation section '.rela.plt' at offset 0x5b8 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000200fc8 000200000007 R_X86_64_JUMP_SLO 0000000000000000 add + 0
000000200fd0 000300000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
另外一种查看重定位表的方式:
objdump -R main_program
main_program: 文件格式 elf64-x86-64
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000200da0 R_X86_64_RELATIVE *ABS*+0x0000000000000740
0000000000200da8 R_X86_64_RELATIVE *ABS*+0x0000000000000700
0000000000201008 R_X86_64_RELATIVE *ABS*+0x0000000000201008
0000000000200fd8 R_X86_64_GLOB_DAT _ITM_deregisterTMCloneTable
0000000000200fe0 R_X86_64_GLOB_DAT __libc_start_main@GLIBC_2.2.5
0000000000200fe8 R_X86_64_GLOB_DAT __gmon_start__
0000000000200ff0 R_X86_64_GLOB_DAT _ITM_registerTMCloneTable
0000000000200ff8 R_X86_64_GLOB_DAT __cxa_finalize@GLIBC_2.2.5
0000000000200fc8 R_X86_64_JUMP_SLOT add
0000000000200fd0 R_X86_64_JUMP_SLOT printf@GLIBC_2.2.5
重定位索引是一个 32 位的非负数,它指向由 DT_JMPREL 动态段条目指定的重定位表。
指定的重定位条目的类型为 R_X86_64_JUMP_SLOT,其偏移量 000000200fc8 指定了在前一个 jmp 指令中使用的全局偏移表条目。也就是说, GOT表add对应的跳转地址在 200fc8位置, 这个值在重定位表中的offset值来表示.
重定位条目包含一个符号表索引, 即Info项的前半部分 2 ,用于引用相应的符号,在本例中是 add, 可以通过该索引找到该函数在动态符号表中的位置:
Symbol table '.dynsym' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND add
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
6: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
7: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
8: 0000000000201010 0 NOTYPE GLOBAL DEFAULT 23 _edata
9: 0000000000201018 0 NOTYPE GLOBAL DEFAULT 24 _end
10: 0000000000201010 0 NOTYPE GLOBAL DEFAULT 24 __bss_start
11: 00000000000005e8 0 FUNC GLOBAL DEFAULT 11 _init
12: 0000000000000804 0 FUNC GLOBAL DEFAULT 15 _fini
推入重定位索引后,程序跳转到 .PLT0,即过程链接表中的第一个条目 600 <.plt> 。pushl 指令将第二个全局偏移表条目(GOT+8)的值压入栈中,从而为动态链接器提供一个标识信息。然后,程序跳转到第三个全局偏移表条目(GOT+16)中的地址,这将控制流转交给动态链接器。
0000000000000600 <.plt>:
600: ff 35 b2 09 20 00 pushq 0x2009b2(%rip) # 200fb8 <_GLOBAL_OFFSET_TABLE_+0x8>
606: ff 25 b4 09 20 00 jmpq *0x2009b4(%rip) # 200fc0 <_GLOBAL_OFFSET_TABLE_+0x10>
60c: 0f 1f 40 00 nopl 0x0(%rax)
当动态链接器接管控制时,它会展开栈,查看指定的重定位条目,找到符号的值,将 add 的“真实”地址存储到其全局偏移表条目中,并将控制流转移到目标地址。
此后, 如果再次调用add, 将直接通过<add@plt>的jmp 指令跳转到GOT中存储的 add的地址,不需要再次经过 <.plt>调用动态链接器。
程序执行过程
当程序开始执行时,首先会加载所需的动态库,并在程序的启动过程中,执行动态链接器的解析工作。具体过程如下:
- 初始化阶段:
- 程序启动时,
_init函数会首先执行。根据你提供的汇编,_init中会检查某个值并进行调用(如__gmon_start__),这通常是与程序初始化或者分析工具(如 gmon)的关联。
- 程序启动时,
- 函数调用与动态链接:
- 在程序的某些函数调用中(如
add函数),可能涉及到外部符号的调用。对于这些符号,程序会通过 PLT 表项来跳转到动态链接器,由动态链接器解析函数的地址并填充 GOT 表。
- 在程序的某些函数调用中(如
- 解析外部符号:
- 程序第一次调用外部函数时,PLT 会通过跳转到动态链接器来解析该函数的地址,动态链接器会根据 RELA 表的信息进行重定位,并将解析出来的地址填入 GOT 中。此后,程序会直接通过 GOT 表访问已解析的函数地址,从而避免了重复解析。
- 程序结束与清理:
- 在程序结束时,
_fini函数会被调用进行清理工作。__cxa_finalize函数通常会在这个阶段调用,执行一些资源释放工作。
- 在程序结束时,
Thanks
ChatGPT https://github.com/iqiyi/xHook/blob/master/docs/overview/android_plt_hook_overview.zh-CN.md https://www.ucw.cz/~hubicka/papers/abi/node22.html