ELF程序头

ELF程序头是对二进制文件中段(segment)的描述,是程序装载必需的部分。

程序头主要描述了程序执行时在内存中的布局

段是在内核装载时被解析的。描述了磁盘上可执行文件的内存布局以及如何映射到内存中,可以通过引用原始ELF头中名为e_phoff的偏移量来得到程序头表。

重定位文件(即elf文件类型为 ET_REL)是没有程序头的,x86-64 程序头结构体如下

typedef struct {
  unsigned int p_type;      /* 程序头类型 */
  unsigned int p_flags;     /* 权限标记-R W E */
  unsigned long p_offset;   /* 段文件偏移 */
  unsigned long p_vaddr;    /* 段映射到内存的虚拟地址 */
  unsigned long p_paddr;    /* 一般跟上面一样 */
  unsigned long p_filesz;   /* 段在文件里的大小 */
  unsigned long p_memsz;    /* 一般跟上面一样 */
  unsigned long p_align;    /* 段对齐 */
} Elf_Internal_Phdr;

下面为5中常见的程序头类型,程序头描述了可执行文件(包括共享库)中段及其类型(为哪种类型的数据或代码而保留的段)

PT_LOAD

这种类型的段会被装载或者映射到内存中,一个动态链接的可执行文件包含以下两个可装载的段

PT_DYNAMIC

动态段是动态链接可执行文件所特有的,包含了动态链接器所需要的一些信息,它包括以下内容

...

c结构体如下

typedef struct elf_internal_dyn {
  unsigned long d_tag;
  union {
    unsigned long d_val;
    unsigned long d_ptr;
  } d_un;
} Elf_Internal_Dyn;

下面命令为打印出动态段表内容,即下面的 04 type:DYNAMIC 段里的内容

readelf -d main
Dynamic section at offset 0xef0 contains 12 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libdy1.so]
 0x000000006ffffef5 (GNU_HASH)           0x400278
 0x0000000000000005 (STRTAB)             0x400320
 0x0000000000000006 (SYMTAB)             0x4002a8
 0x000000000000000a (STRSZ)              42 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x601000
 0x0000000000000002 (PLTRELSZ)           24 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x400350
 0x0000000000000000 (NULL)               0x0

PT_NOTE

可以保存于特定供应商或者系统相关附加信息,这个段在程序运行时是不需要的,因为系统会假设可执行文件是本地的,这个段很容易被感染

PT_INTERP

存放一个以null结尾的字符串位置和大小信息,是对解释器位置的描述

PT_PHDR

保存了程序头本身的位置,phdr表保存了所有phdr


下面打印出一个可执行程序的程序头信息(即段信息),下面的输出是经过简化处理了

[root@izbp1irxwqt7ei21awv6wvz dyn]# readelf -l a.out
9 program headers,offset at 64

00 type:PHDR            offset:0x40         filesz:0x1f8        vaddr:0x400040     memsz:0x1f8        align:0x8          flags:R E
01 type:INTERP          offset:0x238        filesz:0x1c         vaddr:0x400238     memsz:0x1c         align:0x1          flags:R  
   program interpreter[/lib64/ld-linux-x86-64.so.2]
02 type:LOAD            offset:0x0          filesz:0x71c        vaddr:0x400000     memsz:0x71c        align:0x200000     flags:R E
03 type:LOAD            offset:0xe10        filesz:0x228        vaddr:0x600e10     memsz:0x230        align:0x200000     flags:RW 
04 type:DYNAMIC         offset:0xe28        filesz:0x1d0        vaddr:0x600e28     memsz:0x1d0        align:0x8          flags:RW 
05 type:NOTE            offset:0x254        filesz:0x44         vaddr:0x400254     memsz:0x44         align:0x4          flags:R  
06 type:GNU_EH_FRAME    offset:0x5f0        filesz:0x34         vaddr:0x4005f0     memsz:0x34         align:0x4          flags:R  
07 type:GNU_STACK       offset:0x0          filesz:0x0          vaddr:0x0          memsz:0x0          align:0x10         flags:RW 
08 type:GNU_RELRO       offset:0xe10        filesz:0x1f0        vaddr:0x600e10     memsz:0x1f0        align:0x1          flags:R

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .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 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got

02:LOAD即为 text段,权限为可读可执行, 文件 0 - 0x71c 加载到虚拟内存 0x400000 - 0x40071c

03:LOAD即为 data段,为第二个需要加载进内存的段,由于是 0x200000 对齐,所以从 0x600000 开始,权限为读写,文件 0xe10 - 0x1038 加载到虚拟内存 0x600e10 - 0x601038

内存对齐公式,(vaddr - offset) % align == 0

可以结合打印出节命令 readelf -S main 来阅读

由于代码段是只读的,所以 .rodata节(存放只读数据) 也存放在代码段里

.bss节 是存放未初始化的全局变量或者初始化为0的全局变量, .data节 是存放初始化为非0的全局变量,它们都存放在 data段。

接下来查看下该程序在运行时,内存的实际映射

[root@izbp1irxwqt7ei21awv6wvz dyn]# cat /proc/32467/maps
00400000-00401000 r-xp 00000000 fd:01 138966                             /root/elf_write/dyn/a.out
00600000-00601000 r--p 00000000 fd:01 138966                             /root/elf_write/dyn/a.out
00601000-00602000 rw-p 00001000 fd:01 138966                             /root/elf_write/dyn/a.out
...(这里是一些共享库的内存映射,略过)

代码段在内存中是以页(一般为4096字节)为单位的,虽然上面的代码段不足一个页,只有0x71c字节,但是实际会用到0x1000(4096)字节,所以 0x400000-0x401000 内存的权限为可读可执行

数据段大小占用为 0x600e10 - 0x601038 超过了一个页,所以需要有2个页,那么为啥 00600000-00601000 只需要读权限,而 00601000-00602000 需要读写权限

从上面我们知道数据段包含了.init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 8个节,通过 readelf -S a.out 打印出节的信息可知,.init_array .fini_array .jcr .dynamic .got 在第一个页(这些节不需要写权限),.got.plt .data .bss 在第二个页(这里的 .got.plt 和 .data 节都需要写权限)


后面文章将会有实例继续补充说明


上一篇: ELF重定位
下一篇: ELF动态链接
作者邮箱: 203328517@qq.com