readelf —— 解析 ELF 文件结构的首选工具
readelf 专门解析 ELF 的各种结构(header、section、segment、符号表、动态节等),输出规整、最懂 ELF 本身。查"这文件是什么、有哪些段、符号在不在、依赖什么"用它。属 binutils,一般自带。
数据来源
- 来源对象:目标 ELF 文件本身的字节结构——ELF Header、Section Header Table、Program Header Table、各节内容(含 core 文件的 NOTE 段)。
- 采集方式:纯静态读文件并按 ELF 规范解析,不运行程序、不读
/proc、不依赖内核。 - 由此决定的特性:分析任何来路不明的二进制都安全(不会执行它);跨架构也能解析(如在 x86 上读 ARM 的 ELF);缺点是只能看"文件里静态写了什么",看不到运行时状态。
# 一般随 binutils 安装
yum install binutils / apt install binutils一、常用选项
readelf -h <file> # ELF Header(文件总览:类型、架构、入口、表位置)
readelf -l <file> # Program Header(段/Segment,加载视角)+ section→segment 映射
readelf -S <file> # Section Header(节,链接/调试视角)
readelf -s <file> # 符号表(.symtab + .dynsym)
readelf --dyn-syms <file># 只看动态符号表 .dynsym
readelf -d <file> # .dynamic 节:动态链接信息(依赖的库、rpath 等)
readelf -r <file> # 重定位信息
readelf -n <file> # NOTE 段(core 文件里存寄存器/PID/信号,见下)
readelf -x .rodata <file># 以十六进制 dump 指定节内容
readelf -p .comment <file># 以字符串 dump 指定节(看编译器版本)
readelf -a <file> # all,全都打印(信息量大)
readelf -W <file> # 宽行输出,不折断(配合其它选项,推荐)二、-h ELF Header
$ readelf -h ./cpu_demo
Magic: 7f 45 4c 46 02 01 01 00 ...
Class: ELF64
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Entry point address: 0x1180
Number of section headers: 31看点:Class(位数)、Type(REL/EXEC/DYN/CORE,PIE 是 DYN)、Machine(架构,交叉编译时确认没编错平台)、Entry point(入口,_start 不是 main)。
三、-S Section(节,链接/调试视角)
$ readelf -S ./cpu_demo
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[14] .text PROGBITS 0000... 1180 0x2a5 00 AX 0 0 16
[16] .rodata PROGBITS 0000... ... ... 00 A 0 0 8
[25] .symtab SYMTAB 0000... ... ... 18 26 ...
[28] .debug_info PROGBITS 0000... ... ... 00 0 0 1Flg:A=加载进内存、X=可执行、W=可写。.text是AX,.data是WA。- 有
.debug_*说明带-g编译;有.symtab说明没被 strip。 - 快速判断 core 分析能否成功:
readelf -S app | grep -E 'symtab|debug'有输出 = 符号在。
四、-l Program Header(段/Segment,加载视角)
这是内核加载程序时唯一看的表。下面是本仓库 cpu_demo 在 CentOS 7 上 readelf -l --wide 的真实输出:
Elf file type is EXEC (Executable file)
Entry point 0x4019a0
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000400040 0x0000000000400040 0x0001f8 0x0001f8 R E 0x8
INTERP 0x000238 0x0000000000400238 0x0000000000400238 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x006fc5 0x006fc5 R E 0x200000
LOAD 0x007dc8 0x0000000000607dc8 0x0000000000607dc8 0x000404 0x000848 RW 0x200000
DYNAMIC 0x007de8 0x0000000000607de8 0x0000000000607de8 0x000210 0x000210 RW 0x8
NOTE 0x000254 0x0000000000400254 0x0000000000400254 0x000044 0x000044 R 0x4
GNU_EH_FRAME 0x004ea0 0x0000000000404ea0 0x0000000000404ea0 0x00062c 0x00062c R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x007dc8 0x0000000000607dc8 0x0000000000607dc8 0x000238 0x000238 R 0x14.1 每列什么意思
| 列 | 含义 |
|---|---|
Type | 段类型(p_type),决定"运行时对这段字节做什么"——见下表 |
Offset | 段在文件里的起始偏移 |
VirtAddr | 段映射到内存的虚拟地址(PhysAddr 一般同值,普通程序不用管) |
FileSiz | 从文件里读多少字节 |
MemSiz | 在内存里占多少字节(MemSiz > FileSiz 的差额就是 .bss,加载时补零) |
Flg | 权限:R 读 / W 写 / E 执行 |
Align | 对齐要求(LOAD 段 0x200000=2MB,正是两个 LOAD 地址错开 2MB 的原因) |
4.2 9 个段的语义(每种 Type 是给运行时的一条不同指令)
| 段 | 本例的值 | 语义 = 运行时做什么 |
|---|---|---|
PHDR | R E, 0x40 | 程序头表自身的位置(给 ld.so 自举用) |
INTERP | /lib64/ld-linux-x86-64.so.2 | "先加载这个动态链接器"——证明是动态链接程序 |
LOAD #1 | 0x400000, R+E, FileSiz=MemSiz | **"mmap 这段为只读可执行" **:代码 + 常量 + 动态链接元数据 |
LOAD #2 | 0x607dc8, RW, MemSiz>FileSiz | **"mmap 这段为可读写" **:数据 + GOT + .bss |
DYNAMIC | 0x607de8, RW | **"动态链接的工作清单在这" **:依赖库、符号表、重定位表位置 |
NOTE | 0x400254, R | 元数据(build-id、ABI 标记);core 文件里则是寄存器/信号 |
GNU_EH_FRAME | 0x404ea0, R | 异常处理/栈展开索引(C++ 抛异常、gdb 回溯用) |
GNU_STACK | 全 0, RW(无 E) | "栈按这个权限设"——无 E 表示栈不可执行(防栈上代码注入,安全加固) |
GNU_RELRO | 0x607dc8, R | "重定位做完后,把这段 mprotect 成只读"(防 GOT 被覆写攻击) |
只有
LOAD真正决定"哪些字节进内存、什么权限";其余段是给 ld.so 的附加指令或索引(去哪找解释器、动态信息、怎么加固)。这就是 Segment 语义比"一堆 section 打包"丰富的地方。
4.3 用这份输出核对前面讲的三件事
Entry point 0x4019a0—— 正好落在.text(readelf -S里.text的 Address 就是0x4019a0)。注意它是_start附近,不是main(CRT 启动代码先跑)。- 两个 LOAD 隔 2MB:LOAD #1
VirtAddr=0x400000,LOAD #2VirtAddr=0x607dc8,Align=0x200000。权限不同(R+E vs RW)必须落在不同内存页,按 2MB 对齐错开——这解释了readelf -S里.gcc_except_table(0x406e8c)到.init_array(0x607dc8)的地址跳变。 - LOAD #2
MemSiz(0x848) > FileSiz(0x404):差的0x444就是.bss等NOBITS内容——文件里不占字节,内存里要占且清零。这是".bss特例"的段级证据。
4.4 底部:Section → Segment 映射(两种视角的桥梁)
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 .text .fini .rodata .eh_frame_hdr .eh_frame .gcc_except_table
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这张表是 readelf 自己算的(遍历每个 section,看它的地址范围落在哪个段内就归进去),把两种视角对起来。逐行读:
02(=LOAD #1,R+E) 一口气吞下从.interp到.gcc_except_table十几个只读/可执行的节——readelf -S里正是这批带A/AX的节。03(=LOAD #2,RW) 收纳.init_array … .data .bss这些可写节。.bss在这里,印证它虽不占文件、却要进内存段。- 一个 section 可同时属于多个段:
.dynamic同时出现在03(LOAD #2)、04(DYNAMIC)、08(GNU_RELRO)——因为这几个段的地址范围是重叠/包含关系(LOAD 负责映射它、DYNAMIC 把它当清单读、RELRO 事后把它设只读)。这说明段不是"瓜分"section,而是各自按用途框选,允许重叠。 00、07后面为空:PHDR、GNU_STACK不覆盖任何 section(前者指程序头表本身,后者只是个栈权限标记,不对应文件内容)。- 反过来,
readelf -S里Address=0x0的.symtab/.debug_*/.comment在这张表里一个都不出现——它们不属于任何段,运行时不加载。这是"不是所有 section 都进 segment"最直接的证明。
对照口诀:
readelf -S看全部节(含不加载的调试/符号);readelf -l看段 + 映射(只涉及会加载或有运行时用途的节)。两条命令并排看,ELF 的静态/运行双视角就完全打通了。
五、-d 动态依赖(看依赖哪些库)
$ readelf -d ./cpu_demo | grep NEEDED
0x0001 (NEEDED) Shared library: [libstdc++.so.6]
0x0001 (NEEDED) Shared library: [libc.so.6]
0x0001 (NEEDED) Shared library: [libpthread.so.0]NEEDED 就是运行时要加载的 .so(比 ldd 更"干净",只列直接依赖、且不执行程序,见 ldd)。
六、-n NOTE 段(core 文件分析)
core 文件的关键信息都在 NOTE 段(对应 core-dump 说的 PT_NOTE):
$ readelf -n core.1234
NT_PRSTATUS 进程状态 + 寄存器
NT_PRPSINFO 进程名、PID、UID
NT_SIGINFO 导致崩溃的信号
NT_FILE 内存映射的文件列表不用 gdb 也能先 readelf -n core.xxx 快速确认"是哪个程序、什么信号崩的"。
七、结合本仓库
make
readelf -h ./cpu_demo # 类型/架构
readelf -S ./cpu_demo | grep -E 'text|debug' # 确认有代码段和调试节
readelf -d ./cpu_demo | grep NEEDED # 依赖 libstdc++/libc/libpthread
readelf -l ./cpu_demo | grep interpreter # 动态链接器路径readelf 只解析结构、不反汇编。要看机器指令/混排源码用 objdump。
总结
readelf 是解析 ELF 结构的首选工具:纯静态读文件、按规范解析 ELF Header / Section / Segment / 符号表 / 动态节,不运行程序、不读 /proc、不依赖内核,所以分析来路不明或跨架构的二进制都安全,代价是只能看文件里静态写了什么、看不到运行时状态。核心是两条命令配合的双视角:
-S看全部节(含运行时不加载的调试/符号节,Flg 里 A=加载、X=执行、W=可写),-l看内核加载唯一认的 Program Header 和 section→segment 映射——只有 LOAD 段真正决定哪些字节进内存及权限,INTERP 指定动态链接器,GNU_STACK 无 E 表示栈不可执行,GNU_RELRO 提示事后把 GOT 区域设只读。再用-h确认类型/架构/入口(入口是 _start 不是 main)、-d NEEDED干净地列直接依赖(比 ldd 安全)、-n读 core 文件 NOTE 段快速判断崩在哪个程序、什么信号。