
ELF(Executable and Linkable Format)是 Linux/Unix-like 系统的标准二进制文件格式,统一了可执行文件、共享库、目标文件、核心转储文件等多种类型的二进制文件规范。它替代了早期的 a.out 格式,具有跨架构兼容性、灵活的链接机制、支持动态加载等核心优势,是理解 Linux 程序运行和系统底层的关键基础。
一、ELF 的核心定位与作用
1. 核心目标
- 统一二进制文件标准:解决不同类型二进制文件(目标文件、可执行文件、共享库等)的格式碎片化问题,让链接器(
ld)、加载器(execve系统调用)、调试器(gdb)等工具能统一处理。 - 支持多场景需求:同时满足「编译链接阶段」和「运行加载阶段」的需求:
- 链接时:作为目标文件(.o),支持符号解析、重定位、节(Section)合并。
- 运行时:作为可执行文件(.out/.bin)或共享库(.so),支持内存加载、段(Segment)映射、动态链接。
2. 与其他格式的对比
| 格式 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| ELF | Linux/Unix-like 主流 | 跨架构、支持动态链接、结构灵活 | 复杂度高于 a.out |
| a.out | 早期 Unix/Linux | 结构简单、解析快速 | 不支持动态链接、扩展性差 |
| PE/COFF | Windows 系统 | 适配 Windows 内核 | 与 Linux 不兼容 |
二、ELF 的文件结构(核心重点)
ELF 文件的结构遵循「头部 + 表 + 数据区域」的分层设计,所有结构均以 字节对齐 方式存储(避免内存访问异常)。核心结构可分为 4 个部分,用一张图直观展示:

+----------------------------+
| ELF Header (文件头部) | # 全局元信息,工具入口
+----------------------------+
| Program Header Table | # 段表:指导加载器映射内存(仅可执行/共享库有)
+----------------------------+
| 数据区域(Segments/Sections) | # 实际内容:代码、数据、符号表等
| - .text(代码段) |
| - .data(已初始化数据) |
| - .bss(未初始化数据) |
| - .symtab(符号表) |
| - ...(其他节/段) |
+----------------------------+
| Section Header Table | # 节表:指导链接器合并/解析(仅目标文件/可执行文件有)
+----------------------------+
1. 核心结构详解
(1)ELF Header(文件头部)
ELF 文件的「总入口」,存储文件的全局元信息,工具(如 readelf、加载器)首先读取此部分获取文件的基本属性。
关键字段(32 位 / 64 位结构类似,仅字段长度不同):
| 字段名 | 含义 | 示例值 |
|---|---|---|
e_ident | 标识信息:魔数、字节序、架构、ELF 版本 | 魔数 0x7f454c46("ELF") |
e_type | 文件类型 | 1 = 可重定位文件(.o)、2 = 可执行文件、3 = 共享库(.so) |
e_machine | 目标架构 | 3=IA-32、62=x86-64、40=ARM |
e_entry | 程序入口地址(运行时 PC 起始值) | 0x400430(x86-64 可执行文件) |
e_phoff | 程序头表在文件中的偏移量 | 0x40(64 字节) |
e_shoff | 节头表在文件中的偏移量 | 0x1000(4096 字节) |
e_phnum | 程序头表项数量 | 9(x86-64 可执行文件常见) |
e_shnum | 节头表项数量 | 30+(根据文件复杂度变化) |
e_shstrndx | 节名表(.shstrtab)在节头表中的索引 | 29(指向存储节名的节) |
作用:加载器 / 链接器通过 e_ident 验证文件合法性,通过 e_type 确定文件用途,通过 e_phoff/e_shoff 定位程序头表 / 节头表。
(2)Program Header Table(程序头表,段表)
仅存在于 可执行文件(.out) 和 共享库(.so) 中,描述「如何将文件加载到内存」。每个表项对应一个「段(Segment)」,段是内存加载的最小单位(由多个功能相关的「节(Section)」合并而成)。
关键字段(Elf64_Phdr 结构):
| 字段名 | 含义 | 示例值 |
|---|---|---|
p_type | 段类型 | PT_LOAD(可加载段)、PT_DYNAMIC(动态链接信息) |
p_offset | 段在文件中的偏移量 | 0x0(第一个 PT_LOAD 段) |
p_vaddr | 段加载到内存的虚拟地址 | 0x400000(x86-64 代码段地址) |
p_paddr | 物理地址(嵌入式系统用,Linux 忽略) | 0x0 |
p_filesz | 段在文件中的大小(压缩 / 实际存储大小) | 0x1000(4096 字节) |
p_memsz | 段在内存中的大小(可能包含未初始化数据) | 0x2000(8192 字节) |
p_flags | 段权限 | PF_R(读)、PF_X(执行)、PF_W(写) |
p_align | 内存对齐要求(必须是页大小的整数倍) | 0x1000(4KB,Linux 页大小) |
核心段类型:
PT_LOAD:可加载段(最核心),分为两类:- 代码段(权限 R-X):包含
.text(机器指令)、.rodata(只读数据,如字符串常量)。 - 数据段(权限 R-W):包含
.data(已初始化全局变量 / 静态变量)、.bss(未初始化全局变量 / 静态变量,文件中不占空间,内存中分配零初始化)。
- 代码段(权限 R-X):包含
PT_DYNAMIC:动态链接信息段,存储共享库依赖、符号查找等信息(.so 和动态链接的可执行文件有)。PT_INTERP:解释器路径段,存储动态链接器路径(如/lib64/ld-linux-x86-64.so.2),动态链接的可执行文件必须有。
作用:加载器(execve 系统调用触发)根据 PT_LOAD 表项,将文件中的段映射到内存对应虚拟地址,设置权限后,跳转到 e_entry 入口地址执行程序。
(3)Section Header Table(节头表)
存在于 目标文件(.o)、可执行文件、共享库 中,描述「文件的节结构」,是链接器(ld)的核心依赖。每个表项对应一个「节(Section)」,节是链接时的最小单位(按功能划分的逻辑单元)。
关键字段(Elf64_Shdr 结构):
| 字段名 | 含义 | 示例值 |
|---|---|---|
sh_name | 节名(索引到 .shstrtab 节) | 11(对应 ".text") |
sh_type | 节类型 | SHT_PROGBITS(程序数据)、SHT_SYMTAB(符号表) |
sh_flags | 节属性 | SHF_EXECINSTR(可执行)、SHF_WRITE(可写) |
sh_addr | 节在内存中的虚拟地址(仅可执行文件有) | 0x400430(.text 节地址) |
sh_offset | 节在文件中的偏移量 | 0x430(1072 字节) |
sh_size | 节的大小 | 0x120(288 字节) |
sh_link | 关联节的索引(如符号表关联字符串表) | 28(.strtab 节索引) |
sh_info | 附加信息(如重定位表关联的节索引) | 1(.text 节索引) |
sh_addralign | 节的对齐要求(如 8 字节对齐) | 16(.text 节常见) |
核心节类型(按功能分类):
| 节名 | 类型 | 作用 |
|---|---|---|
.text | SHT_PROGBITS | 存储机器指令(代码),权限 R-X |
.rodata | SHT_PROGBITS | 只读数据(字符串常量、const 变量) |
.data | SHT_PROGBITS | 已初始化全局变量 / 静态变量(非 const) |
.bss | SHT_NOBITS | 未初始化全局变量 / 静态变量(文件中不占空间,内存中零初始化) |
.symtab | SHT_SYMTAB | 符号表(函数名、变量名、地址映射) |
.strtab | SHT_STRTAB | 字符串表(存储符号名、节名的原始字符串) |
.shstrtab | SHT_STRTAB | 节名表(专门存储节名的字符串表) |
.rel.text | SHT_REL | 代码段重定位表(记录需要修正的符号地址) |
.rel.data | SHT_REL | 数据段重定位表 |
.dynamic | SHT_DYNAMIC | 动态链接信息(共享库依赖、符号查找表) |
.plt | SHT_PROGBITS | 过程链接表(动态链接时延迟绑定符号) |
.got | SHT_PROGBITS | 全局偏移表(存储符号的实际内存地址) |
作用:链接器通过节头表找到各个节,合并多个目标文件的 .text/.data 节,通过 .symtab 解析符号引用(如函数调用、变量访问),通过 .rel.text/.rel.data 完成重定位(修正符号的实际地址)。
(4)数据区域(Segments/Sections 内容)
ELF 文件的实际存储区域,包含代码、数据、符号表等具体内容:
- 节(Section):链接时的逻辑划分(如
.text是代码,.data是数据)。 - 段(Segment):加载时的物理划分(由多个节合并,如
.text+.rodata→ 一个 R-X 权限的 PT_LOAD 段)。 - 你的程序能跑起来,本质是:链接器把
.o文件的节合并成段,内核把段映射到内存,CPU 执行段里的代码。
关键区别:
| 维度 | 节(Section) | 段(Segment) |
|---|---|---|
| 核心用途 | 编译 / 链接 / 调试(逻辑组织) | 内存加载 / 运行(物理映射) |
| 面向对象 | 编译器(gcc)、链接器(ld)、调试器(gdb) | 内核加载器(execve)、动态链接器(ld-linux.so) |
| 划分依据 | 功能(代码、数据、符号表…) | 内存权限 + 加载方式(R-X、R-W…) |
| 存在场景 | 目标文件(.o)、可执行文件(.out)、共享库(.so) | 仅可执行文件(.out)、共享库(.so) |
| 数量 | 多(几十甚至上百个) | 少(几个到十几个) |
| 关系 | 多个节 → 合并成一个段 | 一个段 ← 包含多个节 |
2. 不同类型 ELF 文件的结构差异
ELF 支持 4 种核心文件类型,结构上各有侧重:
| 文件类型 | 后缀 | 核心结构特点 | 用途 |
|---|---|---|---|
| 可重定位文件 | .o | 有节头表,无程序头表(或 PT_LOAD 表项不完整) | 编译后的中间文件,用于链接生成可执行文件 / 共享库 |
| 可执行文件 | 无后缀 /.out | 有程序头表(含 PT_LOAD)和节头表 | 直接运行(./a.out) |
| 共享目标文件 | .so | 有程序头表(含 PT_LOAD/PT_DYNAMIC)和节头表 | 动态链接库(如 libc.so.6),被多个程序共享 |
| 核心转储文件 | .core | 包含程序崩溃时的内存镜像、寄存器状态 | 调试崩溃原因(gdb ./a.out core) |
三、ELF 的核心流程:链接与加载
1. 链接过程(生成可执行文件 / 共享库)
链接器(ld)将多个 .o 目标文件 + 静态库(.a)/ 共享库(.so)合并为可执行文件 / 共享库,核心步骤:
- 合并节:将所有输入文件的
.text节合并为一个.text节,.data节合并为一个.data节,以此类推。 - 符号解析:通过
.symtab符号表,解析跨文件的符号引用(如main函数调用printf,printf是libc.so中的符号)。 - 重定位:修正符号的实际地址(目标文件中符号地址是相对偏移,链接后替换为内存中的绝对地址):
- 示例:目标文件中
call printf的地址是0x00000000(占位),链接后替换为printf在libc.so中的实际地址(或.plt表项地址,动态链接时)。
- 示例:目标文件中
- 生成输出文件:
- 若生成可执行文件:添加程序头表(PT_LOAD/PT_INTERP 等),设置
e_entry入口地址。 - 若生成共享库:添加 PT_DYNAMIC 段,支持动态链接(延迟绑定、符号重定位)。
- 若生成可执行文件:添加程序头表(PT_LOAD/PT_INTERP 等),设置
2. 加载过程(运行可执行文件 / 共享库)
加载器(Linux 内核的 execve 系统调用 + 动态链接器 ld-linux.so)将 ELF 文件加载到内存并运行,核心步骤:
(1)静态链接可执行文件的加载
- 内核解析 ELF Header,验证文件合法性(魔数、架构匹配)。
- 读取程序头表,找到所有 PT_LOAD 段,将其映射到内存的
p_vaddr虚拟地址(遵循页对齐)。 - 初始化
.bss段(分配内存并清零,因为文件中不存储.bss内容)。 - 设置程序计数器(PC)为
e_entry入口地址,跳转到用户态执行代码。
(2)动态链接可执行文件的加载
- 内核加载可执行文件的 PT_LOAD 段到内存。
- 读取 PT_INTERP 段,找到动态链接器路径(如
/lib64/ld-linux-x86-64.so.2),加载动态链接器到内存。 - 动态链接器解析可执行文件的
.dynamic段,找到依赖的共享库(如libc.so.6),加载共享库到内存。 - 动态链接器通过
.plt(过程链接表)和.got(全局偏移表)完成延迟绑定:- 首次调用共享库函数(如
printf)时,.plt表项触发动态链接器查找printf的实际地址,存入.got。 - 后续调用直接从
.got读取地址,避免重复查找(提升性能)。
- 首次调用共享库函数(如
- 动态链接器修正所有符号的地址后,跳转到
e_entry入口地址执行程序。
四、ELF 工具实战:查看与分析 ELF 文件
Linux 提供多个工具用于分析 ELF 文件,核心工具如下:
1. readelf:全面解析 ELF 结构
最强大的 ELF 分析工具,支持查看所有头部、表项、节 / 段信息。
常用命令:
# 1. 查看 ELF Header(文件头部)
readelf -h a.out
# 2. 查看程序头表(段表)
readelf -l a.out # -l = --program-headers
# 3. 查看节头表(节表)
readelf -S a.out # -S = --section-headers
# 4. 查看符号表
readelf -s a.out # -s = --symbols(.symtab 节)
readelf -s --dyn-syms a.out # 查看动态符号表(.dynsym 节,共享库相关)
# 5. 查看动态链接信息(.dynamic 节)
readelf -d a.out # -d = --dynamic
# 6. 查看重定位表
readelf -r a.out # -r = --relocs(.rel.text/.rel.data 节)
示例输出(ELF Header):
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400430 # 程序入口地址
Start of program headers: 64 (bytes into file)
Start of section headers: 4096 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 29
2. objdump:反汇编与详细分析
侧重反汇编代码、查看节内容,适合调试和理解程序执行逻辑:
# 1. 反汇编 .text 节(查看机器指令)
objdump -d a.out # -d = --disassemble
# 2. 查看所有节的内容(十六进制 + ASCII)
objdump -s a.out # -s = --full-contents
# 3. 查看动态链接信息(类似 readelf -d)
objdump -p a.out # -p = --private-headers
# 4. 查看重定位信息(类似 readelf -r)
objdump -r a.out # -r = --reloc
3. nm:查看符号表
快速查看 ELF 文件的符号(函数名、变量名)及其类型:
nm a.out # 查看所有符号
nm -D a.out # 查看动态符号(共享库相关)
nm -u a.out # 查看未定义符号(需链接其他文件的符号)
符号类型含义:
T:全局函数(.text节)。D:已初始化全局变量(.data节)。B:未初始化全局变量(.bss节)。U:未定义符号(如printf,需要从共享库中查找)。R:只读数据(.rodata节)。
五、ELF 的核心优势
- 跨架构兼容性:支持 x86、ARM、RISC-V 等多种架构,通过
e_machine字段标识目标架构,工具可自动适配。 - 灵活的链接机制:同时支持静态链接(合并静态库到可执行文件)和动态链接(运行时加载共享库,节省内存)。
- 延迟绑定(PLT/GOT):动态链接时,符号的地址绑定延迟到首次调用时,减少启动时间(仅加载必要的共享库符号)。
- 支持核心转储:程序崩溃时生成
.core文件,包含完整的内存镜像和寄存器状态,便于调试。 - 扩展性强:可通过自定义节(如
.note节存储版本信息)扩展功能,不破坏原有结构。
六、总结
ELF 是 Linux 系统底层的「二进制文件标准」,其核心价值在于统一链接和加载的接口:
- 对开发者:编译(
gcc)生成.o目标文件,链接(ld)生成可执行文件 / 共享库,无需关注底层格式细节。 - 对系统:加载器(内核)和动态链接器(
ld-linux.so)通过 ELF 头部和表结构,标准化地完成程序加载和运行。
理解 ELF 的结构和流程,是深入掌握 Linux 程序运行原理、动态链接、调试崩溃问题的关键基础。建议结合 readelf/objdump 工具实际分析一个简单的 C 程序(如 hello.c 编译后的 a.out),直观感受 ELF 的结构和字段含义。
七. 补充
程序执行状态:
| 程序状态 | 虚拟地址情况 | 物理地址情况 | 内核核心操作 |
|---|---|---|---|
| 未执行(磁盘态) | 1. 仅存在「虚拟地址约定」(ELF 文件中的 p_vaddr、e_entry 等字段);2. 这些值是链接器写死的 “加载规则”,不是内存中的有效地址;3. 无进程虚拟地址空间(mm_struct 未创建),虚拟地址未 “生效”。 | 1. 无任何物理地址分配;2. 无页表(虚拟→物理的映射表);3. 程序以二进制文件存储在磁盘,与物理内存无关联。 | 无(内核未为该程序分配任何内存资源)。 |
| 执行中(内存态) | 1. 内核创建 mm_struct(进程虚拟地址空间),ELF 的 p_vaddr 映射为进程内的「合法虚拟地址」(如 0x400000 代码段);2. 虚拟地址是进程私有,每个进程独立(即使同程序,虚拟地址相同但隔离);3. 虚拟地址范围固定(x86-64 用户态 0~0x7FFFFFFFFFFF)。 | 1. 「按需分配」:仅当进程访问虚拟地址时,内核才分配物理页并建立映射;2. 物理地址是全局唯一的硬件内存地址(如 0x12345678);3. 只读段(.text)可被多个进程共享物理内存(如动态库),可写段(.data)默认进程私有(COW 机制)。 | 1. 创建 mm_struct 和 vm_area_struct(虚拟地址区域描述);2. 解析 ELF 段表,建立虚拟地址范围;3. 缺页异常触发物理页分配 + 页表更新;4. 动态链接时映射共享库虚拟地址。 |
动态库的物理内存副本 “不属于任何单个进程”,而是由内核全局管理;每个使用该动态库的进程,只是在自己的虚拟地址空间「共享区」建立了指向这份物理内存的映射
MMU和mm_struct:
构建虚拟地址到物理地址的映射关系,是内核(软件)+ MMU(硬件) 协同完成的核心工作,核心载体是「页表(Page Table)」,且遵循「先搭框架、按需填充」的原则(不会一次性构建所有映射)。
| 维度 | MMU(内存管理单元) | mm_struct(进程地址空间描述符) |
|---|---|---|
| 本质 | CPU 内置硬件组件 | 内核软件数据结构 |
| 核心使命 | 硬件层面完成虚拟→物理地址转换、权限校验 | 软件层面管理进程虚拟地址空间、页表、内存资源 |
| 依赖关系 | 依赖 mm_struct 关联的页表完成转换 | 为 MMU 提供地址映射的 “规则”(页表) |
| 触发交互 | 缺页异常时通知内核(软件)处理 | 内核通过它处理缺页异常、更新页表 |
| 生命周期 | 随 CPU 硬件存在(开机即有) | 随进程创建而创建,进程退出而销毁 |
-
每个进程(或线程)的task_struct中都有一个指针(mm)指向其内存描述符mm_struct。对于普通进程,每个进程都有自己独立的mm_struct。
-
线程组(同一进程中的线程)共享同一个地址空间,因此它们共享同一个mm_struct。在task_struct中,线程的mm指针指向同一个mm_struct。
页表:
页表(Page Table)是 虚拟地址到物理地址映射关系的核心存储载体,本质是内核维护的「虚拟页号(VPN)→ 物理页号(PPN)」的映射表,配合 MMU 完成地址转换。它是虚拟内存机制的核心,解决了 “如何高效存储海量映射关系” 的问题 —— 不按单个字节映射,而是按「页(4KB 默认)」为最小单位映射,大幅降低映射表的内存开销。
动态库链接:
在Linux中,动态库(共享库)在内存中是以共享内存段的形式存在的, 所有依赖其的进程的共享区存放着指向其的引用.当一个动态库被加载到内存中时,它会被映射到进程的虚拟地址空间中的共享区域。多个进程可以共享同一个动态库在物理内存中的同一份代码段,每个进程的虚拟地址空间中都会有一个映射指向这个物理内存区域。
动态链接与静态链接不同,静态链接是在编译链接阶段就将所有库的代码和数据合并到可执行文件中,而动态链接则将链接过程推迟到程序加载时(或运行时)。
具体过程如下:
-
程序加载:当我们运行一个程序时,操作系统(实际上是动态链接器)会先将程序本身的代码和数据加载到内存中。然后,它会检查程序所依赖的动态库(共享库)。
-
动态库加载:操作系统会将程序依赖的动态库也加载到内存中。但是,这些动态库在内存中的加载地址并不是固定的(这与静态链接时确定的地址不同)。操作系统会根据当前地址空间的空闲情况,为每个动态库分配一段虚拟内存空间。
-
地址分配:由于动态库的加载地址在每次运行时可能都不相同(即使同一个库,在不同的运行过程中也可能被加载到不同的地址),因此需要在加载时确定库的具体地址。
-
修正引用:动态库中包含了需要跳转的函数地址(例如,库内部函数调用、库函数被外部调用等)。在加载动态库时,其加载地址确定后,动态链接器会修正这些跳转地址,使其指向正确的内存位置。这个过程称为重定位(Relocation)。
-
符号解析:动态链接器还会解析程序与库之间、库与库之间的符号引用,确保所有符号都能找到对应的地址。
-
延迟绑定:为了优化性能,动态链接通常采用延迟绑定(Lazy Binding)技术,即函数地址的解析推迟到第一次调用该函数时。这样,程序启动时不需要解析所有函数,加快启动速度。
GOT是什么?
GOT(全局偏移表)是一个存储在数据段中的表,其中每一项都是一个指针,指向一个全局符号(函数或变量)的地址。由于数据段在加载时可以被重定位,所以动态链接器可以通过修改GOT中的条目来更新这些符号的实际地址。
在程序开始运行前,动态链接器会解析出这些符号的绝对地址,然后填充GOT。这样,代码中通过GOT间接访问这些符号就可以得到正确的地址。
GOT如何工作?
我们以调用一个共享库中的函数为例:
在编译时,编译器并不知道这个函数的实际地址,所以它会在代码中生成一个对GOT条目的引用。这个GOT条目在编译时被预留,并在动态链接时由动态链接器填充。
例如,在x86_64架构上,使用位置无关代码,调用一个外部函数foo的代码可能看起来像这样:
call [rip + offset_to_GOT_entry_for_foo]
这里,offset_to_GOT_entry_for_foo是相对于当前指令指针(rip)的偏移,用于找到GOT中对应foo函数的条目。注意,GOT的地址在编译时是已知的(因为它是数据段的一部分,而数据段相对于代码段的偏移是固定的),但是GOT中条目的内容(即函数的实际地址)在编译时未知。
在程序加载时,动态链接器会计算foo函数的实际地址,并将其写入到GOT中对应的条目。因此,当上述指令执行时,它实际上是从GOT中取出foo的地址然后进行调用。
位置无关代码(PIC)详解
1. 什么是位置无关代码?
位置无关代码(Position-Independent Code, PIC) 是一种编译和链接技术,使得代码无论被加载到内存的哪个地址,都能够正确执行,而不需要修改代码段中的地址。PIC 是现代操作系统实现动态链接和地址空间布局随机化(ASLR)的基础。
2. 为什么需要 PIC?
在早期静态链接的程序中,代码和数据地址在编译链接时就已经确定。当程序加载到内存时,操作系统必须将其加载到固定的地址(即链接时指定的地址),否则程序无法运行。这带来了两个问题:
-
内存浪费:每个进程都需要自己的一份代码副本,即使代码相同。
-
安全问题:攻击者可以预测代码和数据的位置,从而容易进行攻击。
动态链接库(共享库)被设计为可以被多个进程共享,因此必须能够被加载到不同进程的地址空间的不同位置。此外,为了提高安全性,现代操作系统使用 ASLR 随机化进程地址空间,所以程序代码必须能够适应这种随机化。
3. PIC 的核心思想
PIC 的核心思想是:代码中不包含绝对地址,而是使用相对地址或通过间接寻址来访问数据和函数。这样,无论代码段被加载到哪里,只要相对偏移不变,代码就能正确运行。
PLT是什么?
PLT(Procedure Linkage Table,过程链接表)是动态链接中用于实现延迟绑定(lazy binding)的关键数据结构。它位于代码段,包含一小段代码,用于间接调用动态库中的函数。
当程序调用动态库中的函数时,编译器会生成一个对PLT条目的调用。每个PLT条目对应一个动态库函数,它包含跳转到GOT(Global Offset Table)中对应条目的代码。初始时,GOT中的条目指向PLT中的一段特殊代码,该代码会调用动态链接器来解析函数的真实地址,并将其写回GOT。之后,再次调用该函数时,PLT条目就可以直接跳转到GOT中存储的真实地址。
PLT的工作流程可以分为以下步骤:
-
程序调用一个动态库函数,例如
printf,实际是调用了printf@plt。 -
printf@plt的第一条指令跳转到GOT中对应的条目。初始时,这个GOT条目指向PLT中的下一条指令(即push指令)。 -
接着,程序将函数的编号(或标识符)压栈,然后跳转到PLT[0](这是一个特殊的PLT条目,位于所有PLT条目的开头)。
-
PLT[0]会调用动态链接器(通过GOT[1]和GOT[2]的帮助)来解析函数的真实地址。
-
动态链接器找到函数的真实地址后,将其写回GOT中对应的条目,然后跳转到该地址执行函数。
-
以后再次调用该函数时,
printf@plt的第一条指令就会直接跳转到GOT中存储的真实地址,无需再次解析。
这种机制被称为延迟绑定,因为函数地址的解析被推迟到第一次调用时,从而提高了程序的启动速度。
523

被折叠的 条评论
为什么被折叠?



