基于RISC-V的编译链接和装载05之ELF之段(segment)
基于RISC-V的编译链接和装载05之ELF之段(segment)
段是加载器处理ELF文件的单位,段由1个或1个以上的节组成,内存上有只读,可读可写,可执行等不同属性,所以可以根据属性加载不同的段。
1 使用readelf -l查看程序头
使用readelf -l命令查看程序头表(Program header table):
$ riscv64-unknown-linux-gnu-readelf -l helloworld.elf
注意:使用-lW宽屏显示更方便查看
其输出为:
$ riscv64-unknown-linux-gnu-readelf -lW helloworld.elf
Elf file type is EXEC (Executable file)
Entry point 0x10468
There are 10 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000010040 0x0000000000010040 0x000230 0x000230 R 0x8
INTERP 0x000270 0x0000000000010270 0x0000000000010270 0x000021 0x000021 R 0x1
[Requesting program interpreter: /lib/ld-linux-riscv64-lp64d.so.1]
RISCV_ATTRIBUT 0x001045 0x0000000000000000 0x0000000000000000 0x0000ca 0x000000 R 0x1
LOAD 0x000000 0x0000000000010000 0x0000000000010000 0x0005c0 0x0005c0 R E 0x1000
LOAD 0x000df0 0x0000000000011df0 0x0000000000011df0 0x000228 0x000230 RW 0x1000
DYNAMIC 0x000e08 0x0000000000011e08 0x0000000000011e08 0x0001e0 0x0001e0 RW 0x8
NOTE 0x0005a0 0x00000000000105a0 0x00000000000105a0 0x000020 0x000020 R 0x4
GNU_EH_FRAME 0x000518 0x0000000000010518 0x0000000000010518 0x000024 0x000024 R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x000df0 0x0000000000011df0 0x0000000000011df0 0x000210 0x000210 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .riscv.attributes
03 .interp .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.plt .plt .text .rodata .eh_frame_hdr .eh_frame .note.ABI-tag
04 .preinit_array .init_array .fini_array .dynamic .got .got.plt .sdata .bss
05 .dynamic
06 .note.ABI-tag
07 .eh_frame_hdr
08
09 .preinit_array .init_array .fini_array .dynamic .got
从日志中可以得到如下有用信息:
1.1 节和段的对应关系
日志中最后部分的 Section to Segment mapping 揭示了哪些节被合并到了同一个运行时段中,表明了节与段的映射关系:
比如:
00 段表示Type为PHDR的段,
01段表示Type为INTERP段,
02表示Type为RISCV_ATTRIBUT段,
03表示Type为LOAD段,
04表示Type为LOAD段,与日志上半部分一一对应
程序执行时我们一般只用关注Type为LOAD的段(动态链接还需关注GNU_RELRO段),这几个段与节的对应关系为:
| Segment | 包含的节 | 运行时权限 | 说明 |
|---|---|---|---|
| 03 (LOAD代码段) | .interp .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.plt .plt .text .rodata .eh_frame_hdr .eh_frame .note.ABI-tag | R E | 所有只读、可执行的内容被打包在一起,加载后受 MMU 页保护,不可篡改 |
| 04 (LOAD数据段) | .preinit_array .init_array .fini_array .dynamic .got .got.plt .sdata .bss | RW | 所有可读写的全局数据和动态链接控制表在此,支持运行时修改 |
| 09 (GNU_RELRO) | .preinit_array, .init_array, .fini_array, .dynamic, .got |
加载后变为只读 | 用于保护初始化函数表和 GOT 表,防止安全攻击 |
程序头表本身并不直接记录节和段映射,它只包含段的地址、偏移、大小和权限。
Section to Segment mapping 输出的映射关系是经过readelf解析得到的,解析原理是: readelf通过将程序头中每个段的文件偏移范围(p_offset 到 p_offset+p_filesz)与节头表中每个节的文件偏移范围(sh_offset 到 sh_offset+sh_size)进行比对,来推断/计算这种映射。
1.2 运行时内存段布局
这是加载器实际映射到 RAM 的两个物理段(LOAD 类型),决定了实际内存占用:
| 段类型 | 文件偏移 | 虚拟地址 (VirtAddr) | 文件大小 (FileSiz) | 内存大小 (MemSiz) | 权限 | 实际意义 |
|---|---|---|---|---|---|---|
| LOAD (代码段) | 0x000000 |
0x10000 |
0x5c0 (1472B) |
0x5c0 (1472B) |
R E (读+执行) | 包含代码、只读常量、动态链接表。占用 Flash/RAM 约 1.44 KB(按 4KB 页对齐后实际占 4KB)。 |
| LOAD (数据段) | 0x000df0 |
0x11df0 |
0x228 (552B) |
0x230 (560B) |
RW(读+写) | 包含全局变量、GOT 表。运行时实际占用 560 字节(按 4KB 页对齐后实际占 4KB)。 |
1.3 BSS 段大小(FileSiz vs MemSiz)
数据段的 FileSiz(0x228)比 MemSiz(0x230)少了 8 个字节(0x230 - 0x228 = 0x8)。这 8 字节正是之前 -SW 中看到的 .bss 段大小。说明BSS段不会占用磁盘大小,但是加载后会占用实际的Memory。
ELF 文件中不存储未初始化的全局变量(BSS),但加载器会在内存中 0x11df0 + 0x228 的位置额外开辟 8 字节并清零。这证明.bss 变量(如未初始化的全局数组)会被正确初始化为 0。
1. 4 动态链接器依赖
INTERP 段明确指定了请求的程序解释器:/lib/ld-linux-riscv64-lp64d.so.1。这是 RISC-V 64 位 glibc 动态链接器的绝对路径。 所以此程序必须在带有标准 glibc 的 RISC-V Linux 系统上运行。
2 ELF程序头的数据结构
2.1 ELF程序头结构成员含义
以rv64为例,其文件头数据结构为(rv32成员也与一样,只是数据类型不一样,所以文件头长度也不一样):
// https://github.com/roolebo/elfutils/blob/master/libelf/elf.h
typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;
ELF文件头结构成员含义:
| 序号 | 成员 | 含义 |
|---|---|---|
| 1 | p_type | 段的类型,决定该段的用途。常见值: - PT_NULL = 0:无效段- PT_LOAD = 1:可加载段(如代码、数据)- PT_DYNAMIC = 2:动态链接信息- PT_INTERP = 3:指定动态链接器路径(如 /lib/ld-linux.so)- PT_NOTE = 4:附加信息(如构建 ID)- PT_SHLIB = 5:保留(未使用)- PT_PHDR = 6:程序头表自身的位置- PT_TLS = 7:线程局部存储模板 |
| 2 | p_flags | 段的权限标志,表示该段在内存中的访问权限: - PF_R = 0x4:可读- PF_W = 0x2:可写- PF_X = 0x1:可执行例如: PF_R + PF_X 表示代码段(.text),PF_R + PF_W 表示数据段(.data)。 |
| 3 | p_offset | 段内容在 ELF 文件中的偏移量(字节为单位) 加载器从该偏移开始读取 p_filesz 字节的数据到内存 |
| 4 | p_vaddr | 段在内存中的虚拟地址(运行时地址) 加载器将该段数据复制到此地址。例如: 0x400000 |
| 5 | p_paddr | 段在内存中的物理地址 在大多数现代操作系统中(使用虚拟内存),此字段被忽略,仅在无 MMU 的嵌入式系统中可能有用 |
| 6 | p_filesz | 段在文件中的大小(字节) 表示从文件中读取多少数据 |
| 7 | p_memsz | 段在内存中的大小(字节) 如果 p_memsz > p_filesz,多出的部分用零填充(常用于 .bss 段例如: .bss 段 p_filesz = 0,但 p_memsz = 1024,表示分配 1KB 内存并清零 |
| 8 | p_align | 段的对齐要求(必须是 2 的幂)p_vaddr 和 p_offset 都必须对齐到该值例如: p_align = 4096 表示页对齐 |
2.2 常见段类型举例
PT_LOAD:
注意:一个可执行文件至少有一个 PT_LOAD 类型的段。
这类程序头描述的是可装载的段,也就是说,这种类型的段将被装载或者映射到内存中。
例如:一个需要动态链接的 ELF 可执行文件通常包含以下两个可装载的段(类型为 PT_LOAD):
-
存放程序代码的 text 段,权限为可读/ 可执行;
-
存放全局变量和动态链接信息的 data 段,权限为可读可写。
PT_DYNAMIC:
动态段是动态链接可执行文件所特有的,包含了动态链接器所必需的一些信息。在动态段中包含以下内容:
-
运行时需要链接的共享库列表;
-
全局偏移表(.GOT.PLT)的地址,用于ELF 动态链接;
-
重定位条目的相关信息。
PT_INTERP:
PT_INTERP 段是将程序解释器路径(如/lib/ld-linux-riscv64-lp64d.so.1)存放在一个以 null 为终止符的字符串中。
PT_NOTE:
PT_NOTE 类型的段可能保存了与特定供应商或者系统相关的附加信息。
PT_PHDR:
PT_PHDR 段保存了程序头表本身的位置和大小。
参考:
- 《程序员的自我修养—链接、装载与库》

浙公网安备 33010602011771号