我正在写一个ELF加载器并研究ELF格式。我有一个简单的 hello world 二进制文件,是在 Fedora 38 中使用 Clang 16 创建的,它可以工作,而且我没有使用任何特定选项进行编译/链接 (
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 中加载得很好,但 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
所以我有几个问题:
为什么我的链接器会产生未对齐的段?
事实并非如此。要求是应该可以直接
mmap
LOAD
段,为此,以下 必须 为真:p_vaddr % pagesize == p_offset % pagesize
。对于输出中的所有 LOAD
段都是如此。
为什么我的链接器会创建重叠的段?
这没有什么问题——相同的文件内容会在内存中出现两次。
另一种方法是在文件中插入一个大洞,浪费磁盘空间。
另请参阅此答案。
Linux 是否实现了特定的约定、标准或算法来纠正此问题?
无需修正。仔细阅读 man mmap。
由于
offset
必须页面对齐,因此动态加载程序将 both p_vaddr
和 p_offset
向下舍入以进行页面对齐,并使用两者的页面对齐值执行 mmap(..., MAP_FIXED, ...)
。这“覆盖”了一点数据,但保证代码或数据最终准确地位于其链接的地址处。