ELF可执行文件PT_LOAD段与/proc/pid/maps内存映射差异疑问
提问:ELF加载时PT_LOAD段与/proc/maps显示段数差异的原因
我正在研究Linux内核中ELF二进制文件的加载机制,对PT_LOAD段的内存加载方式存在疑惑。我有一个名为test.elf的ELF可执行文件,通过readelf -l查看其程序头,显示包含4个PT_LOAD段:
root@ubuntu-s-4vcpu-8gb-amd-sgp1-01:~/C# readelf -l test.elf Elf file type is DYN (Position-Independent Executable file) Entry point 0x10c0 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 0x0000000000000718 0x0000000000000718 R 0x1000 LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 0x0000000000000259 0x0000000000000259 R E 0x1000 LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 0x0000000000000124 0x0000000000000124 R 0x1000 LOAD 0x0000000000002da0 0x0000000000003da0 0x0000000000003da0 0x0000000000000274 0x0000000000000278 RW 0x1000 DYNAMIC 0x0000000000002db0 0x0000000000003db0 0x0000000000003db0 0x00000000000001f0 0x00000000000001f0 RW 0x8 NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000030 R 0x8
我原本预期该程序加载到内存后对应4个段,但查看/proc/209183/maps时发现,运行中的进程从该可执行文件加载了5个段:
root@ubuntu-s-4vcpu-8gb-amd-sgp1-01:~/C# cat /proc/209183/maps 55acd5d31000-55acd5d32000 r--p 00000000 fc:01 517731 /root/C/test.elf 55acd5d32000-55acd5d33000 r-xp 00001000 fc:01 517731 /root/C/test.elf 55acd5d33000-55acd5d34000 r--p 00002000 fc:01 517731 /root/C/test.elf 55acd5d34000-55acd5d35000 r--p 00002000 fc:01 517731 /root/C/test.elf 55acd5d35000-55acd5d36000 rw-p 00003000 fc:01 517731 /root/C/test.elf 55acd6e88000-55acd6ea9000 rw-p 00000000 00:00 0 [heap] 7f85da4fe000-7f85da501000 rw-p 00000000 00:00 0 7f85da501000-7f85da529000 r--p 00000000 fc:01 31232 /usr/lib/x86_64-linux-gnu/libc.so.6 7f85da529000-7f85da6be000 r-xp 00028000 fc:01 31232 /usr/lib/x86_64-linux-gnu/libc.so.6 7f85da6be000-7f85da716000 r--p 001bd000 fc:01 31232 /usr/lib/x86_64-linux-gnu/libc.so.6
我还注意到其中第三个和第四个段具有相同的偏移量和权限。我已查阅相关资料并检查了段中包含的节,但未发现特殊之处:
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
希望有人能解释这一差异的原因!我原本预期仅存在4个段。
回答
核心原因:内存页权限约束与GOT安全拆分
1. 内存页是权限控制的最小单位
Linux内核不允许在同一个内存页(通常4KB)中设置不同的权限。当PT_LOAD段的映射范围跨越内存页,且不同页的权限需求冲突时,内核会将单个文件页拆分为多个独立的内存映射。
看你的第四个PT_LOAD段(RW权限):
- 它的文件起始偏移
0x2da0属于文件的0x2000-0x3000页(该页归第三个只读PT_LOAD段所有) - 对应的内存起始地址
0x3da0落在0x3000-0x4000内存页中 - 内核为了保证内存页权限统一,会将
0x3000-0x3da0区间映射为只读(继承原文件页的R权限),而0x3da0-0x4000及后续页映射为可写(遵循RW段的权限),这就额外生成了一个只读映射段。
2. 只读GOT的安全机制
现代GNU链接器为了防御攻击,会将全局偏移表(GOT)拆分为只读静态绑定区和可写动态绑定区:
- 只读区存储无需运行时修改的静态绑定地址,属于
.got节的一部分 - 可写区存储需要动态链接时更新的地址
你的第四个PT_LOAD段包含了.got节,其中的只读区会被内核单独映射为只读内存页,与可写区分开,这也是导致内存段数增加的原因之一。
对重复偏移段的解释
你看到的两个偏移0x2000的只读段,本质是同一个文件页(0x2000-0x3000)的不同内存映射:
- 第一个对应第三个PT_LOAD段的有效内容区间(
0x2000-0x2124) - 第二个对应该文件页的剩余空洞部分,映射到内存的
0x3000-0x4000区间,权限保持只读
内容的提问来源于stack exchange,提问作者Anh Phan
相关产品推荐
相关产品推荐

