简单ELF二进制段重叠、未对齐的原因及Linux处理机制问询
我正在开发ELF加载器并研究ELF格式,使用Fedora 38中的Clang 16编译了一个无特殊编译选项的Hello World二进制文件(命令:clang hello.c -o hello),该文件可正常运行。通过readelf查看程序头时,发现与我对LOAD和DYNAMIC段的理解不符的情况:
程序头信息:
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align … LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000004f0 0x00000000000004f0 R 0x1000 LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000 0x000000000000016d 0x000000000000016d R E 0x1000 LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000 0x00000000000000dc 0x00000000000000dc R 0x1000 LOAD 0x0000000000002df8 0x0000000000403df8 0x0000000000403df8 0x0000000000000214 0x0000000000000218 RW 0x1000 DYNAMIC 0x0000000000002e08 0x0000000000403e08 0x0000000000403e08 0x00000000000001d0 0x00000000000001d0 RW 0x8
前三个LOAD段的虚拟地址对齐符合预期,但最后一个LOAD段与DYNAMIC段重叠且未对齐。该可执行文件在Linux中可正常加载,/proc/<pid>/maps显示的内存映射如下:
00400000-00401000 r--p 00000000 00:23 16905823 /path/to/hello 00401000-00402000 r-xp 00001000 00:23 16905823 /path/to/hello 00402000-00403000 r--p 00002000 00:23 16905823 /path/to/hello 00403000-00404000 r--p 00002000 00:23 16905823 /path/to/hello 00404000-00405000 rw-p 00003000 00:23 16905823 /path/to/hello
我有以下疑问:
- 链接器为何生成未对齐的段?依赖操作系统修正似乎并非良策。
- 链接器为何创建重叠的段?同样,依赖操作系统修正并非良策。
- Linux是否有特定的规范、标准或算法来修正该问题?感觉存在未记载或我未找到的行为。
问题解答
1. 链接器为何生成未对齐的LOAD段?
这里的“未对齐”是针对页面大小(0x1000)而言,但ELF规范并没有要求LOAD段的虚拟地址必须是页面对齐的——它只要求LOAD段的**对齐值(Align字段)**符合规则,且满足VirtAddr % Align == Offset % Align的约束。
链接器这么做是为了缩减二进制文件体积:如果强行把最后一个LOAD段对齐到0x1000,需要在文件中填充大量无意义的空字节,浪费磁盘空间。你可以计算验证:最后一个LOAD段的0x403df8 % 0x1000 = 0xdf8,0x2df8 % 0x1000 = 0xdf8,完全符合ELF规范的对齐要求。
2. 链接器为何创建重叠的段?
这种“重叠”是正常设计,并非错误。DYNAMIC段本身就是最后一个LOAD段的一部分:它的虚拟地址落在最后一个LOAD段的地址范围(0x403df8到0x403df8 + 0x218 = 0x404010)内,且两者权限均为RW。
DYNAMIC是描述ELF元数据的程序头表项,不需要单独创建LOAD段——它的权限和所在的LOAD段完全一致,直接复用现有LOAD段的映射即可,这是合理的资源复用设计。
3. Linux内核的处理逻辑
Linux内核按页面粒度管理内存映射,这就是/proc/<pid>/maps中都是0x1000对齐区间的原因,核心处理逻辑如下:
- 遍历所有LOAD段,将每个段的地址范围向上/向下对齐到页面边界。
- 对跨页面的LOAD段,创建对应内存映射,并根据LOAD段的权限设置页面访问权限。
- 对于LOAD段中
MemSiz大于FileSiz的部分(比如最后一个LOAD段中4字节的差异),内核会将这部分映射为匿名内存并做零初始化,用来满足BSS段的需求(BSS段在文件中不占空间,加载时需要分配零填充的内存)。
这些逻辑完全符合ELF规范,并非未记载行为,内核源码fs/binfmt_elf.c中的load_elf_binary函数详细实现了ELF加载的全过程。
内容的提问来源于stack exchange,提问作者csnate

