基本名词
首先给出一些基本的名词概念,在后续章节会解释这些名词。
ELF文件
ELF(Executable and Linkable Format) 文件,看名字意为可执行/可链接格式的文件。
它有三种类型:
- 可执行文件(Executable File):可直接被操作系统加载和执行。
- 可重定位文件(Relocatable File):编译器生成的代码和数据,包含程序代码和数据等,用于被连接器链接。
- 共享对象文件(Shared Object File):动态链接库或位置无关可执行文件。
链接
程序的链接是指将程序中未解析的符号对象做解析和填充的过程。
我们在实现程序逻辑时,往往希望将程序功能进行模块化拆分,而最终的程序由多个模块“拼接”而成,这个程序的“拼接”过程我们就称之为程序的链接。而程序的链接又分为动态链接和静态链接两种方式:
- 静态链接:在编译阶段将程序中未解析的符号搁置,输出可重定位 (Relocatable File ) 的目标文件。在链接阶段,静态链接器( Static Linker )解析和搜索所有输入的可重定位文件,处理其中所有搁置的未解析符号。最终输出一个完全链接的可执行文件 (Executable File) 。静态链接输出的可执行文件在运行时是与链接阶段输入的可重定位文件完全无关的。
- 动态链接:在编译阶段将程序中未解析的符号搁置,并且记录程序所依赖的共享库( Shared Object File )(共享库中必须定义和实现了程序中未解析的符号),输出一个动态链接的可执行文件。其中未解析的符号会在运行时由动态链接器( Dynamic Linker )将依赖的共享库映射到进程的地址空间,并处理程序中所有未解析的符号。
加载
程序的加载是指操作系统创建或扩充进程镜像的过程。
程序文件可以看作是一串二进制数据(包括程序指令序列、程序数据及各类描述信息),这串二进制数据是静态存储在存储介质(磁盘)上的。
而程序的加载指的是操作系统创建出一个进程,并为这个进程分配CPU时间片和虚拟内存并将静态的程序文件以某种方式拷贝(映射)到进程的虚拟内存的过程。
静态分析—链接
从前一章节的名词解释中,我们可以大体上了解:程序的链接是为了解决程序解耦合或者说模块化的需求。
代码示例
那么我们先引入一个简单的例子来构造这种场景。
这是我们的程序入口 calc.c。它需要依赖外部的 数据 int extern_init_data 和 函数 int add(int a, int b)。 它本身代码空间中也有 已初始化数据 int* init_data_ptr,int init_data ,和 未初始化数据 int not_init_data :
因为希望尽可能避免其它的库依赖,代码中不使用任何c库,包括glibc。所以我们手写入口函数_start,并使用内联汇编触发退出中断,程序以123退出码退出。
1 | |
接下来,在模块libcalc.c中实现主程序中需要的数据和函数定义:
1 | |
前期的代码准备已经完成,我们预期将两个模块化的文件合并关联起来,使 calc.c 可以正确调用libcalc.c中的数据和代码。
静态链接
首先很直观的可以想到在完成多个模块的最终合并时,可以将子模块中的数据和代码直接拷贝到主模块中,让主模块看起来像是直接从一个 “All In One” 单个源代码文件编译而来的。而这,就是我们所说的静态链接。
先将两个C源代码文件编译为可重定位ELF文件:
1 | |
要完成多个可重定位文件的合并,静态链接往往要经历如下三个步骤:
graph TD
B(module_a.c) --> D[compilation]
C(main.c) --> E[compilation]
D --> F[Relocatable Object File: module_a.o]
E --> G[Relocatable Object File: main.o]
F --> H[linking]
G --> H
H --> I[Address Allocation]
I --> J[Symbol Resolution]
J --> K[Relocation]
K --> L[Static executable File: main]
!!!现代静态链接器对可重定位文件的链接过程做了高度优化,往往在一次遍历中完成这三步。
为了理解其中原理性的过程,我们将这一过程拆分,将文件先部分链接,再完成全链接。(并非真实的)
1 | |
那么这部分链接和全链接这两个步骤中,静态链接器都做了哪些工作?
地址与空间分配(Address Allocation)
- 扫描多个输入文件的节(section),获取各节的地址、长度和属性等信息,并计算出合并后可执行文件的各节长度及位置;
- 将多个目标文件的 相同节合并,使其位于同一地址空间;
- 维护一个 全局符号表,用于在合并后的文件地址空间中进行符号寻址;
- 将 相似节合并 生成 段 结构。(在部分链接时,不会将节合并为段)
block-beta
columns 5
elfhdr1["ET_REL ELF Header"]
space:1
elfhdr2["ET_EXEC ELF Header"]
space:1
elfhdr3["ET_REL ELF Header"]
block:text_group:1
columns 1
text[".text
代码节"]
rel_text[".rel.text
代码重定位表"]
rodata[".rodata
只读数据节"]
end
space:1
block:load1:1
columns 1
placeholder["LOAD Segment #1
R+X"]
text_section[".text"]
plt_section[".plt"]
rodata_section[".rodata"]
end
space:1
block:text_group2:1
columns 1
text2[".text
代码节"]
rel_text2[".rel.text
代码重定位表"]
rodata2[".rodata
只读数据节"]
end
text -- "combines" --> text_section
text2 -- "combines" --> text_section
rodata -- "combines" --> rodata_section
rodata2 -- "combines" --> rodata_section
block:data_group:1
columns 1
data[".data
已初始化数据节"]
bss[".bss
未初始化数据节"]
rel_data[".rel.data
数据重定位表"]
end
space:1
block:load2:1
columns 1
placeholder3["LOAD Segment #2
RW"]
init_array_section[".init_array"]
fini_array_section[".fini_array"]
data_seg[".data"]
bss_seg[".bss"]
got_section[".got"]
end
space:1
block:data_group2:1
columns 1
data2[".data
已初始化数据节"]
bss2[".bss
未初始化数据节"]
rel_data2[".rel.data
数据重定位表"]
end
data -- "combines" --> data_seg
data2 -- "combines" --> data_seg
bss -- "combines" --> bss_seg
bss2 -- "combines" --> bss_seg
block:table_group:1
columns 1
symtab[".symtab
符号表"]
strtab[".strtab
字符串表"]
end
space:1
block:pht_group:1
pht["Program Header
程序头表"]
end
space:1
block:table_group2:1
columns 1
symtab2[".symtab
符号表"]
strtab2[".strtab
字符串表"]
end
block:sht_group:1
sht["Section Header
节头表"]
end
space:3
block:sht_group2:1
sht2["Section Header
节头表"]
end
classDef headerStyle fill:#e3f2fd,stroke:#0277bd,stroke-width:3px
classDef execStyle fill:#e8f5e8,stroke:#388e3c,stroke-width:2px
classDef dataStyle fill:#fff3e0,stroke:#f57c00,stroke-width:2px
classDef metaStyle fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
classDef tableStyle fill:#fce4ec,stroke:#c2185b,stroke-width:3px
classDef titleStyle fill:#f8f9fa,stroke:#343a40,stroke-width:2px
classDef segmentStyle fill:#e1f5fe,stroke:#01579b
classDef noteSectionStyle fill:#80808030
classDef optionalStyle stroke-dasharray: 3 3
classDef placeholderStyle fill:#00000000,stroke:#00000000
classDef sectionGroupStyle fill:#00000000,stroke:#00000000
class placeholder,placeholder2,placeholder3,placeholder4 placeholderStyle
class linktitle,linktitle2,runtitle titleStyle
class elfhdr1,elfhdr2,elfhdr3 headerStyle
class text,text2,text_section,init_section,plt_section,fini_section execStyle
class rodata,rodata2,rodata_seg,rodata_section,data,data2,data_seg,bss,bss2,bss_seg,init_array_section,fini_array_section,got_section dataStyle
class rel_text,rel_text2,rel_data,rel_data2,symtab,symtab2,strtab,strtab2 metaStyle
class load1,load2,dynamic_seg,note_seg segmentStyle
class sht,sht2,pht tableStyle
class note_gnu_property,note_gnu_buildid,note_others,note_abi_tag noteSectionStyle
class rel_header_group,rel_header_group2,dyn_header_group,text_group,text_group2,data_group,data_group2,table_group,table_group2,sht_group,sht_group2,pht_group sectionGroupStyle
class plt_section,rodata_section,rel_text2,rel_data2,got_section,init_array_section,fini_array_section,note_gnu_property,note_gnu_buildid,note_others,note_abi_tag optionalStyle
这里我们用上面的示例来理解一下这些概念,先查看文件的节信息:
1 | |
可以看到部分链接的文件中,分别只有一个.text,.data,.bss节,输入的可重定位文件对应节被合并了。
符号搜索与解析(Symbol Resolution)
输入的可重定位的目标文件中包含编译阶段搁置的未解析符号,那么符号被搁置后,在程序中是如何存储的呢?
1 | |
我们将部分链接的代码反汇编后观察到,对于被搁置的符号,程序会将其地址偏移值置为0(相对于 rip 指针,即下一条指令的地址),因为calc.o中未定义这些符号,无法设置正确的偏移值。
所以,为了在运行时能够正确地完成数据符号的寻址以及函数跳转,我们必须先找到程序中所有被搁置的符号,然后搜索该符号的真实地址。
静态链接器通过查表来完成这两步:
- 解析输入目标文件中的.rel.text(代码重定位表)、.rel.data(数据重定位表),获取当前待解决的未解析符号;
1 | |
根据重定位表,我们需要解决以下符号的重定位:
| 符号名称 | 重定位类型 | 符号表索引 |
|---|---|---|
| not_init_data | 32位PC相对重定位 | 0x10 |
| init_data_ptr | 32位PC相对重定位 | 0xb |
| add | 32位PLT间接重定位 | 0xd |
| init_data | 32位PC相对重定位 | 0xc |
| extern_init_data | 64位直接重定位 | 0xf |
解析出搁置符号后,我们在merged.o的.symtab(符号表)中检索这些符号的信息:
1
2
3
4
5
6
7
8
9
10
11
12> readelf -s merged.o
Symbol table '.symtab' contains 17 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
......
11: 0000000000000000 8 OBJECT GLOBAL DEFAULT 7 init_data_ptr
12: 0000000000000000 4 OBJECT GLOBAL DEFAULT 6 init_data
13: 000000000000004d 24 FUNC GLOBAL DEFAULT 2 add
14: 0000000000000000 77 FUNC GLOBAL DEFAULT 2 _start
15: 0000000000000004 4 OBJECT GLOBAL DEFAULT 6 extern_init_data
16: 0000000000000000 4 OBJECT GLOBAL DEFAULT 9 not_init_data注:Ndx指该符号所在节的索引号,例如calc.o的 2号节是.txt节,6号节是.data节;Value指该符号在其所在节的节内偏移。
根据解析符号表的结果,获取到对应的符号表项,我们就可以扩充重定位表解析的表格了:
| 符号名称 | 重定位类型 | 符号表索引 | Type | Section | Offset in Section |
|---|---|---|---|---|---|
| not_init_data | 32位PC相对重定位 | 0x10 | OBJECT | .bss | 0x0 |
| init_data_ptr | 32位PC相对重定位 | 0xb | OBJECT | .data.rel | 0x0 |
| add | 32位PLT间接重定位 | 0xd | FUNC | .text | 0x4d |
| init_data | 32位PC相对重定位 | 0xc | OBJECT | .data | 0x0 |
| extern_init_data | 64位直接重定位 | 0xf | OBJECT | .data | 0x4 |
重定位(Relocation)
现在只差临门一脚,链接器在 符号搜索与解析 阶段解析出了所有搁置的符号,结合在 地址与空间分配 阶段合并而来的全局符号表,一次遍历可以将这些搁置符号设置正确的地址(R_X86_64_PC32,R_X86_64_PLT32 或 R_X86_64_64)。
至此,当前ELF文件中的代码和数据等信息均已完整,段结构也被正确初始化,规划分配了适用于运行时的虚拟地址空间。该文件可以被加载到进程并被执行。
动态链接
上述的静态链接方案看起来已经解决了代码模块化拆分的问题。但在很多场景中,这个方案并不完美。
设想,有一个封装了底层API的公共库A(例如glibc),下游成千上万的工程代码均需要依赖A:
- 那么所有下游代码在发布时,都需要将公共库A编译并链接到自己的ELF文件中;
- 当公共库A版本迭代时,所有的下游代码都需要重新编译A,并静态链接。
这简直就是灾难,我们的设备上将存储无数份公共库A的副本,并且每次公共库A的版本迭代都将导致我们设备上所有软件的更新。
所以这驱动我们探索一个新方案,能够让所有依赖公共库A的下游软件在设备上共享同一份A的代码,并且能够在运行时链接A。这样一来,上面的问题就迎刃而解。
将这个问题拆解:
首先,所有下游软件共享同一份公共库A的代码,那么A的发布形式必须满足A模块内部不能够使用绝对地址寻址,因为无法预测下游软件会将A的代码放在哪段地址空间。所以为了能够保证A模块内部的指令跳转和数据寻址逻辑正确,则A必须全部使用间接寻址(指令指针寄存器寻址),这就是 地址无关代码 。在linux中,我们一般将源代码编译为 Shared Object File(共享对象文件,后缀为 .so) 来实现这一目的;
其次,模块A在运行时被链接到另一个进程,这个过程由于要处理目标文件的搁置符号,并完成符号的重定位,我们一定需要一个链接器,这个链接器在运行时将把模块A的代码和数据放到目标文件的进程内存空间中,并完成符号解析和重定位。这个链接器一般是 ld-.so** ,也称之为解释器(后文称之为解释器)。而将模块A加载到目标文件的进程空间我们需要依赖 mmap 系统调用。
linux的系统调用中,有一个常用的接口:
void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset)。mmap用于将一个linux对象映射到用户空间内存中。我们在用户空间对该段内存映射的所有操作都会反映到映射的linux对象上。
我们沿用上面的示例来完成这一过程:
1 | |
在示例中,我们在运行前做了一次静态链接。但其实这次静态链接与前文的完全静态链接有所差异:
graph LR
A[module_a.c] --> B["compilation"]
C[main.c] --> D["compilation"]
B --> E["Shared Object File: libmodule_a.so"]
D --> F["Relocatable Object File: main.o"]
E --> G["Symbol Resolution"]
G --> G3["Mark Extern Symbol"]
G3 --> G1[".dynsym"]
G --> G4["Combine Global Symbol Table"]
G4 --> G2[".symtab"]
F --> G
E --> I["Relocation Table Generation"]
I --> I3["Data Relocation Section Generation"]
I3 --> I1[".rela.dyn"]
I --> I4["Text Relocation Section Generation"]
I4 --> I2[".rela.plt"]
E --> H["Dependencies Collection"]
H --> H1["Add DT_NEEDED: libmodule_a.so"]
E --> J["Dynamic Section Generation"]
J --> J1[".dynamic"]
H1 --> J1
E --> L["Inject Interpreter"]
L --> L1[".interp"]
G1 --> K["Dynamic Linked Executable File: main"]
G2 --> K
I1 --> K
I2 --> K
J1 --> K
L1 --> K
classDef compile fill:#e6f7ff,stroke:#1890ff;
classDef link fill:#ffe58f,stroke:#faad14;
classDef section fill:#ffd591,stroke:#fa541c,stroke-width:2px;
classDef file fill:#ffa0a0,stroke:#ff2020,stroke-width:2px;
class B,D compile;
class A,C,E,F,K file;
class G,G1,G2,G3,G4,H,H1,H2,I,I1,I2,I3,I4,J,J1,J2,L link;
class G1,G2,J1,I1,I2,L1 section;
在这一链接过程中,链接器事实上 不拷贝代码 ,不重定位符号 。仅将目标文件依赖的模块信息记录,解析所有输入文件,维护一个全局符号表,并标记所有当前搁置的符号。同时注入 解释器 路径。
链接阶段—符号解析
在上图中可以看到符号解析步骤,通过标记搁置符号输出了 .dynsym 节,合并输出了全局符号表 .symtab 。
具象化到上面例子中:
1 | |
链接阶段—重定位表生成
这里的重定位表将函数和数据对象分成了两个表来记录:
1 | |
链接阶段—依赖和解释器信息注入
由于不进行代码拷贝和重定向,在ELF文件中一定是需要一个数据结构来记录这些未重定位符号需要在哪个依赖文件中搜索的,这个结构就是动态节:
1 | |
再进一步去探索这个动态节的数据结构:
1 | |
事实上,这是一个非常规整的表结构,每个表项为16个字节,其中前8字节为该项的标记,后8字节为该项的值,以第一项举例:
1 | |
该项标记为 0x0000000000000001(DT_NEED),表示这项记录一个当前ELF文件的依赖项。该项值为0x0000000000000016,表示该依赖的文件名在字符串表中的偏移值为0x16:
1 | |
从字符串表的基地址0x3e8偏移0x16,可以在0x3fe寻址该字符串 6c 69 62 63 61 6c 63 2e 73 6f(libcalc.so)。
最后,在运行时加载该ELF文件前,进程需要先加载解释器。因此解释器的路径也需要被保存在ELF文件的某节中,即 .interp:
1 | |
至此,一个动态链接ELF文件完成了链接阶段的所有工作。可以明显感觉到,动态链接文件的链接阶段要比静态链接简单得多,因为重定位工作被推迟到了 加载/执行 阶段。
动态分析—加载/执行
stateDiagram-v2
direction TB
[*] --> KernelSpace
state "🔴 KERNEL SPACE" as KernelSpace {
direction TB
[*] --> execve
execve --> load_elf_binary
state "load_elf_phdrs" as load_elf_phdrs
load_elf_binary --> load_elf_phdrs
state elf_load {
direction LR
state "mmap Segments" as mmap_segments
[*] --> mmap_segments
mmap_segments --> mprotect
mprotect --> [*]
}
load_elf_phdrs --> elf_load
elf_load --> elf_load
state "Check Interp Exist" as check_interp
elf_load --> check_interp
state is_interp_exist <>
check_interp --> is_interp_exist
is_interp_exist --> set_elf_entry_static: interp non-exist
is_interp_exist --> load_elf_interp: interp exist
state "Set elf_entry to _start" as set_elf_entry_static
state "load_elf_interp
mmap interp(ld.so)" as load_elf_interp
state "Set elf_entry to dl_main" as set_elf_entry_dynamic
load_elf_interp --> set_elf_entry_dynamic
set_elf_entry_static --> start_thread
set_elf_entry_dynamic --> start_thread
start_thread --> [*]
}
KernelSpace --> UserSpace: context switch
state "🟢 USER SPACE" as UserSpace {
direction TB
[*] --> entry_point
state "_start" as static_start
state "_start" as dynamic_start
state entry_point_fork <>
entry_point --> entry_point_fork
entry_point_fork --> static_start
entry_point_fork --> dl_main
dl_main --> dynamic_linking
dynamic_linking --> dynamic_start
state dynamic_linking {
direction LR
state "Resolve and Load Dependencies" as resolve_and_load_dependencies
state "Symbols Resolve" as symbols_resolve
state "Symbols relocate" as symbols_relocate
[*] --> resolve_and_load_dependencies
resolve_and_load_dependencies --> symbols_resolve
symbols_resolve --> symbols_relocate
symbols_relocate --> [*]
}
state dynamic_start {
[*] --> normal_execution
normal_execution --> [*] : Program exit
normal_execution --> external_function_call: Call external function
external_function_call --> normal_execution: External function return
state check_got_entry <>
external_function_call --> check_got_entry
check_got_entry --> direct_jump: GOT entry resolved
check_got_entry --> plt_lazy_binding: GOT entry unresolved
state "PLT Lazy Binding" as plt_lazy_binding {
[*] --> push_reloc_index
push_reloc_index --> jump_to_plt0
jump_to_plt0 --> _dl_runtime_resolve
state _dl_runtime_resolve {
direction LR
[*] --> lookup_symbol_address
lookup_symbol_address --> update_got_entry
update_got_entry --> [*]
}
_dl_runtime_resolve --> [*]
}
plt_lazy_binding --> function_executed
direct_jump --> function_executed
function_executed --> external_function_call
}
state entry_point_join <>
static_start --> entry_point_join
dynamic_start --> entry_point_join
entry_point_join --> exit
exit --> [*]
}
UserSpace --> [*]
可以看到,一个ELF的的加载和执行,以execve系统调用作为入口。整个过程根据其上下文切换可以分为内核态和用户态两个阶段。
加载
这个阶段,Shell通过 execve 陷入kernel space。
在图中可以清晰看到,execve 对于静态链接文件和动态链接文件的区分并不大。
- 两类ELF文件均需要解析程序头表,遍历加载所有LOAD段;
- 完成LOAD段加载后,会检查该ELF文件是否存在interp段(仅动态链接文件存在该结构)。对于存在interp段的ELF文件,会将其中记录的解释器路径同样mmap到内存中;
- 启动参数传递到进程的用户态堆栈;
- 计算entry_point:静态链接文件的入口地址为文件本身记录的入口点(_start),动态链接文件的入口地址则为运行时解释器的入口点地址(dl_main);
- 最后,start_thread 修改进程的内核堆栈,主要是将上一步获得的enrty_point设置到指令指针寄存器中。
至此,execve 系统调用完成其所有工作,将一个ELF文件成功加载到进程中。
执行
execve返回后,上下文切换至user space。
创建出的进程被内核调度执行,获取时间片后,恢复cpu上下文,进程从其指令指针寄存器指向的运行时地址开始执行。
静态链接
- 静态链接的ELF文件直接从 _start 函数开始执行,其运行所需的所有数据、指令条目(函数)均已经被加载到当前进程的内存空间中;
- 函数跳转只需要按照指令条目中的间接跳转方式进行跳转即可;
- 程序执行完毕,退出后进程被回收。
动态链接
动态链接的ELF文件从其内存中映射的解释器entry_point开始执行;
依赖加载
- 解释器首先检索ELF文件的动态段 .dynamic,从其中获取所有DT_NEED条目,即依赖项;
- 在字符串表.dynstr中获取到该依赖的名称,而后从文件系统中检索并mmap加载该依赖库。
符号解析/重定位
在解释原因前,先给出其行为:
- 解析数据重定位表 .rela.dyn,遍历每个数据重定位条目,并从加载到内存中依赖库的全局符号表搜索该符号,将该数据符号的地址写入数据重定位条目记录的地址中,完成数据重定位;
- 解析代码重定位表.rela.plt,遍历每个代码重定位条目。此时代码重定位有两种策略:
- 立即绑定:对于单个代码重定位条目,直接将依赖库中的符号地址写入条目记录的地址中(.got表项),完成重定位;
- 延迟绑定(默认):对于单个代码重定位条目,什么都不做,等到代码执行需要跳转时,由_dl_runtime_resolve完成重定位。
现在看这些行为,恐怕难以理解。在代码执行阶段将更细致地解释其原因。
代码执行
我们首先将上面例子中链接出的动态链接文件反汇编,并查看其 .got(全局偏移表):
1 | |
在完成动态链接后,程序真正进入其用户代码的执行阶段。在程序的_start中,指令被按顺序执行,其中的寻址主要分为两类:
数据寻址
当前程序中定义的数据符号存储于数据段的.data和.bss节,指令中可以直接读写其内存地址,在例子中的 0x103c和0x1061可以观察到这类寻址方式;
对于外部数据的寻址,在动态链接的重定位阶段,重定位符号的虚拟内存地址被写入数据段(非数据,而是数据的地址)。
指令中通过获取该地址,并再次寻址来获取外部数据。在例子的0x104c和0x1053中可以观察到这一寻址方式。
函数寻址
对于动态链接的ELF文件,函数的寻址都是通过函数调用桩 .plt 来实现的。相当于在没有加载外部依赖前,在指令中使用该调用桩作为占位符以及对于某个函数调用的统一入口。
这么做可以大幅减少重定位的工作(静态链接中,程序有多少次该函数的调用,就会产生多少个重定位表项)。而对应于动态链接时代码重定位的两种绑定方式,执行阶段也同样有所差异。
- 立即绑定:由于在动态链接阶段,.got全局偏移表的表项已经被全部填充为了真实的函数地址,所以指令中可以通过.plt函数调用桩(例如上面的add@plt),直接查.got表的对应索引项获取到函数真实的地址。
- 延迟绑定
- 最初所有.got表项(_dl_runtime_resolve除外)所指向的地址均为.plt的通用逻辑(例子中的0x1010);
- 所以指令中对所有对外部函数的首次调用,均会跳转到.plt;
- .plt会在完成参数传递后,跳转到_dl_runtime_resolve(解释器提供的运行时函数地址解析器);
- 解析器通过搜索解析外部依赖库的符号表,获取该符号的真实地址,而后将其写入.got表项,并完成函数跳转;
- 当后续再次调用该函数时,.got表中该函数对应的表项已经被写入了真实的函数地址,可以直接完成寻址和跳转。
了解完两种绑定策略后,可以发现,事实上立即绑定就是在动态链接阶段提前遍历完成了所有外部函数符号的runtime_resolve工作,使.got表在运行时可以直接被使用。
使用立即绑定的弊端也很明显,在不同的上下文和启动参数下,当用户代码中使用的外部函数少于其所声明(重定位表项)的数量时,遍历整个重定位表将解析很多无意义的外部符号,造成性能损耗,拉长程序启动时间。