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_LOADPT_GNU_RELRO 条目的 VirtAddrMemSizFlags 字段:

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
[...]
  1. 首先所有 PT_LOAD 条目都是进程。它们中的每一个都通过使用 mmap() 触发一个 VMA 的创建。此外,如果 MemSiz > FileSiz,它可能会创建一个额外的匿名 VMA。

  2. 那么所有(实践中只有一次)PT_GNU_RELRO都是流程。它们中的每一个都会触发一个 mprotect() 调用,该调用可能会将现有 VMA 拆分为不同的 VMA。

为了做你想做的,正确的方法可能是模拟 mmapmprotect 调用:

// 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() 调用的内存范围扩展到覆盖整页。