﻿# 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）；缺点是只能看"文件里静态写了什么"，看不到运行时状态。

```bash
# 一般随 binutils 安装
yum install binutils / apt install binutils
```

## 一、常用选项

```bash
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

```bash
$ 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（节，链接/调试视角）

```bash
$ 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  1
```

- `Flg`：`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` 的真实输出：

```bash
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   0x1
```

### 4.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 用这份输出核对前面讲的三件事

1. **`Entry point 0x4019a0`** —— 正好落在 `.text`（`readelf -S` 里 `.text` 的 Address 就是 `0x4019a0`）。注意它是 `_start` 附近，不是 `main`（CRT 启动代码先跑）。
2. **两个 LOAD 隔 2MB**：LOAD #1 `VirtAddr=0x400000`，LOAD #2 `VirtAddr=0x607dc8`，`Align=0x200000`。权限不同（R+E vs RW）必须落在不同内存页，按 2MB 对齐错开——这解释了 `readelf -S` 里 `.gcc_except_table(0x406e8c)` 到 `.init_array(0x607dc8)` 的地址跳变。
3. **LOAD #2 `MemSiz(0x848) > FileSiz(0x404)`**：差的 `0x444` 就是 `.bss` 等 `NOBITS` 内容——文件里不占字节，内存里要占且清零。这是"`.bss` 特例"的段级证据。

### 4.4 底部：Section → Segment 映射（两种视角的桥梁）

```bash
 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` 动态依赖（看依赖哪些库）

```bash
$ 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.md](/concepts/elf/ldd.md)）。

## 六、`-n` NOTE 段（core 文件分析）

core 文件的关键信息都在 NOTE 段（对应 [core-dump.md](/crash/core-dump.md) 说的 `PT_NOTE`）：

```bash
$ readelf -n core.1234
  NT_PRSTATUS   进程状态 + 寄存器
  NT_PRPSINFO   进程名、PID、UID
  NT_SIGINFO    导致崩溃的信号
  NT_FILE       内存映射的文件列表
```

不用 gdb 也能先 `readelf -n core.xxx` 快速确认"是哪个程序、什么信号崩的"。

## 七、结合本仓库

```bash
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](/concepts/elf/objdump.md)。

