从PT_LOAD到虚拟内存:ELF 段为什么要匹配页内偏移

从 PT_LOAD 到虚拟内存:ELF 段为什么要匹配页内偏移

阅读 ELF 程序头时,看到这样的段:

第四个PT_LOAD段的p_offset 和 p_vaddr 看起来都没有按 0x1000 对齐。既然 ELF 加载时按页映射,这样的段是怎么放进内存的?为什么要让文件偏移和虚拟地址的页内偏移相同?

答案的关键在于:段的起点不一定是映射的起点。 PT_LOAD 描述段实际内容从哪里开始;内存管理器则以页为单位建立映射。

段起点与映射起点不是一回事

假设页大小为 0x1000。上面的段实际从以下位置开始:

text 复制代码
文件偏移:0x2d98
虚拟地址:0x3d98

内存映射需要以页为单位,所以起点向下取整:

text 复制代码
文件映射起点:0x2000
虚拟映射起点:0x3000

段实际起点在这两个页中都偏移 0xd98:

text 复制代码
0x2d98 - 0x2000 = 0xd98
0x3d98 - 0x3000 = 0xd98

因此,可以把文件页 0x2000 映射到虚拟页 0x3000。映射保留页内偏移:文件偏移 0x2d98 的字节,就出现在虚拟地址 0x3d98。

换句话说,整页映射的是页;段从页内某个偏移处开始。文件页首和虚拟页首不必是段的实际起点。

为什么页内偏移要相同?

普通文件映射建立的是"文件页到虚拟页"的对应关系。映射不会把文件页中的字节重新排列:文件页内偏移 x 的内容,会出现在虚拟页内偏移 x。

所以,如果段的文件起点在页内偏移 0xd98,而虚拟起点在页内偏移 0xc00,直接映射就无法让段的第一个字节落在指定的虚拟地址。加载器需要额外复制或调整数据,才能满足这种布局。

ELF 的对齐规则让常见加载器能够直接按页映射。对你给出的例子:

text 复制代码
p_offset % 0x1000 = 0xd98
p_vaddr  % 0x1000 = 0xd98

两者页内偏移相同。严格说,ELF 的一般规则是 p_offset 与 p_vaddr 对 p_align 取模后相同;当 p_align 是常见的页大小 0x1000 时,就表现为页内偏移一致。它不要求二者都必须是页边界。

这个约束并非随意规定,而是由 mmap 的底层机制和"按需分页"的效率目标共同决定的。简单说,这是为了让 PT_LOAD 段能够被"零拷贝"地直接映射到内存,而不需要内核在加载时搬运数据。

直接原因:mmap 的偏移必须页对齐

Linux 的 mmap 系统调用有一个硬性限制:文件映射的起始偏移(offset)必须是系统页大小(通常是 4KB)的整数倍。这是因为内存映射的最小单位是"页",内核在管理映射关系时,只能以页为单位,在文件页和内存页之间建立对应关系。

假设一个 PT_LOAD 段的 p_offset 不是页对齐的(例如 0x2558),那么 mmap 调用会直接失败(EINVAL)。为了让映射能够进行,加载器必须将 p_offset 向下舍入到页边界(0x2000)。

但舍入之后,问题来了:如果只把文件偏移对齐到 0x2000,而虚拟地址 p_vaddr 不做相应的调整,那么文件中位于 0x2000 的页,就会被映射到虚拟地址 p_vaddr 处,这会导致文件中数据的相对位置,与链接时预期的虚拟地址布局发生错位。

同余条件 p_vaddr % PAGE_SIZE == p_offset % PAGE_SIZE 正是为了保证这种错位不会发生 。当两者对页大小同余时,你对两者同时做"向下舍入到页边界"的操作,它们之间的相对偏移量(差值)保持不变。这样,文件中正确的那一页,才会落到虚拟地址中正确的那一页上。

根本目的:实现高效的"按需分页"

满足这个约束,才能享受到现代操作系统加载可执行文件时的核心优化------按需分页(Demand Paging)。

正如 Oracle 文档所指出的,为了达到这种效率,可执行文件和共享库必须 具有文件偏移和虚拟地址同余的段映像。因为 mmap 建立映射时,并不会真的把文件内容读进物理内存。它只是建立了一个"虚拟页 → 文件页"的对应关系。只有当程序第一次访问某个虚拟地址时,CPU 触发缺页中断,内核才会根据这个对应关系,去文件中读取相应的页。

如果 p_vaddr 和 p_offset 不同余,这个"对应关系"就无法以整页为单位干净地建立。内核将不得不在缺页中断的处理路径中,进行额外的数据拷贝和对齐操作,这会显著拖慢按需加载的速度。满足同余条件,内核就可以直接按整块磁盘 I/O 读取文件页,无需任何后续的数据调整,因为对齐在映射建立时就已经天然满足了。

规范层面的体现

这个约束在 ELF 规范中也有明确的对应。ELF 的 p_align 字段表示段在文件和内存中的对齐要求,规范要求 p_vaddr 与 p_offset 必须对 p_align 同余。而为了实现上述的效率,链接器通常会将 p_align 设置为一个足够大的 2 的幂(如 4KB、64KB 甚至 1MB),从而在链接阶段就预先保证了运行时页对齐同余的条件得到满足。

小结

p_vaddr 与 p_offset 页对齐同余,本质上是一个为了换取运行时加载效率而达成的"设计契约" :链接器在生成 ELF 文件时,通过精心安排段在文件中的布局,来"承诺"内核和动态链接器可以用最简单、最快速的 mmap 方式,将这些段直接映射到正确的位置。磁盘上可能因此产生少量空洞,但换来的是程序启动和页面调入时零拷贝、零调整的高效路径。

为什么其他段恰好从页边界开始?

例如一个 ELF 可能有如下几个段:

text 复制代码
LOAD Offset 0x000000 VirtAddr 0x0000 ... R
LOAD Offset 0x001000 VirtAddr 0x1000 ... R E
LOAD Offset 0x002000 VirtAddr 0x2000 ... R
LOAD Offset 0x002d98 VirtAddr 0x3d98 ... RW

前三个段恰好从页边界开始,是链接器选择的布局;第四个段的实际内容起点则落在文件页和虚拟页的页内偏移 0xd98 处。它们都符合映射规则。

第三个段的 p_memsz 是 0x1c8,所以它描述的实际内存范围到 0x21c8 为止。虽然 0x2000 所在页还有剩余空间,但下一段是 RW,第三段是 R。内存保护通常按页设置,把可写数据紧接着塞进同一页,就会使只读数据所在的页也需要可写。链接器通常会将不同用途、不同权限的段放到不同的页区域。

还有一个容易忽略的细节:第三段所在的文件页 是 0x2000,第四段映射时也会使用文件页 0x2000,但把它映射到另一个虚拟页 0x3000。因此,同一个文件页可能出现在进程虚拟地址空间的不同位置:

text 复制代码
文件页 0x2000 → 虚拟页 0x2000  (第三段的映射)
文件页 0x2000 → 虚拟页 0x3000  (第四段的映射)

在第二个映射中,段内容从页内偏移 0xd98 开始,也就是虚拟地址 0x3d98。所以"按页映射"不代表一个文件页只能映射到一个虚拟页;而且页内映射到的字节,也不全都属于该段声明的有效内容范围。

至于为什么链接器把第四段的实际起点放在 0x3d98,而不是另一个位置,单凭程序头表无法完全判断;需要结合 section 布局、链接脚本和链接器策略来看。readelf -SW 可以帮助检查各 section 的地址和文件偏移。

谁负责建立这些映射?

在 Linux 上,执行主 ELF 时,内核读取程序头,并为 PT_LOAD 建立相应的虚拟内存区域。文件页面通常可以按需调入,不需要启动时先把整个文件复制进一块连续缓冲区。如果有 PT_INTERP,内核还会启动动态链接器;动态链接器再负责加载共享库和处理动态链接相关工作。

这和 Windows 的 file-backed section view 在"把文件内容映射到进程地址空间"这一点上有相似之处,但它们属于不同系统的机制和对象模型,不能视为完全对应的结构。

与代码注入器里的镜像构建有什么不同?

本文讨论的是常规 ELF 加载时的文件映射。而前面分析的注入器没有直接把 payload 文件映射进目标进程:它先读取 ELF,再根据 PT_LOAD 把 p_filesz 内容复制到自己构造的镜像中,并把缓冲区的其他部分清零,然后再写进目标进程。

所以,ELF 的页内偏移规则解释的是文件映射如何保留段内位置;注入器则通过复制来重建段的逻辑布局。它是简化的加载过程,不等同于内核完整的 ELF 装载实现。

相关推荐
进击的荆棘12 天前
Linux系统——库的原理与制作之ELF文件的形成与加载的过程(上)
linux·elf·动静态库
Lucis__12 天前
深入理解程序链接与加载—从动静态链接到库加载
linux·elf·链接·程序·动静态库
XLYcmy25 天前
基于图神经网络的物联网固件漏洞静态挖掘技术方案 上
神经网络·物联网·网络安全·二进制·elf·漏洞检测·固件
一只老小白1 个月前
ELF 文件格式从魔数到动态链接:读懂 Linux 可执行文件的每一字节
linux·elf·可执行文件·链接器·二进制格式
handler011 个月前
【Linux】ELF、链接与动态库,程序装载全解析
linux·运维·服务器·chrome·c·elf·文件
IOT那些事儿4 个月前
STM32几种特殊格式文件
map·elf·hex·bin·lnp·axf
程序猿编码5 个月前
vmlinuz 到 vmlinux:不碰源码,徒手重建内核 ELF 符号表
linux·服务器·网络·内核·elf
sulikey5 个月前
ELF文件中的“节“与“段“,如何与虚拟地址空间中的“分页“和“分段“产生联系?
linux·服务器·elf·虚拟地址空间·分页·分段·elf文件
逐梦苍穹5 个月前
何凯明 ELF 论文小白解读:AI 写字下一站是“先画后写“
人工智能·论文·elf·何凯明