ELF文件格式


ELF可执行可链接格式,最初由UNIX系统实验室作为应用程序二进制接口的一部分制定,是COFF(common file format)的变种,相关定义在/usr/include/elf.h文件中。

ELF 文件类型

说明:linux和windows识别文件格式不同,linux是根据文件头的字段,windows直接根据文件名后缀。

主要分为三类:

  • 可执行文件executable file:经过链接、可执行的目标文件,也被称为程序。
  • 可重定位文(relocatable file):由源文件编译但是没有链接的目标文件,通常以.o作为拓展名。用于与其他文件链接构成可执行文件或动态链接库,通常是一段位置独立的代码(Position Independent code, PIC)
  • 共享目标文件(shared object file):动态链接库文件,用于在链接的过程中与其他动态链接库或可重定位文件一起构建可执行文件。

除此之外,还存在 核心转储文件(core dump file)作为进程以外终止时地址空间的转储,也是ELF文件的一种。可以使用gdb工具读取辅助调试和查找程序崩溃的原因。

在内核中的定义(/include/uapi/linux/elf.h):

  #define  ET_REL     1
  #define  ET_EXEC   2
  #define  ET_DYN    3
  #define  ET_CORE  4

ELF文件格式

总体布局

ELF文件格式提供了两种视图,分别是链接视图和执行视图。链接视图是以节(section)为单位执行视图是以段(segment)为单位。接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。

可以使用readelf -l hello查看一个链接后的elf可执行文件,Section to Segment 的映射关系

Elf 文件类型为 EXEC (可执行文件)
Entry point 0x401cc0
There are 8 program headers, starting at offset 64

程序头:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000488 0x0000000000000488  R      0x1000
  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000
                 0x000000000007fab1 0x000000000007fab1  R E    0x1000
  LOAD           0x0000000000081000 0x0000000000481000 0x0000000000481000
                 0x0000000000026e0b 0x0000000000026e0b  R      0x1000
  LOAD           0x00000000000a8000 0x00000000004a9000 0x00000000004a9000
                 0x00000000000052f0 0x0000000000006b20  RW     0x1000
  NOTE           0x0000000000000200 0x0000000000400200 0x0000000000400200
                 0x0000000000000044 0x0000000000000044  R      0x4
  TLS            0x00000000000a8000 0x00000000004a9000 0x00000000004a9000
                 0x0000000000000020 0x0000000000000060  R      0x8
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x00000000000a8000 0x00000000004a9000 0x00000000004a9000
                 0x0000000000003000 0x0000000000003000  R      0x1

 Section to Segment mapping:
  段节...
   00     .note.gnu.build-id .note.ABI-tag .rela.plt 
   01     .init .plt .text __libc_freeres_fn .fini 
   02     .rodata .eh_frame .gcc_except_table 
   03     .tdata .init_array .fini_array .data.rel.ro .got .got.plt .data __libc_subfreeres __libc_IO_vtables __libc_atexit .bss __libc_freeres_ptrs 
   04     .note.gnu.build-id .note.ABI-tag 
   05     .tdata .tbss 
   06     
   07     .tdata .init_array .fini_array .data.rel.ro .got 

下面的段序号和上面程序头里的段一一对应。

文件结构

主要从链接的角度看elf文件。

elf头

描述文件的一些基本信息,文件头部存在魔术字符7f 45 4c 46即字符串"\177ELF",当文件被映射到内存时可以通过该字符串查找位置,dump内存。

readelf -h relocfile //查看文件头信息
ELF 头:
  Magic:  7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              REL (可重定位文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:              0x0
  程序头起点:              0 (bytes into file)
  Start of section headers:          816 (bytes into file)
  标志:             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 12
typedef struct elf32_hdr
{
	  unsigned char	e_ident[EI_NIDENT];	/* Magic number and other info */
	  Elf32_Half	e_type;			    /* Object file type */
	  Elf32_Half	e_machine;		    /* Architecture */
	  Elf32_Word	e_version;		    /* Object file version */
	  Elf32_Addr	e_entry;		    /* Entry point virtual address */
	  Elf32_Off	e_phoff;		        /* Program header table file offset */
	  Elf32_Off	e_shoff;		        /* Section header table file offset */
	  Elf32_Word	e_flags;		    /* Processor-specific flags */
	  Elf32_Half	e_ehsize;		    /* ELF header size in bytes */
	  Elf32_Half	e_phentsize;		/* Program header table entry size */
	  Elf32_Half	e_phnum;		    /* Program header table entry count */
	  Elf32_Half	e_shentsize;		/* Section header table entry size */
	  Elf32_Half	e_shnum;		    /* Section header table entry count */
	  Elf32_Half	e_shstrndx;		    /* Section header string table index */
} Elf32_Ehdr;

程序头表(program header table)

列举了所有有效的段(segments)和他们的属性(执行视图),链接视图是可选的,一般没有。

程序头是一个结构的数组,每一个结构都表示一个段(segments)。在可执行文件或者共享链接库中所有的节(sections)都被分为不同的几个段(segments)。

typedef struct elf32_phdr{
	  Elf32_Word	p_type;    /* Magic number and other info */
	  Elf32_Off	p_offset;
	  Elf32_Addr	p_vaddr;
	  Elf32_Addr	p_paddr;
	  Elf32_Word	p_filesz;
	  Elf32_Word	p_memsz;
	  Elf32_Word	p_flags;
	  Elf32_Word	p_align;
} Elf32_Phdr;

程序头的索引地址(e_phoff)、段数量(e_phnum)、表项大小(e_phentsize)都是通过 ELF头部信息获取的。

可通过readelf -l 读取ELF程序头信息:

节头表(section header table)

一个ELF文件中到底有哪些具体的 sections,由包含在这个ELF文件中的 section head table(SHT)决定。每个section描述了这个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其它属性。

typedef struct elf32_shdr{
	    Elf32_Word sh_name;   //节区名,名字是一个 NULL 结尾的字符串。
	    Elf32_Word sh_type;    //为节区类型
	    Elf32_Word sh_flags;    //节区标志
	    Elf32_Addr sh_addr;    //节区的第一个字节应处的位置。否则,此字段为 0。
	    Elf32_Off sh_offset;    //此成员的取值给出节区的第一个字节与文件头之间的偏移。
	    Elf32_Word sh_size;   //此 成 员 给 出 节 区 的 长 度 ( 字 节 数 )。
	    Elf32_Word sh_link;   //此成员给出节区头部表索引链接。其具体的解释依赖于节区类型。
	    Elf32_Word sh_info;       //此成员给出附加信息,其解释依赖于节区类型。
	    Elf32_Word sh_addralign;    //某些节区带有地址对齐约束.
	    Elf32_Word sh_entsize;    //给出每个表项的长度字节数。
}Elf32_Shdr;

节区名存储在.海枫写的《Linux动态链接中的PLT和GOT》。

相关命令

readelf -S relocfile //查看节头表objdump -x -s -d relocfile //查看.text汇编readelf -x .strtab relocfile //查看字符串表readelf -r file //查看重定位表

可执行文件的装载

当运行一个可执行文件时,首先需要将该文件和动态链接库装载到进程空间,形成一个进程镜像。

每个进程都有独立的虚拟地址空间,空间的布局由程序头(program header)决定,通过readelf -l file可以看到节到段的映射,以及段的地址空间布局。每一个段都包含一个或多个节,因为随着节数量的增多,在进行内存映射时就会大量浪费空间和资源。系统并不关心节的实际内容,而是不同节的读写、执行的权限。

参考

ELF文件格式 - 知乎 (zhihu.com)

gcc 编译命令详解及最佳实践 - 知乎 (zhihu.com)

pwn