ELF文件结构与项目实现

新发布LinkerloaderELF文件结构
2026-05-19 08:09
1222

ELF 三视图

同一个 ELF 文件,需要从三个不同的视角去理解。这三个视角分别是:

  • 文件视图
  • 内存视图
  • 动态链接视图
    文件视图回答“这个 ELF 在磁盘里怎么组织”;内存视图回答“这个 ELF 被加载后怎么摆放”;动态链接视图回答“这个 ELF 运行时怎么和外部库协作”。三者看的是同一个文件,但解决的是三类完全不同的问题。

image.png

文件视图

文件视图关心的是:ELF 在文件里是怎么存的
这个视角下最重要的对象是 Section Header Table (节头表)和各类 section。
常见 section 包括:

  • .text:程序代码
  • .rodata:只读常量
  • .data:已初始化全局变量
  • .bss:未初始化全局变量
  • .symtab / .dynsym:符号表
  • .strtab / .dynstr:字符串表
  • .rel.* / .rela.*:重定位表
    文件视图特别适合查看:这个 ELF 里有哪些 section,某个 section 在文件中的偏移是多少,符号表、字符串表、重定位表分别放在哪里,strip 前后文件内部结构发生了什么变化。也就是我们使用IDA,objump的视角。它重点让我们认识某个数据在文件中是属于什么类别
    文件视图的核心结构是:
  • ELF Header:总导航,我们节头表在哪
  • Section Header Table:节目录
  • 各类 section:真实承载代码、数据、符号、字符串等内容

内存视图

内存视图关心的是:程序运行时,ELF 被加载器怎么映射到内存中
这个视角下最重要的对象是 Program Header Table
常见的程序头项包括:

  • PT_LOAD:可装载段
  • PT_INTERP:动态链接器路径
  • PT_DYNAMIC:动态链接所需信息
  • PT_NOTE:附加说明信息
  • PT_GNU_STACK:栈权限
  • PT_GNU_RELRO:重定位后只读区域
    内存视图是 ELF 被加载执行时的视角,主要帮助加载器理解文件中哪些区域要映射到进程虚拟内存。它的核心索引是程序头表,程序头表中的每个程序头项描述一个 segment,例如 PT_LOAD 段会被映射到内存中,并带有虚拟地址、文件偏移、内存大小和读写执行权限。
    内存视图的核心结构:
  • ELF Header:给出程序头表位置
  • Program Header Table:程序头表
  • 各类 segment:真正参与装载的内存块

动态链接视图

动态链接视图关心的是:程序运行时如何解析外部符号、如何连接共享库、如何修正地址
这个视角下最重要的对象包括:

  • Dynamic Segment动态段
  • Dynamic Symbol Table动态符号表
  • Relocation重定位表
  • GOT全局偏移表 存放真实函数地址的数据表
  • PLT过程链接表 调用外部函数时的跳板代码
    动态链接视图是 ELF 在运行时与共享库协作的视角,主要帮助动态链接器理解程序依赖哪些外部库、哪些符号需要解析、哪些地址需要重定位。它的核心索引是动态段 .dynamic,动态段中的各项会描述动态符号表、字符串表、重定位表、GOT、PLT 以及依赖的共享库等信息,使动态链接器能够在程序运行时完成外部函数地址解析与跳转。
    动态链接视图的核心结构
  • PT_INTERP:指定动态链接器路径
  • PT_DYNAMIC / .dynamic:动态链接说明书
  • .dynsym / .dynstr:动态符号和字符串
  • .rel.* / .rela.*:重定位
  • .got / .plt:运行时跳转与延迟绑定

三个视图间的运作关系

这三个视图不是孤立的,它们是一层一层衔接起来的。
最上层的入口是 ELF Header。它先告诉我们:

  • 文件是不是 ELF
  • 是 32 位还是 64 位
  • 是小端还是大端
  • 程序头表在哪
  • 节头表在哪
    接下来:
  • 如果我们顺着 e_shoff 去读,就进入 文件视图
  • 如果我们顺着 e_phoff 去读,就进入 内存视图
  • 如果我们在程序头表里继续找到 PT_DYNAMICPT_INTERP,就进入 动态链接视图
    三视图之间的关系并不是“互相竞争”,而是“互相接力”:
  1. ELF Header 负责总导航
  2. Section Header Table 负责解释文件内部怎么分类
  3. Program Header Table 负责解释文件如何被装进内存
  4. Dynamic Segment 负责解释程序如何在运行时完成动态链接
    换句话说,文件视图时静态结构图,内存视图是运行时布局图,动态链接视图是运行时协作图

image.png

ELF Header

ELF Header就是ELF文件开头的一小段“总控表”
我们后面要解析 Program Header、Section Header、动态段,第一步都要靠它给出的偏移和数量去定位。
ELF文件已经在elf.h共享库文件里面定义[[Linux文件结构#usr#include]]
readelf -h first-arm查看文件ELF Header
image.png
我们在010editor里面开启ELF.bat模板,查看二进制文件的结构树
image.png

[!tip]
Addr 表示地址,也就是虚拟地址
在ELF里:

  • Elf32_Addr 是 4字节
  • Elf64_Addr 是 8字节
    常见字段:
  • e_entry
  • sh_addr
  • p_vaddr
    它的含义是: 程序装载到内存后,这个东西在内存中的地址是多少。

Off 表示偏移,通常指文件偏移
在ELF里:

  • Elf32_off 是 4字节
  • Elf64_off 是 8字节
    常见字段:
  • e_phoff
  • e_shoff
  • p_offset
  • sh_offset

它的含义是:
这个结构或数据在ELF文件里的位置是多少
Half 表示 半字,通常是 2 字节
在 ELF 里一般对应:
- Elf32_Half
- Elf64_Half
底层通常就是:

  • uint16_t
    常用来表示:
  • 类型编号
  • 数量
  • 索引
  • 小范围标志值

Word表示,通常是4字节
在ELF里一般对应:

  • Elf32_Word
  • Elf64_Word
    底层通常是:
  • uint32_t
    常用来表示:
  • 标志
  • 计数
  • 版本
  • section类型等

e_ident

Magic
e_ident[EI_MAG0~EI_MAG3]
该字段是 ELF 文件的魔数标识,固定为 0x7F 'E' 'L' 'F',用于告诉加载器和分析工具“这就是一个 ELF 文件”。 如果这 4 个字节不对,系统和逆向工具通常都会直接认为它不是合法 ELF。
图中对应的是:

  • file_identification[0] = 0x7F
  • file_identification[1] = 'E'
  • file_identification[2] = 'L'
  • file_identification[3] = 'F'
    Class
    e_ident[EI_CLASS]
    该字节用于标识 ELF 的位数类型,也就是这是 32 位 ELF 还是 64 位 ELF。
    常见取值:
  • ELFCLASS32 (1):32 位
  • ELFCLASS64 (2):64 位
    Data
    e_ident[EI_DATA]
    该字节用于标识 ELF 文件采用的数据编码方式,也就是大小端序
    常见取值:
  • ELFDATA2LSB (1):小端
  • ELFDATA2MSB (2):大端
    Version
    e_ident[EI_VERSION]
    该字节用于标识 ELF 文件头版本,通常应为:
  • EV_CURRENT / E_CURRENT (1)
    它表示当前使用的是 ELF 的标准版本。 正常 ELF 文件这里一般都是 1,如果该字段不是当前版本值,加载器或分析工具可能会认为该 ELF 头不规范或存在异常。
    OS/ABI
    e_ident[EI_OSABI]
    该字节用于标识 ELF 文件面向的目标 ABI / 操作系统环境。
    常见取值包括:
  • ELFOSABI_NONE (0):通常表示 System V / 通用默认 ABI
  • 也可能见到 Linux、FreeBSD 等其他 ABI 标识
    在很多 Android 和 Linux 场景下,这个字段即使是 0 也完全正常,因为很多工具链默认就写成 ELFOSABI_NONE。 它更多是“说明目标 ABI 环境”的辅助信息,而不是决定文件能不能运行的唯一依据。
    ABIVersion
    e_ident[EI_ABIVERSION]
    该字节用于标识 ABI 的具体版本号。 如果 OSABI 没有特别指定复杂 ABI,这里通常就是 0。
    在大多数普通 ELF 分析中,这个字段存在感不高,但它仍属于 ELF 身份信息的一部分。
    Pad
    e_ident[EI_PAD]
    这是保留填充字段,用来把 e_ident 补齐到固定长度。 这些字节通常都是 0,主要作用是保留扩展空间和保持结构对齐。 它本身一般不承载实际业务语义,但如果这里出现异常值,有时也可以作为样本是否被篡改的辅助参考。
    **Nident
    e_ident[EI_NIDENT]
    EI_NIDENT 更常表示 e_ident 这个数组的总长度常量,标准大小是 16 字节。
    e_ident 整体就是 ELF 头最前面的 16 字节身份信息区。
    它包含了前面整体

e_type

该字段用于标识 ELF 文件的目标文件类型,也就是这个 ELF 到底是可重定位文件、可执行文件、共享库还是核心转储文件。
常见取值:

  • ET_REL:可重定位文件,通常是 .o
  • ET_EXEC:可执行文件
  • ET_DYN:共享对象文件,常见于 .so,也可能是 PIE 可执行文件
  • ET_CORE:核心转储文件
    在现代 Linux / Android 环境下,很多开启了 PIE 的可执行文件在头里也会显示成 ET_DYN,所以看到 ET_DYN 不一定就代表它一定是传统意义上的 .so 动态库,还要结合程序头、入口点和装载方式一起分析。

e_machine

该字段用于标识 ELF 面向的目标架构,也就是 CPU 指令集类型。
常见取值:

  • EM_386:x86
  • EM_X86_64:x86_64
  • EM_ARM:32 位 ARM
  • EM_AARCH64:64 位 ARM
  • EM_MIPS:MIPS

e_version

该字段用于标识 ELF 文件版本,通常应为:

  • EV_CURRENT (1)
    它和前面的 e_ident[EI_VERSION] 类似,都是说明当前 ELF 使用的是标准定义的当前版本。
    正常情况下这里几乎总是 1。
    如果该字段不是标准值,通常意味着这个 ELF 文件头可能被破坏、伪造,或者不符合常规规范。

e_entry

该字段表示 ELF 文件的入口点虚拟地址,也就是程序开始执行时第一条指令所在的位置。
图中该字段的值是:0x000005C1
对于可执行文件或可装载对象来说,这个值很关键,因为它告诉加载器“程序应该从哪里开始跑”。

e_phoff

该字段表示 程序头表 Program Header Table 在文件中的偏移
图中该字段的值是:52,也就是说,从文件开头偏移 52 字节的位置开始,就是程序头表。

e_shoff

该字段表示 节头表 Section Header Table 在文件中的偏移
图中该字段的值是:7012,也就是说,从文件开头偏移 7012 字节的位置开始,就是节头表。

e_flags

该字段表示 处理器相关标志位
图中该字段的值是:

  • 83887104
    这个字段的具体含义依赖于目标架构。
    也就是说,在 ARM、MIPS、RISC-V 等不同架构下,e_flags 的解释方法不一样。
    在 ARM ELF 中,它常常携带一些与 ABI、浮点、指令集特性相关的信息。
    因此分析时不能只看十进制值本身,而要结合对应架构规范进一步拆解。

e_ehsize

该字段表示 ELF Header 本身的大小,单位是字节。
图中该字段的值是:52,这说明这个 ELF 文件头长度是 52 字节。
对于 32 位 ELF 来说,这个值通常就是常见的标准大小。
它的作用是明确告诉解析器:ELF 头到这里结束,后面的结构从哪里开始继续读。

e_phentsize

该字段表示 每个程序头表项的大小,单位是字节。
图中该字段的值是:32也就是说,程序头表中每一个 Program Header Entry 占 32 字节。
有了这个值,再配合 e_phnum,就能算出整个程序头表的总大小。

e_phnum

该字段表示 程序头表项的数量
图中该字段的值是:

  • 9
    这意味着当前 ELF 一共有 9 个程序头项。
    程序头项通常包括:
  • PT_LOAD
  • PT_DYNAMIC
  • PT_INTERP
  • PT_NOTE
  • PT_GNU_STACK
    这些项决定了程序如何被装载进内存,因此 e_phnum 直接反映了加载层面的结构复杂度。

e_shentsize

该字段表示 每个节头表项的大小,单位是字节。
图中该字段的值是:

  • 40
    也就是说,每个 Section Header Entry 占 40 字节。
    配合 e_shnum,就可以计算整个节头表的大小范围。

e_shnum

该字段表示 节头表项的数量
图中该字段的值是:

  • 29
    这意味着当前 ELF 一共有 29 个 section。
    这些 section 可能包括:
  • .text
  • .data
  • .bss
  • .rodata
  • .dynsym
  • .dynstr
  • .rel.plt
    逆向分析时,e_shnum 可以帮助我们快速判断这个文件的 section 组织规模。

e_shstrndx

该字段表示 节名字字符串表在节头表中的索引

Program_header_table(程序头表)

如果说 ELF Header 是整个 ELF 的总导航,那么 Program Header Table 就是 ELF 的装载清单
它不关心“文件里有哪些漂亮的 section 名字”,它只关心一件事:ELF 运行时,哪些内容要被加载到内存,加载到哪里,权限是什么。
我们把程序头表分为三层,程序头项是程序头表中的单个表项,而程序头字段是组成该表项的成员;这些字段共同定义了对应段的类型、位置、大小和权限。
image.png

Program Header Entry

程序头表里的每一条记录,叫一个 Program Header Entry,也就是程序头项。每个程序头项都描述一个segment的装载规则,一条程序头项,对应一个 segment。但程序头项不是 segment 本体,程序头项是对这个 segment 的描述记录。

程序头项的通用字段

在不同的程序头项里,最常见的字段基本是一套固定结构。
p_type表示这一项是什么类型的段。这是判断该程序头项“在干什么”的第一入口。常见值包括:
PT_LOAD表示一个真正会被加载进内存的段。
PT_INTERP表示解释器路径,通常就是动态链接器路径。
PT_DYNAMIC表示动态段,这是进入动态链接视图的核心入口。
PT_NOTE表示附加说明信息段。
PT_GNU_STACK表示栈的权限要求。
PT_GNU_RELRO表示一块在重定位完成后会变成只读的区域。
p_offset表示这段内容在文件中的偏移。
p_vaddr表示这段内容装载到内存后的虚拟地址。
p_paddr表示物理地址。在大多数普通 Linux 用户态分析里,这个字段通常不太关键,更多见于某些特定平台或底层场景。
p_filesz表示该段在文件中实际占用的字节数。
p_memsz表示该段装载到内存后占用的字节数。如果 p_memsz > p_filesz,通常说明还有一部分内容需要在内存中补零,比如 .bss
p_flags 表示该段在内存中的权限。
p_align决定该段在文件和内存中的对齐方式。

Section_Header_table(节头表)

Section Header Table 是一张表。这张表主要给链接器、静态分析器和逆向工具看,而不是给加载器看。它的作用是描述每个 section 的名字、类型、偏移和大小,各个表之间怎么互相索引。它就相当于ELF 的文件内部目录。
image.png
和程序头表一样,它也是三层结构。节头表,节头项和字段。

Section Header Entry

程序头表里的每一条记录,叫一个 Section Header Entry,也就是节头项。每个程序头项都描述一个section的装载规则,一条节头项,对应一个 section。但节头项不是 section 本体,节头项是对这个 section 的描述记录。
image.png

节头项的通用字段

不同的 section 类型虽然用途不同,但它们的节头项大体都共享一套字段结构。
sh_name
表示该 section 名字在节名字字符串表中的偏移。
sh_type
表示该 section 的类型。这是判断这个 section “属于哪类数据”的第一入口。
常见值包括:
- SHT_PROGBITS:表示该 section 的内容真实存放在文件里,常见如 .text.rodata.data.plt
- SHT_NOBITS:表示该 section 在文件中通常不真正占据内容,只在内存中分配空间,典型就是 .bss
- SHT_SYMTAB:普通符号表,对静态分析很重要,通常保存更完整的函数名、变量名和本地符号
- SHT_DYNSYM:动态符号表,主要保存动态链接阶段需要解析的符号,常和 .dynstr 配合使用
- SHT_STRTAB:字符串表,主要保存名字字符串,常见有 .strtab.dynstr.shstrtab
- SHT_REL:重定位表,不显式保存 addend,常见如 .rel.dyn.rel.plt
-
- SHT_RELA:重定位表,显式保存 addend,常见于很多 64 位 ELF,如 .rela.dyn.rela.plt
sh_flags
表示该 section 的属性标志,常见用来说明:是否可写、是否会被装载到内存、是否可执行
sh_addr
表示该 section 装载到内存后的地址,它是内存视角下的值,不是文件偏移。
sh_offset
表示该 section 在文件中的偏移。表示这个 section 在 ELF 文件里从哪开始。
sh_size
表示该 section 的大小。
sh_link
表示该 section 与其他 section 的关联索引,这个字段常用于符号表、字符串表、重定位表之间的关联。
sh_info
表示补充信息,具体含义要看 section 类型,不同 section 的解释方式可能不一样。
sh_addralign
表示该 section 的对齐要求。
sh_entsize
表示该 section 中每个表项的大小,如果该 section 是一个“表”,比如符号表、重定位表,这个字段就很有意义。

常见 section 类型

学习节头表时,最重要的不是把所有 sh_type 都背下来,而是先把最常见、最有分析价值的 section 看懂。
.text
.text 是代码 section,通常存放程序的机器指令。
.rodata
.rodata 是只读数据 section,通常存放字符串常量、格式化字符串、查表数据等。
.data
.data 是已初始化全局变量 section,通常存放程序启动前就有明确初始值的数据。
.bss
.bss 是未初始化全局变量 section。
.symtab
.symtab 是普通符号表,也可以理解成更完整的符号表。
.dynsym
.dynsym 是动态符号表,主要保存动态链接阶段需要的符号。
.strtab
.strtab 是普通字符串表,经常用来存放 .symtab 对应的符号名。
.dynstr
.dynstr 是动态字符串表,通常给:
- .dynsym
- .dynamic
提供名字恢复能力。
.rel. / .rela.**
这类 section 是重定位表。
.got / .got.plt
这两类 section 和全局偏移表、过程链接表密切相关。

Program Header 和 Section Header 的关系

一个 segment 往往包含多个 section。
例如一个代码段可能同时覆盖:

  • .text
  • .rodata
  • .plt
  • .init
    一个数据段可能同时覆盖:
  • .data
  • .bss
  • .got
  • .dynamic
    多个 section 会按装载需求和权限,被归并到少数几个 segment 中。

String Table(字符串表)

字符串表本质上就是一块连续的字节区域,里面顺序存放了多个以 \0 结尾的字符串。ELF 里的很多结构并不会直接把名字写进去,而是只保存一个偏移值,真正的名字要去对应的字符串表里取。
所以看到:

  • sh_name
  • st_name
  • DT_NEEDED
    这类字段它保存的往往不是名字本身,而是指向某个字符串表的索引或偏移。

三类最常见的字符串表

.shstrtab
.shstrtab节名字符串表。它专门给节头表服务,用来恢复 section 的名字。
image.png
.strtab
.strtab普通字符串表。它最常见的作用,是给 .symtab 提供符号名字。
image.png
.dynstr
.dynstr动态字符串表。它主要给动态链接相关结构提供字符串名字。
image.png

字符串表和字段的对应关系

  • sh_name -> .shstrtab
  • st_name(普通符号)-> .strtab
  • st_name(动态符号)-> .dynstr
  • DT_NEEDED -> .dynstr
    所以字符串表不是“单独一张孤立的表”,而是 ELF 里各种名字恢复的基础设施。
    ELF 采用的是:结构里只保存偏移,字符串统一放到字符串表里。 这和很多二进制格式里的“索引 + 字符串池”思路是一样的。
    看字符串表时,先确认这是哪一类字符串表,再看是谁在引用它,最后根据偏移恢复真正字符串。
    例如:
    看 section 名字: Section Header Entry -> sh_name -> .shstrtab
    看普通符号名: .symtab -> st_name -> .strtab
    看动态符号或依赖库名: .dynsym / .dynamic -> st_name / DT_NEEDED -> .dynstr

Symbol Table(符号表)

符号表本质上是一张“名字与地址关系表”,它记录了程序中有意义的目标如: 函数、全局变量、外部引用、局部符号。符号表通常不会把名字完整写在表项里,而是通过 st_name 去对应的字符串表里恢复名字。
image.png

符号表里常见的关键字段

st_name
表示符号名字在字符串表中的偏移,它不直接等于名字本身。
st_value
表示符号值,很多时候可以理解成地址或地址相关值。在函数和全局对象分析里非常重要。
st_size
表示符号大小,对于函数、对象等都可能有意义。
st_info
表示符号类型和绑定方式:是函数还是对象、是本地还是全局
st_shndx
表示该符号所属的 section,这个字段有助于判断符号落在哪个区域里。

Dynamic Segment(动态段)

动态段本质上是一组 DT_* 标签项。它不是单纯的一块“存库名的地方”,而是给动态链接器的指引
动态链接器会根据它知道:依赖了哪些共享库、动态字符串表在哪、动态符号表在哪、重定位表在哪、GOT/PLT 所需入口信息在哪。
image.png

Dynamic 里最常见的关键项

DT_NEEDED
表示依赖的共享库,例如程序可能依赖:libc.so.6 -> libm.so.6。这些库名通常不是直接写死在这里,而是通过 .dynstr 恢复。
DT_STRTAB
表示动态字符串表的位置,动态链接过程中,很多名字恢复都离不开它。
DT_SYMTAB
表示动态符号表的位置,动态链接器需要依赖它去解析外部符号。
DT_REL / DT_RELA
表示重定位表位置,它告诉动态链接器:哪些地址需要在运行时被修正。
DT_PLTGOT
表示 GOT 相关入口地址,它和后面的 GOT/PLT 调用链关系非常紧密。
Dynamic Segment 最大的价值是让我们把动态链接的几张关键表串.dynstr.dynsym、重定位表、GOT / PLT起来:

GOT 与 PLT

GOT 是 Global Offset Table,也就是全局偏移表,也就是一张存地址槽位的表。这些槽位在运行时会被填成真正的目标地址。
PLT 是 Procedure Linkage Table,也就是过程链接表,也就是一组专门用来跳转到外部函数的跳板代码。 GOT:更像“地址表”,PLT:更像“跳板代码”。
程序在编译时,往往还不知道某些共享库函数最终会被加载到哪个地址。这些外部函数的真实地址,往往要等程序运行时才能确定。需要一套机制,让程序先“跳到一个中间层”,再由运行时把真实地址补上。这套机制就是 GOT 和 PLT。

调用外部函数时的大致过程

第一次调用时:

  • 程序先跳到 PLT
  • PLT 再通过 GOT 找地址
  • 此时 GOT 里可能还没有最终地址
  • 动态链接器介入,解析符号
  • 把真实地址写回 GOT
  • 再跳去真正的共享库函数
    后续调用:
    第一次修好之后,后续再调用同一个函数时,通常就直接通过 GOT 命中真实地址,不需要再完整走一遍解析流程。这就是我们常看到的:第一次调用触发 lazy binding,后续调用直接走已修好的 GOT。

GOT / PLT 相关的关键对象

.got
保存全局偏移表内容,也就是一组地址槽位。
.got.plt
通常和 PLT 配套,保存外部函数调用相关的地址槽位。
.plt
保存过程链接表跳板代码。
.rel.plt / .rela.plt
保存和 PLT 调用链相关的重定位信息。

模仿readelf做一个小的解析器

思路框架

ELF是一整块文件字节,第一步要做的是把整个文件读进内存,再使用Buffer结构保存文件内容指针;文件大小;文件路径。这样我们所有的解析逻辑都会基于这块内存工作,不需要反复的读写操作。第二步就是要做格式判断和格式分流,首先要做校验,先检查文件头,是不是.ELF;再根据[EI_CLASS]判断是32位还是64位。第三步解析ELF Header,通过它拿到全局信息。第四步就是通过ELF Header提供的偏移和数量解析节头表和程序头表。

typedef struct {
    unsigned char *data;
    size_t size;
    const char *path;
} Buffer;

源码

主函数:首先我们规定usage,要求参数必须是一个目标文件的文件路径。紧接着,使用read_file()函数把整个文件读到缓冲区中,确保以后解析都冲buffer缓冲区开始,range_ok()函数确保文件长度够长,它是一个保险函数,我们会多次用到它。memcmp函数确保是ELF结构的,也就是检查文件头是不是.ELF。然后判断格式是32位还是64位,分别根据不同的架构去到不同的函数进行解析。

int main(int argc, char **argv) {
    if (argc != 2) {
        fprintf(stderr, "usage: %s <elf-file>\n", argv[0]);
        return 1;
    }

    Buffer buf = read_file(argv[1]);
    if (!range_ok(buf.size, 0, EI_NIDENT)) {
        free(buf.data);
        fail("file too small");
    }

    if (memcmp(buf.data, ELFMAG, SELFMAG) != 0) {
        free(buf.data);
        fail("not an ELF file");
    }

    if (buf.data[EI_DATA] != ELFDATA2LSB) {
        free(buf.data);
        fail("only little-endian ELF is supported for now");
    }

    printf("File: %s\n\n", buf.path);
    if (buf.data[EI_CLASS] == ELFCLASS32) {
        inspect_32(&buf);
    } else if (buf.data[EI_CLASS] == ELFCLASS64) {
        inspect_64(&buf);
    } else {
        free(buf.data);
        fail("unknown ELF class");
    }

    free(buf.data);
    return 0;
}

read_file()函数,先获取文件的总大小,再分配同样大小的内存,一次性把文件读到内存中,返回一个buffer。

static Buffer read_file(const char *path) {
    FILE *fp = fopen(path, "rb");
    if (fp == NULL) {
        fail_errno("fopen");
    }

    if (fseek(fp, 0, SEEK_END) != 0) {
        fclose(fp);
        fail_errno("fseek end");
    }

    long raw_size = ftell(fp);
    if (raw_size < 0) {
        fclose(fp);
        fail_errno("ftell");
    }

    if (fseek(fp, 0, SEEK_SET) != 0) {
        fclose(fp);
        fail_errno("fseek set");
    }

    unsigned char *data = malloc((size_t)raw_size);
    if (data == NULL) {
        fclose(fp);
        fail("malloc failed");
    }

    size_t nread = fread(data, 1, (size_t)raw_size, fp);
    fclose(fp);
    if (nread != (size_t)raw_size) {
        free(data);
        fail("short read");
    }

    Buffer buf = {
        .data = data,
        .size = nread,
        .path = path,
    };
    return buf;
}

inspect_32() 是 32 位 ELF 的主解析流程,这个函数接收一个 Buffer 指针。Buffer 里已经保存好了整份 ELF 文件的内容、大小和路径,所以 inspect_32() 不再负责读文件,只负责解析 32 位 ELF。

static void inspect_32(const Buffer *buf) {
    if (!range_ok(buf->size, 0, sizeof(Elf32_Ehdr))) {
        fail("file too small for Elf32_Ehdr");
    }

    const Elf32_Ehdr *eh = (const Elf32_Ehdr *)buf->data;
    print_ident(eh->e_ident);
    printf("  Type:    %s\n", elf_type_name(eh->e_type));
    printf("  Machine: %s\n", machine_name(eh->e_machine));
    printf("  Entry:   0x%08" PRIx32 "\n", eh->e_entry);
    printf("  PHOff:   0x%08" PRIx32 " (%u entries)\n", eh->e_phoff, eh->e_phnum);
    printf("  SHOff:   0x%08" PRIx32 " (%u entries)\n", eh->e_shoff, eh->e_shnum);
    printf("\n");

这里先确认,从文件偏移 0 开始,至少能取出一个完整的 Elf32_Ehdr。紧接着把文件开头那段字节按 Elf32_Ehdr 的结构布局来读取。 从这里开始,eh->e_entryeh->e_phoff 这些字段才有意义。紧接着,打印ELF Header。
程序从下面开始转折:首先确保没有越界,然后把程序头表和节头表映射成数组,正式开始按表项遍历ELF结构。

if (!range_ok(buf->size, eh->e_phoff, (uint64_t)eh->e_phnum * eh->e_phentsize)) {
        fail("program header table outside file");
    }
    if (!range_ok(buf->size, eh->e_shoff, (uint64_t)eh->e_shnum * eh->e_shentsize)) {
        fail("section header table outside file");
    }

    const Elf32_Phdr *phdrs = (const Elf32_Phdr *)(buf->data + eh->e_phoff);
    const Elf32_Shdr *shdrs = (const Elf32_Shdr *)(buf->data + eh->e_shoff);

打印节头表和程序头表:遍历所有 Elf32_Phdr,打印每个 segment 的:类型、文件偏移、虚拟地址、文件大小、内存大小、权限、对齐值。遍历所有 Elf32_Shdr,打印每个 section 的:名字、类型、地址、偏移、大小、标志位
这里 sh_name 只是一个偏移,必须通过 safe_string()shstrtab 里把名字取出来。

const unsigned char *shstrtab = NULL;
    size_t shstrtab_size = 0;
    if (eh->e_shstrndx < eh->e_shnum) {
        const Elf32_Shdr *shstr = &shdrs[eh->e_shstrndx];
        if (range_ok(buf->size, shstr->sh_offset, shstr->sh_size)) {
            shstrtab = buf->data + shstr->sh_offset;
            shstrtab_size = shstr->sh_size;
        }
    }

    printf("Program Headers:\n");
    for (size_t i = 0; i < eh->e_phnum; ++i) {
        const Elf32_Phdr *ph = &phdrs[i];
        printf("  [%2zu] %-12s off 0x%08" PRIx32 " vaddr 0x%08" PRIx32
               " filesz 0x%08" PRIx32 " memsz 0x%08" PRIx32 " flags ",
               i, segment_type_name(ph->p_type), ph->p_offset, ph->p_vaddr,
               ph->p_filesz, ph->p_memsz);
        print_program_flags(ph->p_flags);
        printf(" align 0x%" PRIx32 "\n", ph->p_align);
    }
    printf("\n");

    printf("Section Headers:\n");
    for (size_t i = 0; i < eh->e_shnum; ++i) {
        const Elf32_Shdr *sh = &shdrs[i];
        const char *name = shstrtab ? safe_string(shstrtab, shstrtab_size, sh->sh_name) : "<no-shstrtab>";
        printf("  [%2zu] %-18s %-12s addr 0x%08" PRIx32 " off 0x%08" PRIx32
               " size 0x%08" PRIx32 " flags ",
               i, name, section_type_name(sh->sh_type), sh->sh_addr, sh->sh_offset, sh->sh_size);
        print_section_flags(sh->sh_flags);
        printf("\n");
    }
    printf("\n");

所有 section 里找到 .dynamic,然后把动态链接相关的信息逐项解析并打印出来。首先遍历所有section,从节头表 shdrs 里一个一个拿 section,找到动态节,然后检查是否越界。紧接着把当前的SHT_DYNAMIC 节对应的文件内容解释成 Elf32_Dyn[],再通过 sh_link 找到它关联的.dynstr。随后逐项遍历每个动态条目,打印它的 d_tag和 d_val;如果条目类型是 DT_NEEDED(依赖共享库)或 DT_SONAME(共享库自身名称),就进一步去字符串表中把偏移还原成真实字符串。遇到
DT_NULL(动态条目结束标记)时停止遍历。

for (size_t i = 0; i < eh->e_shnum; ++i) {
        const Elf32_Shdr *sh = &shdrs[i];
        if (sh->sh_type != SHT_DYNAMIC) {
            continue;
        }
        if (!range_ok(buf->size, sh->sh_offset, sh->sh_size)) {
            continue;
        }
        printf("Dynamic Section (%zu):\n", i);
        const Elf32_Dyn *dyns = (const Elf32_Dyn *)(buf->data + sh->sh_offset);
        size_t dyn_count = sh->sh_size / sizeof(Elf32_Dyn);
        const unsigned char *dynstr = NULL;
        size_t dynstr_size = 0;
        if (sh->sh_link < eh->e_shnum) {
            const Elf32_Shdr *strsh = &shdrs[sh->sh_link];
            if (range_ok(buf->size, strsh->sh_offset, strsh->sh_size)) {
                dynstr = buf->data + strsh->sh_offset;
                dynstr_size = strsh->sh_size;
            }
        }
        for (size_t j = 0; j < dyn_count; ++j) {
            printf("  [%2zu] %-14s 0x%08" PRIx32, j, dyn_tag_name(dyns[j].d_tag), dyns[j].d_un.d_val);
            if ((dyns[j].d_tag == DT_NEEDED || dyns[j].d_tag == 

试读结束,发布七天后转为公开

公开时间:2026年5月26日 08:09:25

提前解锁全文,需花费 50 积分

登录解锁全文
附件下载
  • 登录后可下载文章附件
  • 分享到

    参与评论

    0 / 200

    全部评论 0

    暂无人评论
    投稿
    签到
    联系我们
    关于我们