【Linux系统编程】(二十八)深入 ELF 文件原理:从目标文件到程序加载的完整揭秘

📅 发布时间:2026/7/5 3:10:58 👁️ 浏览次数:
【Linux系统编程】(二十八)深入 ELF 文件原理:从目标文件到程序加载的完整揭秘
目录​编辑前言一、先搞懂什么是目标文件—— 编译后的 “半成品”1.1 目标文件的本质ELF 格式的 “最小单元”步骤 1写两个源码文件步骤 2编译生成目标文件步骤 3查看目标文件类型1.2 为什么需要目标文件—— 模块化开发的核心二、ELF 文件全景解析4 种类型 4 大核心结构2.1 核心结构 1ELF 头ELF Header—— 文件的 “总目录”用readelf -h查看 ELF 头内核中的 ELF 头数据结构2.2 核心结构 2节Section—— ELF 文件的 “数据容器”用readelf -S查看所有节关键节详解2.3 核心结构 3节头表Section Header Table—— 节的 “索引表”2.4 核心结构 4程序头表Program Header Table—— 加载的 “说明书”用readelf -l查看程序头表关键字段详解三、ELF 的生命周期从源码到加载的完整流程3.1 阶段 1ELF 形成可执行文件 —— 目标文件的 “合并与重定位”步骤 1目标文件的节合并步骤 2符号解析步骤 3地址重定位3.2 阶段 2ELF 可执行文件加载 —— 从磁盘到内存的 “变身”3.2.1 加载的核心原则Section 合并为 Segment3.2.2 加载的完整流程3.2.3 虚拟地址空间与 ELF 加载的关系3.2.4 加载后的内存布局四、实战用工具分析 ELF 文件 —— 亲手拆解 ELF 黑盒4.1 常用工具清单4.2 实战 1分析目标文件的重定位信息4.3 实战 2分析可执行文件的节合并与地址重定位4.4 实战 3分析动态库的 ELF 结构五、ELF 文件的核心作用 —— 为什么它能成为 Linux 的 “标准”总结前言在 Linux/Unix 系统中ELFExecutable and Linkable Format是绝对的 “核心玩家”—— 无论是我们写的 C/C 代码编译后的目标文件.o、可执行程序还是动态库.so本质上都是 ELF 格式文件。你每天运行的ls、gcc甚至系统内核的部分模块背后都藏着 ELF 的身影。但 ELF 文件就像一个精密的 “黑盒子”它内部如何组织代码和数据目标文件如何合并成可执行程序程序加载到内存时又发生了什么今天我们就亲手拆解这个黑盒子从底层原理到实战操作带你彻底搞懂 ELF 文件的方方面面。下面就让我们正式开始吧一、先搞懂什么是目标文件—— 编译后的 “半成品”在聊 ELF 之前我们得先明白 “目标文件”.o 文件的角色。写过 C/C 的同学都知道程序从源码到可执行文件要经过 “编译→链接” 两步编译编译器如 gcc将单个.c/.cpp源码翻译成 CPU 能识别的机器码生成目标文件.o。链接链接器如 ld将多个目标文件 依赖的库文件合并修正函数 / 变量的地址最终生成可执行程序。1.1 目标文件的本质ELF 格式的 “最小单元”目标文件是 ELF 文件的一种可重定位文件它是源码编译后的 “半成品”—— 包含了编译后的机器码、数据以及链接时需要的重定位信息、符号表等。我们用一个简单的例子直观感受步骤 1写两个源码文件// hello.c #includestdio.h void run(); // 声明在code.c中定义的函数 int main() { printf(hello world!\n); run(); return 0; }// code.c #includestdio.h void run() { printf(running...\n); }步骤 2编译生成目标文件用gcc -c命令只编译不链接生成.o文件gcc -c hello.c # 生成hello.o gcc -c code.c # 生成code.o ls -l # 输出 # -rw-rw-r-- 1 user user 62 10月 31 15:36 code.c # -rw-rw-r-- 1 user user 1672 10月 31 15:46 code.o # -rw-rw-r-- 1 user user 103 10月 31 15:36 hello.c # -rw-rw-r-- 1 user user 1744 10月 31 15:46 hello.o步骤 3查看目标文件类型用file命令可以验证目标文件的格式file hello.o # 输出hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not strippedELF 64-bit64 位 ELF 文件relocatable可重定位文件目标文件的类型not stripped保留了符号表等调试信息1.2 为什么需要目标文件—— 模块化开发的核心目标文件的存在让大型项目的开发变得高效独立编译修改一个源码文件后只需重新编译对应的.o文件无需编译整个项目节省时间。模块复用多个项目可以共用同一个.o文件避免重复编译。链接灵活可以通过链接不同的.o文件和库组合出不同功能的可执行程序。比如一个游戏项目渲染模块、物理引擎模块、网络模块可以分别编译成.o文件最后通过链接器合并成完整的游戏程序。二、ELF 文件全景解析4 种类型 4 大核心结构ELF 文件不只是目标文件它是一个 “家族”包含 4 种核心类型每种类型都有特定的用途ELF 文件类型后缀用途示例可重定位文件.o编译后的目标文件用于链接成可执行程序或动态库hello.o、code.o可执行文件无后缀可以直接运行的程序/bin/ls、a.out共享目标文件.so动态库可被多个程序共享使用libc.so.6、libmystdio.so内核转储文件core进程崩溃时的内存快照用于调试core.12345无论哪种 ELF 文件内部结构都遵循统一的规范核心由 4 部分组成ELF 头ELF Header文件的 “身份证”描述文件的基本信息类型、架构、大小、各个部分的偏移量等。程序头表Program Header Table告诉操作系统如何加载文件到内存仅可执行文件和动态库有。节头表Section Header Table描述文件中的 “节”Section信息是链接器的主要操作对象。节SectionELF 文件的基本组成单位存储代码、数据、符号表等具体内容。我们可以用一张图直观理解 ELF 文件的结构2.1 核心结构 1ELF 头ELF Header—— 文件的 “总目录”ELF 头位于文件的最开始是整个 ELF 文件的 “总目录”操作系统和工具如链接器、调试器首先读取 ELF 头才能找到其他部分的位置。用readelf -h查看 ELF 头以目标文件hello.o为例readelf -h hello.o输出结果详解关键字段ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 # 魔数ELF文件标识 Class: ELF64 # 64位架构 Data: 2s complement, little endian # 小端序 Version: 1 (current) # 版本 OS/ABI: UNIX - System V # 目标操作系统 ABI Version: 0 Type: REL (Relocatable file) # 类型可重定位文件 Machine: Advanced Micro Devices X86-64 # 目标CPU架构 Version: 0x1 Entry point address: 0x0 # 入口地址目标文件无入口可执行文件有 Start of program headers: 0 (bytes into file) # 程序头表偏移目标文件无 Start of section headers: 728 (bytes into file) # 节头表偏移 Flags: 0x0 Size of this header: 64 (bytes) # ELF头大小 Size of program headers: 0 (bytes) # 程序头表条目大小目标文件无 Number of program headers: 0 # 程序头表条目数目标文件无 Size of section headers: 64 (bytes) # 节头表条目大小 Number of section headers: 13 # 节的数量 Section header string table index: 12 # 节名称字符串表的索引内核中的 ELF 头数据结构操作系统内核通过解析 ELF 头来识别和加载 ELF 文件内核中定义了对应的结构体/linux/include/elf.h// 64位ELF头结构体 typedef struct elf64_hdr { unsigned char e_ident[EI_NIDENT]; // 魔数和相关属性 Elf64_Half e_type; // ELF文件类型REL、EXEC、DYN等 Elf64_Half e_machine; // 目标CPU架构 Elf64_Word e_version; // 文件版本 Elf64_Addr e_entry; // 程序入口虚拟地址可执行文件 Elf64_Off e_phoff; // 程序头表在文件中的偏移量 Elf64_Off e_shoff; // 节头表在文件中的偏移量 Elf64_Word e_flags; // 处理器特定标志 Elf64_Half e_ehsize; // ELF头大小 Elf64_Half e_phentsize; // 每个程序头表条目的大小 Elf64_Half e_phnum; // 程序头表条目数量 Elf64_Half e_shentsize; // 每个节头表条目的大小 Elf64_Half e_shnum; // 节头表条目数量节的总数 Elf64_Half e_shstrndx; // 节名称字符串表的索引 } Elf64_Ehdr;2.2 核心结构 2节Section—— ELF 文件的 “数据容器”节是 ELF 文件存储数据和代码的基本单元不同类型的节负责不同的功能。我们最常用的节有以下几种节名称类型用途.text代码节存储编译后的机器指令程序的执行代码.data数据节存储已初始化的全局变量和局部静态变量.bss未初始化数据节为未初始化的全局变量和局部静态变量预留空间文件中不占空间加载到内存后分配空间并清零.rodata只读数据节存储只读数据如字符串常量、const 变量.symtab符号表存储函数名、变量名与对应地址的映射关系.rel.text重定位表存储.text 节中需要重定位的符号信息如未解析的函数调用.shstrtab节名称字符串表存储所有节的名称.strtab字符串表存储符号名称等字符串用readelf -S查看所有节以hello.o为例readelf -S hello.o输出结果关键节Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 00000040 0000000000000024 0000000000000000 AX 0 0 1 # 代码节AX可执行 [ 2] .data PROGBITS 0000000000000000 00000064 0000000000000000 0000000000000000 WA 0 0 1 # 数据节WA可写 [ 3] .bss NOBITS 0000000000000000 00000064 0000000000000000 0000000000000000 WA 0 0 1 # 未初始化数据节 [ 4] .rodata PROGBITS 0000000000000000 00000064 000000000000000d 0000000000000000 A 0 0 1 # 只读数据节字符串hello world!\n [ 5] .rel.text RELA 0000000000000000 00000078 0000000000000030 0000000000000018 I 9 1 8 # 重定位表.text节的重定位信息 [ 9] .symtab SYMTAB 0000000000000000 000000a8 0000000000000170 0000000000000018 10 12 8 # 符号表 [10] .strtab STRTAB 0000000000000000 00000218 000000000000005c 0000000000000000 0 0 1 # 字符串表 [12] .shstrtab STRTAB 0000000000000000 00000274 000000000000004f 0000000000000000 0 0 1 # 节名称字符串表关键节详解.text 节存储机器指令用objdump -d反汇编.text节查看编译后的机器码objdump -d hello.o输出hello.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 main: 0: f3 0f 1e fa endbr64 4: 55 push %rbp 5: 48 89 e5 mov %rsp,%rbp 8: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # f main0xf f: e8 00 00 00 00 callq 14 main0x14 # 调用printf地址暂为0需重定位 14: b8 00 00 00 00 mov $0x0,%eax 19: e8 00 00 00 00 callq 1e main0x1e # 调用run地址暂为0需重定位 1e: b8 00 00 00 00 mov $0x0,%eax 23: 5d pop %rbp 24: c3 retq可以看到printf和run的调用地址都是00 00 00 00这是因为编译时编译器不知道这两个函数的实际地址需要在链接时通过重定位表修正。1.symtab 节符号表存储符号信息符号表记录了函数、变量的名称、类型、地址等信息用readelf -s查看readelf -s hello.o输出中关键条目Symbol table .symtab contains 14 entries: Num: Value Size Type Bind Vis Ndx Name 6: 0000000000000000 37 FUNC GLOBAL DEFAULT 1 main # 全局函数main位于.text节Ndx1 12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts # 未定义符号putsprintf的实现 13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND run # 未定义符号runNdxUND表示该符号在当前目标文件中未定义需要从其他目标文件或库中查找重定位的对象。2.rel.text 节重定位表记录需要修正的地址重定位表存储了.text节中需要重定位的符号位置用readelf -r查看readelf -r hello.o输出Relocation section .rel.text at offset 0x78 contains 2 entries: Offset Info Type Sym. Value Sym. Name Addend 0000000000000010 00000c0200000004 R_X86_64_PLT32 0000000000000000 puts - 4 000000000000001a 00000d0200000004 R_X86_64_PLT32 0000000000000000 run - 4这表示在.text节偏移0x10处需要重定位符号puts在.text节偏移0x1a处需要重定位符号run链接器会根据这些信息找到puts来自 C 标准库和run来自code.o的实际地址替换掉原来的00 00 00 00。2.3 核心结构 3节头表Section Header Table—— 节的 “索引表”节头表是所有节的 “索引表”每个条目对应一个节记录了该节的名称、类型、大小、偏移量、标志等信息。链接器如 ld主要通过节头表来操作各个节如合并、重定位。节头表的位置由 ELF 头中的e_shoff字段指定每个条目的大小由e_shentsize指定条目数量由e_shnum指定。内核中的节头表结构体typedef struct elf64_shdr { Elf64_Word sh_name; // 节名称在.shstrtab中的索引 Elf64_Word sh_type; // 节类型如PROGBITS、SYMTAB等 Elf64_Xword sh_flags; // 节标志如可执行、可写、只读 Elf64_Addr sh_addr; // 节在内存中的虚拟地址加载后 Elf64_Off sh_offset; // 节在文件中的偏移量 Elf64_Xword sh_size; // 节的大小字节 Elf64_Word sh_link; // 关联的其他节的索引如重定位表关联符号表 Elf64_Word sh_info; // 额外信息如重定位表关联的节索引 Elf64_Xword sh_addralign; // 节在内存中的对齐方式如8字节对齐 Elf64_Xword sh_entsize; // 节中每个条目的大小如符号表条目大小 } Elf64_Shdr;2.4 核心结构 4程序头表Program Header Table—— 加载的 “说明书”程序头表仅存在于可执行文件和动态库中它是操作系统加载 ELF 文件的 “说明书”—— 描述了如何将 ELF 文件的内容加载到内存中分成哪些段Segment每个段的权限可读、可写、可执行等。用readelf -l查看程序头表以可执行文件a.out由hello.o和code.o链接生成为例# 先链接生成可执行文件 gcc hello.o code.o -o a.out # 查看程序头表 readelf -l a.out输出结果关键字段Elf file type is EXEC (Executable file) Entry point 0x4004e0 # 程序入口地址 There are 9 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x00000000000001f8 0x00000000000001f8 R E 8 INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] # 动态链接器路径 LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x0000000000000744 0x0000000000000744 R E 200000 # 代码段R E只读、可执行 LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 0x0000000000000218 0x0000000000000220 RW 200000 # 数据段RW可读写 DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28 0x00000000000001d0 0x00000000000001d0 RW 8 NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254 0x0000000000000044 0x0000000000000044 R 4 GNU_EH_FRAME 0x00000000000005a0 0x00000000004005a0 0x00000000004005a0 0x000000000000004c 0x000000000000004c R 4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 10 GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 0x00000000000001f0 0x00000000000001f0 R 1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 08 .init_array .fini_array .jcr .dynamic .got关键字段详解Type段类型LOAD需要加载到内存的段最核心的类型。INTERP指定动态链接器的路径如/lib64/ld-linux-x86-64.so.2。DYNAMIC动态链接相关的信息如依赖的动态库、重定位信息。GNU_STACK栈的权限设置RW 表示栈可读写。Flags段权限R只读如代码段、只读数据段。W可写如数据段、栈段。E可执行如代码段。Section to Segment mapping显示每个段由哪些节合并而成。例如第一个LOAD段R E由.text、.rodata等节合并而成是程序的代码和只读数据部分。第二个LOAD段RW由.data、.bss、.got等节合并而成是程序的可读写数据部分。三、ELF 的生命周期从源码到加载的完整流程理解了 ELF 的核心结构后我们再来看 ELF 文件的完整生命周期源码 → 目标文件 → 可执行文件 → 加载到内存运行。3.1 阶段 1ELF 形成可执行文件 —— 目标文件的 “合并与重定位”将多个目标文件.o和库文件合并成可执行文件主要完成 3 件事节合并、符号解析、地址重定位。步骤 1目标文件的节合并链接器会将所有输入目标文件的同名节合并成一个新的节所有.text节合并成一个新的.text节存储所有代码。所有.data节合并成一个新的.data节存储所有已初始化数据。所有.bss节合并成一个新的.bss节存储所有未初始化数据。合并过程示意图步骤 2符号解析链接器会收集所有目标文件和库文件中的符号函数名、变量名建立全局符号表然后解析每个未定义符号NdxUND对于用户定义的符号如run函数从其他目标文件中查找对应的定义。对于库符号如puts函数从依赖的库如 C 标准库libc.so中查找对应的定义。步骤 3地址重定位这是链接过程的核心 —— 修正所有未定义符号的地址。以hello.o中的callq run为例合并后run函数在新的.text节中的地址是0x400529假设。链接器根据.rel.text重定位表的信息找到callq run指令的位置偏移0x1a。将原来的00 00 00 00替换为run函数的实际地址相对偏移。重定位后的机器码objdump -d a.out | grep -A 10 main输出0000000000400506 main: 400506: f3 0f 1e fa endbr64 40050a: 55 push %rbp 40050b: 48 89 e5 mov %rsp,%rbp 40050e: 48 8d 3d 0f 00 00 00 lea 0xf(%rip),%rdi # 400524 main0x1e 400515: e8 0a 00 00 00 callq 400524 putsplt # puts的实际地址 40051a: b8 00 00 00 00 mov $0x0,%eax 40051f: e8 05 00 00 00 callq 400529 run # run的实际地址 400524: b8 00 00 00 00 mov $0x0,%eax 400529: 5d pop %rbp 40052a: c3 retq可以看到callq指令的地址已经被修正为实际的函数地址。3.2 阶段 2ELF 可执行文件加载 —— 从磁盘到内存的 “变身”可执行文件生成后双击或在终端运行时操作系统会将其加载到内存中并执行。这个过程的核心是将 ELF 文件的段Segment加载到进程的虚拟地址空间并完成初始化。3.2.1 加载的核心原则Section 合并为 SegmentELF 文件中的“节”Section是链接器的操作单位而“段”Segment是操作系统加载的操作单位。加载时操作系统会根据程序头表的描述将属性相同的节合并成一个 Segment然后加载到内存中。合并原则可读 可执行R E如.text、.rodata等节合并成代码段。可读 可写RW如.data、.bss、.got等节合并成数据段。这样做的目的是减少内存碎片内存分配的基本单位是页通常 4KB合并后可以减少占用的内存页数。例如.text节 4097 字节 .init节 512 字节 4609 字节合并后占用 2 个页8KB如果不合并会占用 3 个页12KB。统一权限管理同一 Segment 的所有内容拥有相同的访问权限方便操作系统进行内存保护如代码段只读防止被篡改。3.2.2 加载的完整流程以a.out的加载为例完整流程如下创建进程操作系统创建一个新的进程分配 PID、进程控制块task_struct等。分配虚拟地址空间为进程分配虚拟地址空间划分出代码区、数据区、堆区、栈区、共享库区等。读取 ELF 头和程序头表操作系统读取a.out的 ELF 头找到程序头表的位置解析每个LOAD段的信息。加载 Segment 到内存对于代码段R E将文件中对应的内容读取到虚拟地址空间的代码区设置权限为 “只读 可执行”。对于数据段RW将文件中对应的内容读取到虚拟地址空间的数据区设置权限为 “可读 可写”对于.bss节分配内存并清零.bss在文件中不占空间。初始化动态链接如果可执行文件依赖动态库如libc.so操作系统会启动动态链接器ld-linux.so加载依赖的动态库完成符号解析和重定位。设置程序入口将 CPU 的程序计数器PC指向 ELF 头中e_entry字段指定的入口地址如0x4004e0程序开始执行。3.2.3 虚拟地址空间与 ELF 加载的关系现代操作系统采用 “虚拟地址空间” 机制每个进程都有独立的虚拟地址空间与物理内存通过页表映射。ELF 文件在编译时就已经进行了 “统一编址”—— 即文件中的代码和数据已经分配了虚拟地址加载时只需将这些虚拟地址映射到物理内存即可。例如a.out的代码段虚拟地址是0x400000加载时操作系统会将这个虚拟地址范围映射到物理内存的某个区域进程执行时直接使用0x400000开始的虚拟地址访问代码。用readelf -h查看可执行文件的入口地址readelf -h a.out | grep Entry point # 输出Entry point address: 0x4004e0这个地址就是程序开始执行的虚拟地址对应_start函数C 运行时库的入口函数_start函数会初始化栈、调用动态链接器、最终调用main函数。3.2.4 加载后的内存布局以 64 位 Linux 系统为例a.out加载后的虚拟地址空间布局大致如下高地址 ------------------------ | 内核空间内核代码/数据 | ------------------------ | 命令行参数/环境变量 | ------------------------ | 栈区向下增长 | ------------------------ | 共享库区 | # 动态库如libc.so加载区域 ------------------------ | 堆区向上增长 | ------------------------ | 数据段.data、.bss等 | # 虚拟地址0x600000左右 ------------------------ | 代码段.text等 | # 虚拟地址0x400000左右 ------------------------ | 只读数据段.rodata等 | ------------------------ 低地址四、实战用工具分析 ELF 文件 —— 亲手拆解 ELF 黑盒掌握了 ELF 的理论后我们用常用工具实战分析 ELF 文件加深理解。4.1 常用工具清单工具功能常用命令readelf查看 ELF 文件的详细信息ELF 头、节、程序头表等readelf -hELF 头、readelf -S节、readelf -l程序头表、readelf -s符号表objdump反汇编 ELF 文件查看机器码objdump -d反汇编.text 节、objdump -s查看节内容file查看文件类型file a.outldd查看可执行文件依赖的动态库ldd a.outsize查看 ELF 文件各节的大小size a.out4.2 实战 1分析目标文件的重定位信息以hello.o为例查看重定位表和符号表理解重定位的必要性# 1. 查看符号表找到未定义符号 readelf -s hello.o | grep UND # 输出 # 12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts # 13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND run # 2. 查看重定位表找到需要修正的地址 readelf -r hello.o # 输出 # Relocation section .rel.text at offset 0x78 contains 2 entries: # Offset Info Type Sym. Value Sym. Name Addend # 0000000000000010 00000c0200000004 R_X86_64_PLT32 0000000000000000 puts - 4 # 000000000000001a 00000d0200000004 R_X86_64_PLT32 0000000000000000 run - 4 # 3. 反汇编查看未重定位的机器码 objdump -d hello.o | grep -A 5 callq # 输出 # f: e8 00 00 00 00 callq 14 main0x14 # puts的调用地址为0 # 14: b8 00 00 00 00 mov $0x0,%eax # 19: e8 00 00 00 00 callq 1e main0x1e # run的调用地址为04.3 实战 2分析可执行文件的节合并与地址重定位链接生成a.out后查看合并后的节和重定位后的地址# 1. 查看合并后的.text节大小 readelf -S a.out | grep -A 1 .text # 输出 # [11] .text PROGBITS 00000000004004e0 000004e0 # 0000000000000056 0000000000000000 AX 0 0 1 # 2. 查看符号表中已定义的符号地址 readelf -s a.out | grep -E main|run|puts # 输出 # 59: 0000000000400506 37 FUNC GLOBAL DEFAULT 11 main # 60: 0000000000400529 6 FUNC GLOBAL DEFAULT 11 run # 61: 0000000000000000 0 FUNC GLOBAL DEFAULT UND putsGLIBC_2.2.5 (2) # 3. 反汇编查看重定位后的机器码 objdump -d a.out | grep -A 10 main # 输出 # 0000000000400506 main: # 400506: f3 0f 1e fa endbr64 # 40050a: 55 push %rbp # 40050b: 48 89 e5 mov %rsp,%rbp # 40050e: 48 8d 3d 0f 00 00 00 lea 0xf(%rip),%rdi # 400524 main0x1e # 400515: e8 0a 00 00 00 callq 400524 putsplt # puts的实际地址 # 40051a: b8 00 00 00 00 mov $0x0,%eax # 40051f: e8 05 00 00 00 callq 400529 run # run的实际地址可以看到main和run的地址已经被分配0x400506和0x400529callq指令的地址也被修正为实际地址。4.4 实战 3分析动态库的 ELF 结构动态库.so也是 ELF 文件类型为DYN共享目标文件我们以 C 标准库libc.so.6为例# 1. 查看动态库的ELF类型 file /lib/x86_64-linux-gnu/libc.so.6 # 输出/lib/x86_64-linux-gnu/libc.so.6: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked # 2. 查看动态库的程序头表 readelf -l /lib/x86_64-linux-gnu/libc.so.6 | grep LOAD # 输出 # LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 # 0x00000000001e68d8 0x00000000001e68d8 R E 1000 # LOAD 0x00000000001e74e0 0x00000000003e74e0 0x00000000003e74e0 # 0x0000000000008f48 0x000000000000b3e8 RW 1000 # 3. 查看动态库中的符号如printf readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep -E printf$ # 输出 # 1731: 000000000005f860 61 FUNC GLOBAL DEFAULT 13 printf五、ELF 文件的核心作用 —— 为什么它能成为 Linux 的 “标准”ELF 文件之所以能成为 Linux/Unix 系统的标准可执行文件格式核心在于它的灵活性和统一性统一格式目标文件、可执行文件、动态库、核心转储文件都采用 ELF 格式工具如 readelf、objdump可以统一处理降低开发和维护成本。跨平台兼容ELF 支持 32 位 / 64 位架构、不同 CPUx86、ARM、RISC-V 等只需编译时指定目标架构即可生成对应的 ELF 文件。支持动态链接ELF 的程序头表、GOT全局偏移表、PLT过程链接表等结构完美支持动态链接实现了代码共享和模块化复用。调试友好ELF 保留了符号表、重定位表等调试信息调试器如 gdb可以通过这些信息实现断点调试、变量查看等功能。总结ELF 文件看似复杂但只要抓住 “结构→流程→工具” 三个核心就能逐步拆解它的神秘面纱。希望这篇文章能帮助你从 “会用” 到 “懂原理”在 C/C 开发和 Linux 系统学习的道路上更上一层楼。如果你在实际操作中遇到了 ELF 相关的问题如链接错误、动态库加载失败欢迎在评论区交流 也可以尝试用本文介绍的工具分析自己的程序加深对 ELF 原理的理解