【问题标题】:relationship between VMA and ELF segmentsVMA 和 ELF 段之间的关系
【发布时间】:2016-02-18 18:36:55
【问题描述】:

我需要确定 ELF 可执行文件的可加载段的 VMA。 VMA 可以从/proc/pid/maps 打印。 maps 显示的 VMA 与可加载段之间的关系我也很清楚。每个段由一个或多个 VMA 组成。内核使用什么方法从 ELF 段形成 VMA:它是否只考虑权限/标志或其他东西是必需的?据我了解,带有 Read, Execute(code) 标志的段将进入具有相同权限的单独 VMA。而具有 Read, Write(data) 权限的下一个段应该进入另一个 VMA。但第二个可加载段不是这种情况,它通常被分成两个或多个 VMA:一些使用read and write,而另一些使用read only。因此,我认为标志是 VMA 生成的唯一罪魁祸首的假设似乎是错误的。我需要帮助来理解分段和 VMA 之间的这种关系。

我想要做的是以编程方式确定 ELF 可加载段的 VMA,而不是将其加载到内存中。所以这个方向的任何指针/帮助都是这篇文章的主要目标。

【问题讨论】:

    标签: c unix memory execution elf


    【解决方案1】:

    VMA 是虚拟内存的同质区域,具有:

    • 相同的权限(PROT_EXEC等);

    • 同类型(MAP_SHARED/MAP_PRIVATE);

    • 相同的备份文件(如果有);

    • 文件中的一致偏移量。

    例如,如果你有一个 VMA 是 RW 并且你 mprotect PROT_READ(你删除了写权限)在 VMA 中间的一部分,内核会将 VMA 分成三个 VMA (第一个是RW,第二个是R,最后一个是RW)。

    让我们看一下来自可执行文件的典型 VMA:

    $ 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是来自可执行文件的文本段部分。

    • 006fb000-00702000 不存在于可执行文件中,因为它最初用零填充。进程的未初始化变量全部组合在一起(在.bss段中),为了节省空间,不在可执行文件中表示(1)。

    这来自可执行文件 (readelf -l) 的程序头表的 PT_LOAD 条目,它描述了要映射到内存中的段:

    类型 偏移 VirtAddr PhysAddr FileSiz MemSiz 标志对齐 [...] 加载 0x00000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000f1a74 0x00000000000f1a74 R E 200000 加载 0x00000000000f1de0 0x00000000006f1de0 0x00000000006f1de0 0x0000000000009068 0x000000000000f298 读写 200000 [...]

    如果您查看对应的PT_LOAD 条目,您会注意到该段的一部分没有在文件中表示(因为文件大小小于内存大小)。

    不在可执行文件中的数据段部分用零初始化:动态链接器对这部分数据段使用MAP_ANONYMOUS 映射。这就是为什么它显示为一个单独的 VMA(它没有相同的支持文件)。

    重定位保护(PT_GNU_RELRO)

    当动态链接器完成重定位 (2) 后,它可能会将数据段的某些部分(.got 等部分)标记为只读,以避免 GOT 中毒攻击或错误。在程序头表的PT_GNU_RELRO 条目中描述的重定位后应保护的数据段部分:动态链接器mprotect(addr, len, PROT_READ) 完成重定位后的给定区域(3)。这个mprotect 调用将第二个VMA 分成两个VMA(第一个R 和第二个RW)。

    类型 偏移 VirtAddr PhysAddr FileSiz MemSiz 标志对齐 [...] 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 字段:

    类型 偏移 VirtAddr PhysAddr FileSiz MemSiz 标志对齐 [...] 加载 0x00000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000f1a74 0x00000000000f1a74 R E 200000 加载 0x00000000000f1de0 0x00000000006f1de0 0x00000000006f1de0 0x0000000000009068 0x000000000000f298 读写 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
    

    [...]

    与各部分的对应

    部分标题:
      [Nr] 名称类型地址偏移量
           大小 EntSize 标志链接信息对齐
      [0]空0000000000000000 00000000
           0000000000000000 0000000000000000 0 0 0
      [1] .interp 程序 0000000000400238 00000238
           000000000000001c 0000000000000000 A 0 0 1
      [ 2] .note.ABI 标记注 0000000000400254 00000254
           0000000000000020 0000000000000000 A 0 0 4
      [3] .note.gnu.build-i 注释 0000000000400274 00000274
           0000000000000024 0000000000000000 A 0 0 4
      [4] .gnu.hash GNU_HASH 0000000000400298 00000298
           0000000000004894 0000000000000000 A 5 0 8
      [5] .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 人工智能 5 12 8
      [11] .init 程序 000000000041d570 0001d570
           000000000000001a 0000000000000000 AX 0 0 4
      [12] .plt 程序 000000000041d590 0001d590
           0000000000000d60 0000000000000010 AX 0 0 16
      [13] .text 程序 000000000041e2f0 0001e2f0
           0000000000099c42 0000000000000000 AX 0 0 16
      [14] .fini 程序 00000000004b7f34 000b7f34
           0000000000000009 0000000000000000 AX 0 0 4
      [15] .rodata 程序 00000000004b7f40 000b7f40
           000000000001ebb0 0000000000000000 A 0 0 64
      [16] .eh_frame_hdr 程序 00000000004d6af0 000d6af0
           000000000000407c 0000000000000000 A 0 0 4
      [17] .eh_frame 程序 00000000004dab70 000dab70
           0000000000016f04 0000000000000000 A 0 0 8
      [18] .init_array INIT_ARRAY 00000000006f1de0 000f1de0
           0000000000000008 0000000000000000 西澳 0 0 8
      [19] .fini_array FINI_ARRAY 00000000006f1de8 000f1de8
           0000000000000008 0000000000000000 西澳 0 0 8
      [20] .jcr 程序 00000000006f1df0 000f1df0
           0000000000000008 0000000000000000 西澳 0 0 8
      [21] .动态动态 00000000006f1df8 000f1df8
           0000000000000200 0000000000000010 西澳 6 0 8
      [22] .得到程序 00000000006f1ff8 000f1ff8
           0000000000000008 0000000000000008 西澳 0 0 8
      [23] .got.plt 程序 00000000006f2000 000f2000
           00000000000006c0 0000000000000008 WA 0 0 8
      [24] .data 程序 00000000006f26c0 000f26c0
           0000000000008788 0000000000000000 西澳 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, .bss 开头 006fb000-00702000 rw-p - : .bss 的其余部分

    注意事项

    (1):事实上,它更复杂:出于页面对齐的原因,.bss 部分的一部分可能会在可执行文件中表示。

    (2):实际上,当它完成非惰性重定位时。

    (3):MMU 操作使用页面粒度,因此mmap()mprotect()munmap() 调用的内存范围扩展为覆盖整页。

    【讨论】:

    • 感谢您的全面回复。我得到了一些新知识,但恐怕它不能回答我的问题
    • 嗯,答案是 VMA 的位置由 PT_LOAD 和 PT_GNU_RELRO 条目给出。
    • 啊,好吧,我尝试通过在早上跟踪地址来吸收这一点。再次感谢您的努力。
    • 如果你想说 006f1000-006fb000 是数据段的一部分(你输入了文本),请纠正我。我得到的是数据段既有初始化数据又有未初始化数据。在拆分和 mprotect 之后,第二个 vma(第一个数据 vma 006f1000-006f2000)代表 .data 部分,而第三个 vma 006f2000-006fb000 代表.bss。分割的位置可以由.bss section addr决定。哪个 vma 将只读由 .got 决定
    • 006fb000-00702000 代表堆?我声明了 100 个整数的未初始化数组,当我声明包含 1000 个整数的数组时,它显示在地图中。在数组声明代码之前和之后使用 malloc 但没有 1000 个整数的数组,就没有堆。堆与.bss的连接是什么?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-06
    • 2016-06-10
    • 2013-02-03
    相关资源
    最近更新 更多