程序头表(Program Header Table)是ELF文件中描述段(segments)如何映射到内存中的关键结构。
程序头表基本概念
程序头表由多个程序头(Program Header)组成,每个程序头描述一个段(segment)的信息。这些段告诉操作系统如何将程序加载到内存中执行。
程序头表的主要属性:
只在可执行文件和共享库中存在(目标文件没有)
由e_phoff指定在文件中的偏移量
包含e_phnum个条目
每个条目大小为e_phentsize字节
程序头结构
每个程序头是一个Elf64_Phdr结构(32位是Elf32_Phdr),包含以下字段:
typedef struct {
Elf64_Word p_type; // 段类型
Elf64_Word p_flags; // 段标志
Elf64_Off p_offset; // 段在文件中的偏移
Elf64_Addr p_vaddr; // 段的虚拟地址
Elf64_Addr p_paddr; // 段的物理地址(通常与虚拟地址相同)
Elf64_Xword p_filesz; // 段在文件中的大小
Elf64_Xword p_memsz; // 段在内存中的大小
Elf64_Xword p_align; // 对齐方式
} Elf64_Phdr;
以helloworld为例分析
编译一个简单的helloworld程序并分析其程序头表:
// hello.c
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
编译:gcc -o hello hello.c
使用readelf -l hello
查看程序头表:
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
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
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000005c8 0x00000000000005c8 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x00000000000001f5 0x00000000000001f5 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000158 0x0000000000000158 R 0x1000
LOAD 0x0000000000002db0 0x0000000000003db0 0x0000000000003db0
0x0000000000000258 0x0000000000000260 RW 0x1000
DYNAMIC 0x0000000000002dc0 0x0000000000003dc0 0x0000000000003dc0
0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002db0 0x0000000000003db0 0x0000000000003db0
0x0000000000000250 0x0000000000000250 R 0x1
LOAD 0x0000000000004000 0x0000000000004000 0x0000000000004000
0x00000000000003c8 0x00000000000003c8 RW 0x1000
关键段类型解释
PHDR: 程序头表本身的位置和大小
INTERP: 指定程序解释器(动态链接器路径)
LOAD: 可加载段,包含代码或数据
第一个LOAD: 只读数据(如.rodata,通常映射到较低的虚拟地址)
第二个LOAD: 可执行代码(.text)
第三个LOAD: 只读数据(通常映射到更高的虚拟地址)
第四个LOAD: 可读写数据(.data, .bss)
DYNAMIC: 动态链接信息
NOTE: 附加信息
GNU_STACK: 控制栈属性
GNU_RELRO: 指定重定位后只读的内存区域
段标志含义
R: 可读
W: 可写
E: 可执行
内存映射过程
当加载hello程序时,操作系统:
1.读取程序头表,找到所有LOAD类型的段
2.将这些段按指定的虚拟地址映射到内存
文件偏移0x1000映射到虚拟地址0x1000(代码段)
文件偏移0x2000映射到虚拟地址0x2000(只读数据)
文件偏移0x2db0映射到虚拟地址0x3db0(数据段)
3.初始化.bss段(MemSiz > FileSiz的部分清零)
4.加载动态链接器(由INTERP段指定)
5.将控制权转移到入口点(0x1060)
实际应用中的观察
1.代码段:包含main函数和printf的调用代码
标志为R E(可读可执行)
通常位于第二个LOAD段
2.字符串常量:"Hello, World!\n"位于只读数据段
标志为R(只读)
通常位于第一个或第三个LOAD段
3.数据段:包含全局变量等
标志为RW(可读写)
通常位于第四个LOAD段
通过分析程序头表,我们可以了解程序的内存布局和加载方式,这对于理解程序执行、调试和安全分析都非常重要。