VMA 和 ELF 段之间的关系
relationship between VMA and ELF segments
我需要确定 ELF 可执行文件的可加载段的 VMA。可以从 /proc/pid/maps
打印 VMA。 maps
显示的 VMA 与可加载段之间的关系对我来说也很清楚。每个段由一个或多个 VMA 组成。内核使用什么方法从 ELF 段形成 VMA:它只考虑 permissions/flags 还是还需要其他东西?根据我的理解,带有标志 Read, Execute
(code) 的段将进入具有相同权限的单独 VMA。下一个具有 Read、Write(data) 权限的段应该进入另一个 VMA。但这不是第二个可加载段的情况,它通常分为两个或多个 VMA:一些带有 read and write
,而另一些带有 read only
。因此,我认为标志是 VMA 生成的唯一罪魁祸首的假设似乎是错误的。我需要帮助来理解段和 VMA 之间的这种关系。
我想做的是以编程方式确定 ELF 的可加载段的 VMA,而不将其加载到内存中。所以这个方向的任何pointer/help就是这个post的主要objective。
VMA 是虚拟内存的同构区域:
相同的权限(PROT_EXEC
等);
同类型(MAP_SHARED/MAP_PRIVATE
);
相同的备份文件(如果有);
文件内的一致偏移量。
例如,如果您有一个 RW
的 VMA,并且您 mprotect
PROT_READ
(您删除了写入权限)VMA 中间的一部分,则内核会将 VMA 拆分为三个 VMA(第一个 RW
、第二个 R
和最后一个 RW
)。
让我们看看来自 execu 的典型 VMAtable:
$ cat /proc/$$/maps
00400000-004f2000 r-xp 00000000 08:01 524453 /bin/bash
006f1000-006f2000 r--p 000f1000 08:01 524453 /bin/bash
006f2000-006fb000 rw-p 000f2000 08:01 524453 /bin/bash
006fb000-00702000 rw-p 00000000 00:00 0
[...]
第一个 VMA 是文本段。第二个,第三个和第四个VMA是数据段。
.bss
的匿名映射
在流程开始的时候,你会有这样的东西:
$ cat /proc/$$/maps
00400000-004f2000 r-xp 00000000 08:01 524453 /bin/bash
006f1000-006fb000 rw-p 000f1000 08:01 524453 /bin/bash
006fb000-00702000 rw-p 00000000 00:00 0
[...]
006f1000-006fb000
是来自 executable 文件的文本段的一部分。
006fb000-00702000
不存在于 executable 文件中,因为它最初用零填充。进程的非初始化变量全部组合在一起(在.bss
段),不在executable文件中表示,以便保存space(1).
这来自 executable 文件 (readelf -l
) 的程序头 table 的 PT_LOAD
条目,它描述了要映射到内存中的段:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
[...]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000f1a74 0x00000000000f1a74 R E 200000
LOAD 0x00000000000f1de0 0x00000000006f1de0 0x00000000006f1de0
0x0000000000009068 0x000000000000f298 RW 200000
[...]
如果您查看相应的 PT_LOAD
条目,您会注意到该段的一部分未在文件中表示(因为文件大小小于内存大小)。
不在 executable 文件中的数据段部分用零初始化:动态链接器对这部分数据段使用 MAP_ANONYMOUS
映射。这就是它作为单独的 VMA 出现的原因(它没有相同的支持文件)。
搬迁保护(PT_GNU_RELRO
)
当动态链接器完成重定位 (2) 时,它可能会将数据段的某些部分(.got
部分等)标记为只读以避免 GOT 中毒攻击或错误。程序头 PT_GNU_RELRO
条目描述的重定位后应保护的数据段部分 table:动态链接器 mprotect(addr, len, PROT_READ)
完成重定位后的给定区域 (3 ).此 mprotect
调用将第二个 VMA 拆分为两个 VMA(第一个 R
和第二个 RW
)。
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
[...]
GNU_RELRO 0x00000000000f1de0 0x00000000006f1de0 0x00000000006f1de0
0x0000000000000220 0x0000000000000220 R
[...]
总结
VMA
00400000-004f2000 r-xp 00000000 08:01 524453 /bin/bash
006f1000-006f2000 r--p 000f1000 08:01 524453 /bin/bash
006f2000-006fb000 rw-p 000f2000 08:01 524453 /bin/bash
006fb000-00702000 rw-p 00000000 00:00 0
派生自 PT_LOAD
和 PT_GNU_RELRO
条目的 VirtAddr
、MemSiz
和 Flags
字段:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
[...]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000f1a74 0x00000000000f1a74 R E 200000
LOAD 0x00000000000f1de0 0x00000000006f1de0 0x00000000006f1de0
0x0000000000009068 0x000000000000f298 RW 200000
[...]
GNU_RELRO 0x00000000000f1de0 0x00000000006f1de0 0x00000000006f1de0
0x0000000000000220 0x0000000000000220 R
[...]
首先所有 PT_LOAD
条目都是进程。它们中的每一个都通过使用 mmap()
触发一个 VMA 的创建。此外,如果 MemSiz > FileSiz
,它可能会创建一个额外的匿名 VMA。
那么所有(实践中只有一次)PT_GNU_RELRO
都是流程。它们中的每一个都会触发一个 mprotect()
调用,该调用可能会将现有 VMA 拆分为不同的 VMA。
为了做你想做的,正确的方法可能是模拟 mmap
和 mprotect
调用:
// Virtual Memory Area:
struct Vma {
std::uint64_t addr, length;
std::string file_name;
int prot;
int flags;
std::uint64_t offset;
};
// Virtual Address Space:
class Vas {
private:
std::list<Vma> vmas_;
public:
Vma& mmap(
std::uint64_t addr, std::uint64_t length, int prot,
int flags, int fd, off_t offset);
int mprotect(std::uint64_t addr, std::uint64_t len, int prot);
std::list<Vma> const& vmas() const { return vmas_; }
};
for (Elf32_Phdr const& h : phdrs)
if (h.p_type == PT_LOAD) {
vas.mmap(...);
if (anon_size)
vas.mmap(...);
}
for (Elf32_Phdr const& h : phdrs)
if (h.p_type == PT_GNU_RELRO)
vas.mprotect(...);
一些计算示例
地址略有不同,因为 VMA 是页面对齐的 (3)(对于 x86 和 x86_64 使用 4Kio = 0x1000 页):
第一个 VMA 由第一个 PT_LOAD
条目描述:
vma[0].start = page_floor(load[0].virt_addr)
= 0x400000
vma[0].end = page_ceil(load[1].virt_addr + load[1].phys_size)
= page_ceil(0x400000 + 0xf1a74)
= page_ceil(0x4f1a74)
= 0x4f2000
下一个VMA是被保护的数据段的一部分,描述为PT_GNU_RELRO
:
vma[1].start = page_floor(relro[0].virt_addr)
= page_floor(0xf1de0)
= 0x6f1000
vma[1].end = page_ceil(relro[0].virt_addr + relo[0].mem_size)
= page_ceil(0x6f1de0 + 0x220)
= page_ceil(0x6f2000)
= 0x6f2000
[...]
与章节对应
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
0000000000004894 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 0000000000404b30 00004b30
000000000000d6c8 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 00000000004121f8 000121f8
0000000000008c25 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 000000000041ae1e 0001ae1e
00000000000011e6 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 000000000041c008 0001c008
00000000000000b0 0000000000000000 A 6 2 8
[ 9] .rela.dyn RELA 000000000041c0b8 0001c0b8
00000000000000c0 0000000000000018 A 5 0 8
[10] .rela.plt RELA 000000000041c178 0001c178
00000000000013f8 0000000000000018 AI 5 12 8
[11] .init PROGBITS 000000000041d570 0001d570
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 000000000041d590 0001d590
0000000000000d60 0000000000000010 AX 0 0 16
[13] .text PROGBITS 000000000041e2f0 0001e2f0
0000000000099c42 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 00000000004b7f34 000b7f34
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 00000000004b7f40 000b7f40
000000000001ebb0 0000000000000000 A 0 0 64
[16] .eh_frame_hdr PROGBITS 00000000004d6af0 000d6af0
000000000000407c 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 00000000004dab70 000dab70
0000000000016f04 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 00000000006f1de0 000f1de0
0000000000000008 0000000000000000 WA 0 0 8
[19] .fini_array FINI_ARRAY 00000000006f1de8 000f1de8
0000000000000008 0000000000000000 WA 0 0 8
[20] .jcr PROGBITS 00000000006f1df0 000f1df0
0000000000000008 0000000000000000 WA 0 0 8
[21] .dynamic DYNAMIC 00000000006f1df8 000f1df8
0000000000000200 0000000000000010 WA 6 0 8
[22] .got PROGBITS 00000000006f1ff8 000f1ff8
0000000000000008 0000000000000008 WA 0 0 8
[23] .got.plt PROGBITS 00000000006f2000 000f2000
00000000000006c0 0000000000000008 WA 0 0 8
[24] .data PROGBITS 00000000006f26c0 000f26c0
0000000000008788 0000000000000000 WA 0 0 64
[25] .bss NOBITS 00000000006fae80 000fae48
00000000000061f8 0000000000000000 WA 0 0 64
[26] .shstrtab STRTAB 0000000000000000 000fae48
00000000000000ef 0000000000000000 0 0 1
如果您将部分的地址 (readelf -S
) 与 VMA 的范围进行比较,您会发现映射:
00400000-004f2000 r-xp /bin/bash : .interp, .note.ABI-tag, .note.gnu.build-id, .gnu.hash, .dynsym, .dynstr, .gnu.version, .gnu.version_r, .rela.dyn, .rela.plt, .init, .plt, .text, .fini, .rodata.eh_frame_hdr, .eh_frame
006f1000-006f2000 r--p /bin/bash : .init_array, .fini_array, .jcr, .dynamic, .got
006f2000-006fb000 rw-p /bin/bash : .got.plt, .data, beginning of .bss
006fb000-00702000 rw-p - : rest of .bss
备注
(1):事实上,它更复杂:出于页面对齐的原因,.bss
部分的一部分可能在 executable 文件中表示。
(2): 事实上,当它完成非惰性重定位时。
(3):MMU 操作使用页面粒度,因此 mmap()
、mprotect()
、munmap()
调用的内存范围扩展到覆盖整页。
我需要确定 ELF 可执行文件的可加载段的 VMA。可以从 /proc/pid/maps
打印 VMA。 maps
显示的 VMA 与可加载段之间的关系对我来说也很清楚。每个段由一个或多个 VMA 组成。内核使用什么方法从 ELF 段形成 VMA:它只考虑 permissions/flags 还是还需要其他东西?根据我的理解,带有标志 Read, Execute
(code) 的段将进入具有相同权限的单独 VMA。下一个具有 Read、Write(data) 权限的段应该进入另一个 VMA。但这不是第二个可加载段的情况,它通常分为两个或多个 VMA:一些带有 read and write
,而另一些带有 read only
。因此,我认为标志是 VMA 生成的唯一罪魁祸首的假设似乎是错误的。我需要帮助来理解段和 VMA 之间的这种关系。
我想做的是以编程方式确定 ELF 的可加载段的 VMA,而不将其加载到内存中。所以这个方向的任何pointer/help就是这个post的主要objective。
VMA 是虚拟内存的同构区域:
相同的权限(
PROT_EXEC
等);同类型(
MAP_SHARED/MAP_PRIVATE
);相同的备份文件(如果有);
文件内的一致偏移量。
例如,如果您有一个 RW
的 VMA,并且您 mprotect
PROT_READ
(您删除了写入权限)VMA 中间的一部分,则内核会将 VMA 拆分为三个 VMA(第一个 RW
、第二个 R
和最后一个 RW
)。
让我们看看来自 execu 的典型 VMAtable:
$ cat /proc/$$/maps 00400000-004f2000 r-xp 00000000 08:01 524453 /bin/bash 006f1000-006f2000 r--p 000f1000 08:01 524453 /bin/bash 006f2000-006fb000 rw-p 000f2000 08:01 524453 /bin/bash 006fb000-00702000 rw-p 00000000 00:00 0 [...]
第一个 VMA 是文本段。第二个,第三个和第四个VMA是数据段。
.bss
的匿名映射
在流程开始的时候,你会有这样的东西:
$ cat /proc/$$/maps 00400000-004f2000 r-xp 00000000 08:01 524453 /bin/bash 006f1000-006fb000 rw-p 000f1000 08:01 524453 /bin/bash 006fb000-00702000 rw-p 00000000 00:00 0 [...]
006f1000-006fb000
是来自 executable 文件的文本段的一部分。006fb000-00702000
不存在于 executable 文件中,因为它最初用零填充。进程的非初始化变量全部组合在一起(在.bss
段),不在executable文件中表示,以便保存space(1).
这来自 executable 文件 (readelf -l
) 的程序头 table 的 PT_LOAD
条目,它描述了要映射到内存中的段:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align [...] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000f1a74 0x00000000000f1a74 R E 200000 LOAD 0x00000000000f1de0 0x00000000006f1de0 0x00000000006f1de0 0x0000000000009068 0x000000000000f298 RW 200000 [...]
如果您查看相应的 PT_LOAD
条目,您会注意到该段的一部分未在文件中表示(因为文件大小小于内存大小)。
不在 executable 文件中的数据段部分用零初始化:动态链接器对这部分数据段使用 MAP_ANONYMOUS
映射。这就是它作为单独的 VMA 出现的原因(它没有相同的支持文件)。
搬迁保护(PT_GNU_RELRO
)
当动态链接器完成重定位 (2) 时,它可能会将数据段的某些部分(.got
部分等)标记为只读以避免 GOT 中毒攻击或错误。程序头 PT_GNU_RELRO
条目描述的重定位后应保护的数据段部分 table:动态链接器 mprotect(addr, len, PROT_READ)
完成重定位后的给定区域 (3 ).此 mprotect
调用将第二个 VMA 拆分为两个 VMA(第一个 R
和第二个 RW
)。
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align [...] GNU_RELRO 0x00000000000f1de0 0x00000000006f1de0 0x00000000006f1de0 0x0000000000000220 0x0000000000000220 R [...]
总结
VMA
00400000-004f2000 r-xp 00000000 08:01 524453 /bin/bash 006f1000-006f2000 r--p 000f1000 08:01 524453 /bin/bash 006f2000-006fb000 rw-p 000f2000 08:01 524453 /bin/bash 006fb000-00702000 rw-p 00000000 00:00 0
派生自 PT_LOAD
和 PT_GNU_RELRO
条目的 VirtAddr
、MemSiz
和 Flags
字段:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align [...] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000f1a74 0x00000000000f1a74 R E 200000 LOAD 0x00000000000f1de0 0x00000000006f1de0 0x00000000006f1de0 0x0000000000009068 0x000000000000f298 RW 200000 [...] GNU_RELRO 0x00000000000f1de0 0x00000000006f1de0 0x00000000006f1de0 0x0000000000000220 0x0000000000000220 R [...]
首先所有
PT_LOAD
条目都是进程。它们中的每一个都通过使用mmap()
触发一个 VMA 的创建。此外,如果MemSiz > FileSiz
,它可能会创建一个额外的匿名 VMA。那么所有(实践中只有一次)
PT_GNU_RELRO
都是流程。它们中的每一个都会触发一个mprotect()
调用,该调用可能会将现有 VMA 拆分为不同的 VMA。
为了做你想做的,正确的方法可能是模拟 mmap
和 mprotect
调用:
// Virtual Memory Area:
struct Vma {
std::uint64_t addr, length;
std::string file_name;
int prot;
int flags;
std::uint64_t offset;
};
// Virtual Address Space:
class Vas {
private:
std::list<Vma> vmas_;
public:
Vma& mmap(
std::uint64_t addr, std::uint64_t length, int prot,
int flags, int fd, off_t offset);
int mprotect(std::uint64_t addr, std::uint64_t len, int prot);
std::list<Vma> const& vmas() const { return vmas_; }
};
for (Elf32_Phdr const& h : phdrs)
if (h.p_type == PT_LOAD) {
vas.mmap(...);
if (anon_size)
vas.mmap(...);
}
for (Elf32_Phdr const& h : phdrs)
if (h.p_type == PT_GNU_RELRO)
vas.mprotect(...);
一些计算示例
地址略有不同,因为 VMA 是页面对齐的 (3)(对于 x86 和 x86_64 使用 4Kio = 0x1000 页):
第一个 VMA 由第一个 PT_LOAD
条目描述:
vma[0].start = page_floor(load[0].virt_addr)
= 0x400000
vma[0].end = page_ceil(load[1].virt_addr + load[1].phys_size)
= page_ceil(0x400000 + 0xf1a74)
= page_ceil(0x4f1a74)
= 0x4f2000
下一个VMA是被保护的数据段的一部分,描述为PT_GNU_RELRO
:
vma[1].start = page_floor(relro[0].virt_addr)
= page_floor(0xf1de0)
= 0x6f1000
vma[1].end = page_ceil(relro[0].virt_addr + relo[0].mem_size)
= page_ceil(0x6f1de0 + 0x220)
= page_ceil(0x6f2000)
= 0x6f2000
[...]
与章节对应
Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000400238 00000238 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.ABI-tag NOTE 0000000000400254 00000254 0000000000000020 0000000000000000 A 0 0 4 [ 3] .note.gnu.build-i NOTE 0000000000400274 00000274 0000000000000024 0000000000000000 A 0 0 4 [ 4] .gnu.hash GNU_HASH 0000000000400298 00000298 0000000000004894 0000000000000000 A 5 0 8 [ 5] .dynsym DYNSYM 0000000000404b30 00004b30 000000000000d6c8 0000000000000018 A 6 1 8 [ 6] .dynstr STRTAB 00000000004121f8 000121f8 0000000000008c25 0000000000000000 A 0 0 1 [ 7] .gnu.version VERSYM 000000000041ae1e 0001ae1e 00000000000011e6 0000000000000002 A 5 0 2 [ 8] .gnu.version_r VERNEED 000000000041c008 0001c008 00000000000000b0 0000000000000000 A 6 2 8 [ 9] .rela.dyn RELA 000000000041c0b8 0001c0b8 00000000000000c0 0000000000000018 A 5 0 8 [10] .rela.plt RELA 000000000041c178 0001c178 00000000000013f8 0000000000000018 AI 5 12 8 [11] .init PROGBITS 000000000041d570 0001d570 000000000000001a 0000000000000000 AX 0 0 4 [12] .plt PROGBITS 000000000041d590 0001d590 0000000000000d60 0000000000000010 AX 0 0 16 [13] .text PROGBITS 000000000041e2f0 0001e2f0 0000000000099c42 0000000000000000 AX 0 0 16 [14] .fini PROGBITS 00000000004b7f34 000b7f34 0000000000000009 0000000000000000 AX 0 0 4 [15] .rodata PROGBITS 00000000004b7f40 000b7f40 000000000001ebb0 0000000000000000 A 0 0 64 [16] .eh_frame_hdr PROGBITS 00000000004d6af0 000d6af0 000000000000407c 0000000000000000 A 0 0 4 [17] .eh_frame PROGBITS 00000000004dab70 000dab70 0000000000016f04 0000000000000000 A 0 0 8 [18] .init_array INIT_ARRAY 00000000006f1de0 000f1de0 0000000000000008 0000000000000000 WA 0 0 8 [19] .fini_array FINI_ARRAY 00000000006f1de8 000f1de8 0000000000000008 0000000000000000 WA 0 0 8 [20] .jcr PROGBITS 00000000006f1df0 000f1df0 0000000000000008 0000000000000000 WA 0 0 8 [21] .dynamic DYNAMIC 00000000006f1df8 000f1df8 0000000000000200 0000000000000010 WA 6 0 8 [22] .got PROGBITS 00000000006f1ff8 000f1ff8 0000000000000008 0000000000000008 WA 0 0 8 [23] .got.plt PROGBITS 00000000006f2000 000f2000 00000000000006c0 0000000000000008 WA 0 0 8 [24] .data PROGBITS 00000000006f26c0 000f26c0 0000000000008788 0000000000000000 WA 0 0 64 [25] .bss NOBITS 00000000006fae80 000fae48 00000000000061f8 0000000000000000 WA 0 0 64 [26] .shstrtab STRTAB 0000000000000000 000fae48 00000000000000ef 0000000000000000 0 0 1
如果您将部分的地址 (readelf -S
) 与 VMA 的范围进行比较,您会发现映射:
00400000-004f2000 r-xp /bin/bash : .interp, .note.ABI-tag, .note.gnu.build-id, .gnu.hash, .dynsym, .dynstr, .gnu.version, .gnu.version_r, .rela.dyn, .rela.plt, .init, .plt, .text, .fini, .rodata.eh_frame_hdr, .eh_frame 006f1000-006f2000 r--p /bin/bash : .init_array, .fini_array, .jcr, .dynamic, .got 006f2000-006fb000 rw-p /bin/bash : .got.plt, .data, beginning of .bss 006fb000-00702000 rw-p - : rest of .bss
备注
(1):事实上,它更复杂:出于页面对齐的原因,.bss
部分的一部分可能在 executable 文件中表示。
(2): 事实上,当它完成非惰性重定位时。
(3):MMU 操作使用页面粒度,因此 mmap()
、mprotect()
、munmap()
调用的内存范围扩展到覆盖整页。