PHT
概述
Program Header Table是ELF文件中给加载器(loader)看的部分Program Header Table描述"加载器视角"——文件应该怎么映射到内存中执行
整体结构
PHT是一个数组,每个元素叫Program Header(也叫Segment Header)- 每个
Program Header描述一个段(Segment),告诉加载器一段连续的内存区域应该如何创建
PHT 在 ELF 文件中的位置
|
1 2 3 4 5 6 7 |
PHT 在 ELF 文件中的位置由 ELF Header 中的字段指定: e_phoff = PHT 在文件中的偏移 e_phnum = PHT 中有多少个条目 e_phentsize = 每个条目的大小(64 位下是 56 字节) 只有可执行文件和共享库(.so)有 PHT,.o 文件没有 因为 .o 文件还没准备好被加载,PHT 由链接器在生成可执行文件时创建 |
单个 Program Header 的字段(Elf64_Phdr)
|
1 2 3 4 5 6 7 8 9 10 11 |
// 来自 /usr/include/elf.h typedef struct { Elf64_Word p_type; // 段类型(最重要的字段) Elf64_Word p_flags; // 权限标志(R/W/X) Elf64_Off p_offset; // 段在文件中的偏移 Elf64_Addr p_vaddr; // 段加载到内存的虚拟地址 Elf64_Addr p_paddr; // 物理地址(用户态程序基本不用,嵌入式用) Elf64_Xword p_filesz; // 段在文件中的大小 Elf64_Xword p_memsz; // 段在内存中的大小 Elf64_Xword p_align; // 对齐要求(通常是页大小 4096) } Elf64_Phdr; |
字段说明
p_type(段类型)- 最关键的字段,告诉加载器"这是什么样的段,怎么处理"
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
类型 值 含义 必要性 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ PT_LOAD 1 需要加载到内存的段 ★★★ 核心 一个程序通常有 2-4 个 LOAD 段 用 mmap 把文件内容映射到指定虚拟地址 PT_DYNAMIC 2 动态链接信息所在的位置 ★★ 动态链接必需 指向 .dynamic 节 告诉动态链接器去哪找依赖库列表、符号表等 PT_INTERP 3 动态链接器的路径 ★★ 动态链接必需 内容通常是 "/lib64/ld-linux-x86-64.so.2" 内核读到这个段就知道要先加载动态链接器 PT_NOTE 4 辅助信息(Build ID、ABI 标识等) 一般有 不影响加载和执行 调试和工具用 PT_PHDR 6 PHT 自身的描述 通常有 有点"自指"的意思 某些工具需要 PHT 也作为一个段被映射 PT_TLS 7 线程本地存储模板 用 TLS 时有 每个线程的 thread_local 变量从这里复制初始值 PT_GNU_EH_FRAME 0x6474e550 .eh_frame_hdr 段的位置 C++ 程序通常有 异常处理时快速查找展开信息 PT_GNU_STACK 0x6474e551 栈的属性(是否可执行) 现代 ELF 都有 权限通常是 RW(不可执行)—— NX 保护 防止栈溢出执行 shellcode PT_GNU_RELRO 0x6474e552 启动后变只读的区域 安全特性 GOT 等数据在重定位完成后改为只读 防止 GOT 被篡改 PT_GNU_PROPERTY 0x6474e553 处理器特性属性 新一些的 ELF 有 CET(控制流强制技术)等安全特性 |
p_flags(权限)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
PF_X = 1 可执行(Execute) PF_W = 2 可写(Write) PF_R = 4 可读(Read) 常见组合: R + X = 5 代码段(.text + .rodata) R + W = 6 数据段(.data + .bss) R = 4 只读段(动态链接器路径等) 注意: - 没有 W + X 的段(除非编译器配置错误) - W^X 原则:可写的不可执行,可执行的不可写 - 这是防止代码注入攻击的基础安全机制 - 对应 Windows 的 DEP(Data Execution Prevention) |
p_offset和p_filesz(文件中的位置和大小)
|
1 2 3 4 5 6 |
p_offset = 段在 ELF 文件中的字节偏移 p_filesz = 段在文件中占多少字节 加载器做的事: mmap(p_vaddr, p_filesz, ...) 把文件 [p_offset, p_offset+p_filesz) 映射到内存 [p_vaddr, p_vaddr+p_filesz) |
p_vaddr和p_memsz(内存中的位置和大小)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
p_vaddr = 段加载到内存的虚拟地址 p_memsz = 段在内存中占多少字节 注意 p_memsz 可能大于 p_filesz! 这种情况发生在段包含 .bss(未初始化数据)时 例子: .data 段 = 100 字节(已初始化数据) .bss 段 = 1000 字节(未初始化数据,全是 0) 合并到一个 LOAD 段: p_filesz = 100 ← 文件里只存 .data 的 100 字节 p_memsz = 1100 ← 内存里需要 1100 字节 加载器看到 memsz > filesz 时: 把文件中的 100 字节映射进来 把后面 1000 字节填零(zero-fill) 这就是为什么 .bss 不占磁盘空间 |
p_paddr(物理地址)
|
1 2 3 4 5 |
用户态程序基本忽略这个字段(设为 0 或等于 p_vaddr) 因为用户态程序运行在虚拟地址空间,不直接管物理地址 嵌入式系统、内核、bootloader 才会用这个字段 比如裸机程序需要知道代码加载到物理内存的哪里 |
p_align(对齐要求)
|
1 2 3 4 5 6 |
段的虚拟地址和文件偏移必须满足: p_vaddr % p_align == p_offset % p_align 对于 LOAD 段,通常 p_align = 4096(页大小) 因为 mmap 要求按页对齐 段在文件中的偏移和加载到内存的地址必须有相同的页内偏移 |
- 示例
|
1 2 |
g++ -g math.cpp main.cpp -o program readelf -l program |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
Elf file type is DYN (Position-Independent Executable file) Entry point 0x10a0 There are 13 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x00000000000002d8 0x00000000000002d8 R 0x8 ↑ PHT 自身的描述 INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318 0x000000000000001c 0x000000000000001c R 0x1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] ↑ 动态链接器的路径 LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000638 0x0000000000000638 R 0x1000 ↑ 第一个 LOAD 段:只读(R=4) ↑ 包含 ELF Header、PHT、.interp、.note 等元数据 LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 0x00000000000001f5 0x00000000000001f5 R E 0x1000 ↑ 第二个 LOAD 段:可读+可执行(R+X=5) ↑ 包含 .text(代码)、.plt 等 LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 0x000000000000016c 0x000000000000016c R 0x1000 ↑ 第三个 LOAD 段:只读(R=4) ↑ 包含 .rodata(字符串常量等) LOAD 0x0000000000002da0 0x0000000000003da0 0x0000000000003da0 0x0000000000000270 0x0000000000000278 RW 0x1000 ↑ 第四个 LOAD 段:可读+可写(R+W=6) ↑ 包含 .data、.bss、.got、.got.plt ↑ 注意 MemSiz(0x278) > FileSiz(0x270) → 后 8 字节是 .bss DYNAMIC 0x0000000000002db0 0x0000000000003db0 0x0000000000003db0 0x00000000000001f0 0x00000000000001f0 RW 0x8 ↑ 动态链接信息(指向 .dynamic 节) NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8 ↑ Build ID 等辅助信息 NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358 0x0000000000000044 0x0000000000000044 R 0x4 ↑ ABI 相关的 Note GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8 ↑ 处理器特性 GNU_EH_FRAME 0x000000000000202c 0x000000000000202c 0x000000000000202c 0x000000000000003c 0x000000000000003c R 0x4 ↑ 异常处理框架 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10 ↑ 栈属性:RW 不可执行(NX 保护) GNU_RELRO 0x0000000000002da0 0x0000000000003da0 0x0000000000003da0 0x0000000000000260 0x0000000000000260 R 0x1 ↑ RELRO:启动后变只读的区域(保护 GOT) |
Section to Segment 的映射
PHT不是从零创建的——每个Segment是由若干个Section组合而成的
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
readelf -l program # 在输出末尾会看到: Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 03 .init .plt .plt.got .plt.sec .text .fini 04 .rodata .eh_frame_hdr .eh_frame 05 .init_array .fini_array .dynamic .got .data .bss 06 .dynamic 07 .note.gnu.property 08 .note.gnu.build-id .note.ABI-tag 09 .note.gnu.property 10 .eh_frame_hdr 11 12 .init_array .fini_array .dynamic .got |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
关键观察: - Segment 02(第一个 LOAD,R):包含元数据相关的 sections - Segment 03(第二个 LOAD,R+X):包含所有代码 sections - Segment 04(第三个 LOAD,R):包含只读数据 - Segment 05(第四个 LOAD,R+W):包含可写数据 为什么这样合并? 按权限分组:相同权限的 section 放到同一个 segment 原因:mmap 是按页(4KB)粒度设置权限的,按权限分组减少 mmap 调用次数 同一个 section 可能在多个 segment 中(比如 .dynamic 既在 LOAD 中也在 DYNAMIC 中) 这是因为: - LOAD 段告诉加载器把它映射到内存 - DYNAMIC 段告诉动态链接器它在哪里 两者描述的是同一块内存的不同视角 |
加载器是怎么用 PHT 的
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
内核执行 execve("./program") 时: 1. 读取 ELF Header - 验证 ELF 魔数 - 找到 PHT 的位置(e_phoff) 2. 遍历 PHT 中的所有 PT_LOAD 段 for each Program Header with type == PT_LOAD: mmap(p_vaddr, p_memsz, p_flags转换的权限, MAP_PRIVATE | MAP_FIXED, fd, p_offset) if p_memsz > p_filesz: memset(p_vaddr + p_filesz, 0, p_memsz - p_filesz) # .bss 清零 3. 检查是否有 PT_INTERP 段 if 有 PT_INTERP: 读取 p_offset 处的字符串(如 "/lib64/ld-linux-x86-64.so.2") 把动态链接器也 mmap 到内存 设置 RIP = 动态链接器的入口 动态链接器接管后续工作(加载共享库、做动态重定位) else: 直接设置 RIP = ELF Header 中的 e_entry 4. 设置栈和初始上下文(argv、envp、auxv 等) 5. 切换到用户态执行 |
SHT VS PHT
|
1 2 3 4 5 6 7 8 9 10 |
Section Header Table Program Header Table ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 谁看 链接器、调试器、工具 内核、动态链接器 描述什么 Section(细粒度) Segment(粗粒度,按权限合并) 什么文件有 所有 ELF 文件 只有可执行文件和 .so .o 文件没有 能否去掉 可以(strip 后保留运行需要的) 不能(去掉无法运行) 查看命令 readelf -S readelf -l 描述维度 每种数据一个 entry 每段连续内存一个 entry 数量 通常 20-30 个 通常 5-13 个 |
简单的内存观察
|
1 2 3 4 5 6 |
# 启动你的程序,让它休眠一会 ./program & PID=$! # 查看进程的内存映射 cat /proc/$PID/maps |
关于pht的理解
- 链接完成时:
ELF文件中所有字节的布局是固定的- 各个
section(.text/.rodata/.data等)在文件中的偏移是固定的 PHT中各个segment的p_offset和p_vaddr也是固定的
- 加载时:
- 加载器读
PHT,不看Section Header Table - 对每个
PT_LOAD segment,用mmap把文件的[p_offset, p_offset+p_filesz)映射到虚拟地址[p_vaddr, p_vaddr+p_filesz),权限按p_flags - 如果
p_memsz > p_filesz(有.bss),多出部分用匿名零页填充 - 其他类型的
segment(INTERP/DYNAMIC/NOTE等)不做mmap,由动态链接器或运行时按需使用
- 加载器读
- 另,
PHT描述的是Segment,不是.text/.rodata/.data/.bss这些Section
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
术语区分(非常重要): Section(节) Segment(段) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 细粒度 粗粒度 按数据类型划分 按内存权限划分 .text / .rodata LOAD R+X / LOAD R / LOAD R+W .data / .bss (一个 LOAD 包含多个 section) 由 Section Header 由 Program Header Table 描述 Table 描述 链接器看的视角 加载器看的视角 PHT 中没有 ".text 段" 这种东西 PHT 中只有 "LOAD 段"、"DYNAMIC 段"、"INTERP 段" 等 一个 LOAD 段内部可能包含 .text + .init + .fini + .plt 等多个 section |
SHT
概述
Section Header Table描述"链接器视角"——文件由哪些section组成Section Header Table(SHT)是ELF文件的"目录"——它告诉链接器、调试器和各种工具,这个文件由哪些部分组成、每个部分是什么、在哪里
SHT 在文件中的位置
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
SHT 由 ELF Header 中的字段定位: e_shoff = SHT 在文件中的偏移(通常在文件末尾) e_shnum = SHT 中有多少个条目(section 的数量) e_shentsize = 每个条目的大小(64 位下是 64 字节) e_shstrndx = section 名字符串表(.shstrtab)在 SHT 中的索引 ← 这个字段很关键,下面会讲 ELF 文件的典型布局: ┌─────────────────────┐ │ ELF Header │ 开头 ├─────────────────────┤ │ Program Header Table │ (可执行文件/共享库才有) ├─────────────────────┤ │ .text section │ │ .rodata section │ 各个 section 的实际数据 │ .data section │ │ ... │ ├─────────────────────┤ │ Section Header Table │ 通常在末尾 ← SHT 在这里 └─────────────────────┘ |
单个 Section Header 的字段(Elf64_Shdr)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 来自 /usr/include/elf.h typedef struct { Elf64_Word sh_name; // section 名(是 .shstrtab 中的偏移,不是字符串本身) Elf64_Word sh_type; // section 类型 Elf64_Xword sh_flags; // section 标志(属性) Elf64_Addr sh_addr; // 如果加载到内存,虚拟地址是多少 Elf64_Off sh_offset; // section 在文件中的偏移 Elf64_Xword sh_size; // section 的大小 Elf64_Word sh_link; // 关联到另一个 section(含义随类型变化) Elf64_Word sh_info; // 附加信息(含义随类型变化) Elf64_Xword sh_addralign; // 对齐要求 Elf64_Xword sh_entsize; // 如果是表,每个表项的大小 } Elf64_Shdr; |
字段说明
sh_namesh_name不是字符串,而是一个整数偏移
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
sh_name 是一个偏移量,指向 .shstrtab 这个特殊 section 中的位置 .shstrtab(Section Header String Table)专门存放所有 section 的名字 它本身也是一个 section,索引由 ELF Header 的 e_shstrndx 指定 例子: .shstrtab 的内容(一串以 \0 分隔的字符串): 偏移: 0 1...5 6...11 12...17 内容: \0 .text\0 .data\0 .bss\0 ↑ 第 0 字节通常是空字符串 某个 section 的 sh_name = 1 → 去 .shstrtab 偏移 1 处读字符串 → 得到 ".text" 为什么这样设计? - ELF 头是定长结构,没法直接放变长的字符串 - 所有名字集中存到一个字符串表,统一管理 - 多个地方可以共享同一个字符串 |
|
1 2 3 |
# 查看 .shstrtab 的内容 readelf -p .shstrtab program # 输出所有 section 名的字符串 |
sh_type(section类型)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
类型 值 含义 典型 section ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SHT_NULL 0 无效/空 section 第 0 个 section 总是 NULL SHT_PROGBITS 1 程序数据(代码、初始化数据等) .text .data .rodata 内容由程序定义,文件中实际存储 SHT_SYMTAB 2 符号表(完整版) .symtab SHT_STRTAB 3 字符串表 .strtab .shstrtab .dynstr SHT_RELA 4 带加数的重定位表 .rela.text .rela.data SHT_HASH 5 符号哈希表(动态链接加速) .hash SHT_DYNAMIC 6 动态链接信息 .dynamic SHT_NOTE 7 辅助信息 .note.gnu.build-id SHT_NOBITS 8 不占文件空间的数据 .bss ← 关键! 文件中不存储,加载时清零 SHT_REL 9 不带加数的重定位表 .rel.text(x86-32 用) SHT_DYNSYM 11 动态符号表(精简版) .dynsym SHT_INIT_ARRAY 14 初始化函数指针数组 .init_array SHT_FINI_ARRAY 15 析构函数指针数组 .fini_array |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
SHT_PROGBITS(如 .data): sh_offset 指向文件中真实存在的数据 sh_size 字节的数据真实存储在文件里 SHT_NOBITS(如 .bss): sh_offset 仍然有值,但文件中并没有真实数据 sh_size 表示加载到内存后占多少字节 这就是为什么 .bss 不占磁盘空间——它的类型是 NOBITS 验证: readelf -S program | grep -A1 bss # .bss NOBITS ... ← 类型是 NOBITS |
sh_flags(section属性)- 注意
SHF_ALLOC这个标志——它决定了section是否会被加载到内存 .text/.data/.rodata/.bss有这个标志(运行时需要),而.symtab/.debug_*/.comment没有(只在文件中,运行时用不到)strip去掉的就是那些没有ALLOC标志的section
- 注意
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
标志 值 含义 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SHF_WRITE 0x1 运行时可写 .data .bss 有这个 SHF_ALLOC 0x2 加载时占用内存 .text .data .rodata .bss 有 .symtab .debug_* 没有(不加载) SHF_EXECINSTR 0x4 包含可执行指令 .text 有 SHF_MERGE 0x10 内容可合并 .rodata.str(相同字符串合并) SHF_STRINGS 0x20 包含字符串 字符串常量 section SHF_INFO_LINK 0x40 sh_info 是 section 索引 SHF_TLS 0x400 线程本地存储 .tdata .tbss 常见组合(readelf -S 输出中的 Flg 列): .text: AX = ALLOC + EXECINSTR 加载到内存 + 可执行 .rodata: A = ALLOC 加载到内存 + 只读 .data: WA = WRITE + ALLOC 加载到内存 + 可写 .bss: WA = WRITE + ALLOC 加载到内存 + 可写(NOBITS) .symtab: (空) = 没有 ALLOC 不加载到内存(仅文件中存在) .debug_info: (空) 不加载到内存(调试时才读) |
sh_addr(虚拟地址)
|
1 2 3 4 5 6 7 8 9 10 |
如果 section 有 SHF_ALLOC 标志(会被加载到内存): sh_addr = 它被加载到的虚拟地址 如果 section 没有 SHF_ALLOC 标志(不加载,如 .symtab .debug_*): sh_addr = 0 注意: .o 文件中所有 section 的 sh_addr 都是 0 因为 .o 还没链接,地址还没确定 链接器在生成可执行文件时才填入真实的 sh_addr |
sh_offset和sh_size(文件位置和大小)
|
1 2 3 4 5 6 7 |
sh_offset = section 在文件中的字节偏移 sh_size = section 的字节大小 特例:SHT_NOBITS(.bss) sh_offset 仍然指向一个文件位置(通常是下一个 section 的起点) 但文件中实际没有这个 section 的数据 sh_size 表示它在内存中应该占的大小 |
sh_link和sh_info(关联信息)- 这两个字段的含义随
sh_type变化,是SHT中最灵活也最容易混淆的部分:
- 这两个字段的含义随
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
对于符号表(SHT_SYMTAB / SHT_DYNSYM): sh_link = 关联的字符串表的 section 索引 (符号名存在那个字符串表里) sh_info = 第一个全局符号的索引 (符号表中局部符号在前,全局符号在后,sh_info 是分界线) 对于重定位表(SHT_RELA / SHT_REL): sh_link = 关联的符号表的 section 索引 (重定位条目引用的符号在那个符号表里) sh_info = 被重定位的 section 索引 (比如 .rela.text 的 sh_info 指向 .text) 对于其他类型: 通常为 0 |
sh_addralign(对齐)
|
1 2 3 4 5 6 7 8 9 |
sh_addralign = section 要求的对齐边界 例子: .text 通常对齐到 16(函数入口对齐,缓存友好) .data 对齐到 8 或更大 SIMD 数据可能对齐到 16 或 32 链接器合并 section 时必须遵守对齐要求 比如把多个 .o 的 .text 合并时,每个的起始地址要满足对齐 |
sh_entsize(表项大小)
|
1 2 3 4 5 6 7 8 |
如果 section 是一个"表"(每个元素大小固定),sh_entsize 是元素大小 符号表 .symtab:sh_entsize = 24(每个 Elf64_Sym 是 24 字节) 重定位表 .rela.text:sh_entsize = 24(每个 Elf64_Rela 是 24 字节) 普通数据 section(.text .data):sh_entsize = 0(不是表) 用途:解析时用 sh_size / sh_entsize 算出表中有多少个元素 |
观察 SHT
- 文件
|
1 2 |
g++ -g math.cpp main.cpp -o program readelf -S program |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
Section Headers: [Nr] Name Type Address Off Size ES Flg Lk Inf Al [ 0] NULL 0000000000000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 0000000000000318 000318 00001c 00 A 0 0 1 [ 2] .note.gnu.pr[...] NOTE 0000000000000338 000338 000020 00 A 0 0 8 [ 3] .note.gnu.bu[...] NOTE 0000000000000358 000358 000024 00 A 0 0 4 [ 4] .gnu.hash GNU_HASH 0000000000000380 000380 000024 00 A 5 0 8 [ 5] .dynsym DYNSYM 00000000000003a8 0003a8 0000a8 18 A 6 1 8 [ 6] .dynstr STRTAB 0000000000000450 000450 00008d 00 A 0 0 1 [ 7] .gnu.version VERSYM 00000000000004de 0004de 00000e 02 A 5 0 2 [ 8] .gnu.version_r VERNEED 00000000000004f0 0004f0 000030 00 A 6 1 8 [ 9] .rela.dyn RELA 0000000000000520 000520 0000c0 18 A 5 0 8 [10] .rela.plt RELA 00000000000005e0 0005e0 000018 18 AI 5 24 8 [11] .init PROGBITS 0000000000001000 001000 00001b 00 AX 0 0 4 [12] .plt PROGBITS 0000000000001020 001020 000020 10 AX 0 0 16 [13] .text PROGBITS 0000000000001060 001060 000131 00 AX 0 0 16 [14] .fini PROGBITS 0000000000001194 001194 00000d 00 AX 0 0 4 [15] .rodata PROGBITS 0000000000002000 002000 00001d 00 A 0 0 4 [16] .eh_frame_hdr PROGBITS 000000000000201c 00201c 00003c 00 A 0 0 4 [17] .eh_frame PROGBITS 0000000000002058 002058 0000ac 00 A 0 0 8 [18] .init_array INIT_ARRAY 0000000000003da0 002da0 000008 08 WA 0 0 8 [19] .fini_array FINI_ARRAY 0000000000003da8 002da8 000008 08 WA 0 0 8 [20] .dynamic DYNAMIC 0000000000003db0 002db0 0001f0 10 WA 6 0 8 [21] .got PROGBITS 0000000000003fa0 002fa0 000030 08 WA 0 0 8 [22] .data PROGBITS 0000000000004000 003000 000010 00 WA 0 0 8 [23] .bss NOBITS 0000000000004010 003010 000008 00 WA 0 0 4 [24] .comment PROGBITS 0000000000000000 003010 00002b 01 MS 0 0 1 [25] .symtab SYMTAB 0000000000000000 003040 000378 18 26 18 8 [26] .strtab STRTAB 0000000000000000 0003b8 000156 00 0 0 1 [27] .shstrtab STRTAB 0000000000000000 00050e 0000ee 00 0 0 1 |
- 解读
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
Nr = section 编号(索引) Name = section 名(从 .shstrtab 中解析出来的) Type = sh_type Address = sh_addr(虚拟地址,0 表示不加载) Off = sh_offset(文件偏移) Size = sh_size ES = sh_entsize(表项大小,0 表示不是表) Flg = sh_flags(A=ALLOC, W=WRITE, X=EXEC, M=MERGE, S=STRINGS, I=INFO_LINK) Lk = sh_link Inf = sh_info Al = sh_addralign 关键观察: 1. [0] NULL section:第 0 个永远是空的 这样符号表里 "section index = 0" 可以表示 "无 section" 2. 有 Address 的 section([1]-[23]):Flg 都含 A(ALLOC),会加载到内存 没 Address 的 section([24]-[27]):Flg 没有 A,不加载(调试/符号信息) 3. .text [13]:Flg = AX(加载+可执行),Al = 16(16 字节对齐) 4. .data [22]:Flg = WA(加载+可写),Type = PROGBITS(文件中有数据) 5. .bss [23]:Flg = WA,Type = NOBITS(文件中无数据) 注意它的 Off = 003010,和 .comment [24] 的 Off 相同 因为 .bss 不占文件空间,下一个 section 直接接在它"应该"的位置 6. .symtab [25]:sh_link(Lk) = 26 → 指向 .strtab(符号名在那里) sh_info(Inf) = 18 → 第 18 个符号开始是全局符号 ES = 18(0x18=24) → 每个符号 24 字节 7. .rela.plt [10]:sh_link(Lk) = 5 → 指向 .dynsym(重定位引用的符号在那里) sh_info(Inf) = 24 → 这些重定位作用于第 24 个 section(.got) 8. .strtab/.symtab/.shstrtab:Address = 0(不加载到内存,运行时不需要) |
.o文件的SHT对比
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
关键差异(和可执行文件对比): 1. 所有 section 的 Address 都是 0 因为 .o 还没链接,地址未确定 链接器会在生成可执行文件时填入真实地址 2. 有 .rela.text section(可执行文件中没有了) .o 中需要重定位信息告诉链接器哪里要修正 链接完成后,静态重定位都处理完了,可执行文件不再需要 .rela.text (只保留动态链接需要的 .rela.dyn / .rela.plt) 3. 没有 PHT readelf -l math.o 会显示 "There are no program headers in this file." .o 不需要加载,所以没有执行视角的 PHT 4. section 更"原始" 没有 .plt / .got / .interp / .dynamic 这些链接器生成的 section 这些都是链接时才创建的 |
SHT 的解析依赖链
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
1. 读 ELF Header → 拿到 e_shoff(SHT 位置) → 拿到 e_shnum(section 数量) → 拿到 e_shstrndx(.shstrtab 的索引) 2. 定位 SHT SHT 起始 = 文件基址 + e_shoff 第 i 个 section header = SHT 起始 + i * e_shentsize 3. 找到 .shstrtab shstrtab_header = 第 e_shstrndx 个 section header shstrtab 数据 = 文件基址 + shstrtab_header->sh_offset 4. 解析每个 section 的名字 for i in 0..e_shnum: shdr = 第 i 个 section header name = shstrtab 数据 + shdr->sh_name ← sh_name 是偏移 5. 根据 sh_type 做进一步解析 if shdr->sh_type == SHT_SYMTAB: 解析符号表,符号名在 sh_link 指向的字符串表里 if shdr->sh_type == SHT_RELA: 解析重定位表,符号在 sh_link 指向的符号表里 |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Linux目录的作用03/16
- ♥ Vim编辑器的操作03/17
- ♥ Cef:介绍06/29
- ♥ Linux_ 命令大全 文档编辑03/16
- ♥ Linux 线程的同步与互斥03/31
- ♥ Linux 内核空间&&用户空间03/30