ELF 文件解析

1,566 阅读18分钟

今天我们来探究一下Linux 系统的基石:ELF文件。 为什么这么说呢,因为 unix 的哲学,一切皆文件。 在操作系统眼里,一切东西无非就是两种属性,读和写。那为什么不把他们抽象成一种数据类型:文件。拥有的方法无非就是Open,Close,read,write。所有的东西都可以看成是File,包括我们接入的鼠标,键盘,u 盘,键盘接入操作系统,操作系统给他分配一个路径,如/dev/input,键盘输入hello world 即写入一个 /dev/input/event0 的文件,然后进程去这个路径读取这个 event0 文件就好了,进行一系列处理之后删除这个文件。

对于ELF文件而言,也是类似的道理,进程启动的时候就是将文件的内容搬运到内存上,然后再搬运到cpu中,读取并进行一些处理,然后移除,关闭

虽然都是文件,但是文件都有其特有的编码规范,比如class文件就是安卓Class文件规范来写的,我们读的时候按照对应的规范完善就好了。同理ELF文件也有其规范。我们来解读一下ELF文件规范:

ELF 文件类型

ELF全称Executable and Linkable Format,可执行连接格式,ELF格式的文件用于存储Linux程序。ELF文件(目标文件)格式主要三种:

  • 可重定位文件:用户和其他目标文件一起创建可执行文件或者共享目标文件,例如lib*.a文件。
  • 可执行文件:用于生成进程映像,载入内存执行,例如编译好的可执行文件a.out。
  • 共享目标文件:用于和其他共享目标文件或者可重定位文件一起生成elf目标文件或者和执行文件一起创建进程映像,例如lib*.so文件。

一般的 ELF 文件包括三个索引表:ELF header,Program header table,Section header table。

  • ELF header:在文件的开始,保存了路线图,描述了该文件的组织情况。
  • Program header table:告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。
  • Section header table:包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。

ELF 文件头

我们看看Linux系统源码中的ELF header 的结构体定义:

// 32 位系统
typedef struct
{
  unsigned char    e_ident[EI_NIDENT];    /* Magic number和其它信息 */
  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;

// 64 位系统
typedef struct
{
  unsigned char    e_ident[EI_NIDENT];    /* Magic number and other info */
  Elf64_Half    e_type;            /* Object file type */
  Elf64_Half    e_machine;        /* Architecture */
  Elf64_Word    e_version;        /* Object file version */
  Elf64_Addr    e_entry;        /* Entry point virtual address */
  Elf64_Off    e_phoff;        /* Program header table file offset */
  Elf64_Off    e_shoff;        /* Section header table file offset */
  Elf64_Word    e_flags;        /* Processor-specific flags */
  Elf64_Half    e_ehsize;        /* ELF header size in bytes */
  Elf64_Half    e_phentsize;        /* Program header table entry size */
  Elf64_Half    e_phnum;        /* Program header table entry count */
  Elf64_Half    e_shentsize;        /* Section header table entry size */
  Elf64_Half    e_shnum;        /* Section header table entry count */
  Elf64_Half    e_shstrndx;        /* Section header string table index */
} Elf64_Ehdr;

下面解释下 header 的含义:

  • e_ident[EI_NIDENT] 占16个字节。前四个字节被称作ELF的Magic Number。后面的字节描述了ELF文件内容如何解码等信息。
  • e_type 2字节,描述了ELF文件的类型。以下取值有意义:
 ET_NONE, 0, No file type
 ET_REL, 1, Relocatable file(可重定位文件,通常是文件名以.o结尾,目标文件)
 ET_EXEC, 2, Executable file (可执行文件)
 ET_DYN, 3, Shared object file (动态库文件,你用gcc编译出的二进制往往也属于这种类型,惊讶吗?)
 ET_CORE, 4, Core file (core文件,是core dump生成的吧?)
 ET_NUM, 5,表示已经定义了5种文件类型
 ET_LOPROC, 0xff00, Processor-specific
 ET_HIPROC, 0xffff, Processor-specific
  • e _machine 2字节。描述了文件面向的架构
  • e_version 2字节,描述了ELF文件的版本号
  • e_entry,(32位4字节,64位8字节),执行入口点,如果文件没有入口点,这个域保持0。
  • e_phoff, (32位4字节,64位8字节),program header table的offset,如果文件没有PH,这个值是0。
  • e_shoff, (32位4字节,64位8字节), section header table 的offset,如果文件没有SH,这个值是0。
  • e_flags, 4字节,特定于处理器的标志,32位和64位Intel架构都没有定义标志,因此eflags的值是0。
  • e_ehsize, 2字节,ELF header的大小,32位ELF是52字节,64位是64字节。
  • e_phentsize,2字节。program header table中每个入口的大小。
  • e_phnum, 2字节。如果文件没有program header table, e_phnum的值为0。e_phentsize乘以e_phnum就得到了整个program header table的大小。
  • e_shentsize, 2字节,section header table中entry的大小,即每个section header占多少字节。
  • e_shnum, 2字节,section header table中header的数目。如果文件没有section header table, e_shnum的值为0。e_shentsize乘以e_shnum,就得到了整个section header table的大小。
  • e_shstrndx, 2字节。section header string table index. 包含了section header table中section name string table。如果没有section name string table, e_shstrndx的值是SHN_UNDEF.

我们在结合readelf来解读一下ELF header 例如我将一个go程序编译成main(ELF文件),然后执行readelf -h mian,得到如下结果:

ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x45d310
  Start of program headers:          64 (bytes into file)
  Start of section headers:          456 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         7
  Size of section headers:           64 (bytes)
  Number of section headers:         25
  Section header string table index: 3

以上信息展示得很明确了,我们来看看具体得二进制信息是怎样的,根据以上信息,我们知道该ELF header 有64个字节,所以,我们来只查看该elf文件的前64该字节的内容:

0000000    457f    464c    0102    0001    0000    0000    0000    0000
0000010    0002    003e    0001    0000    d310    0045    0000    0000
0000020    0040    0000    0000    0000    01c8    0000    0000    0000
0000030    0000    0000    0040    0038    0007    0040    0019    0003
0000040

这64个字节的数据通过前面的ELF header的定义能够得到readelf解析的内容,就是一个对应的解析,难度不大,这里我就抽几个变量进行解析:

  • e_ident[EI_NIDENT] 16 个字节 内容为457f 464c 0102 0001 0000 0000 0000 0000
  • e_entry 程序的入口点为:0x45d310

ELF Section

在ELF header中有e_shoff字段,我们根据该字段的信息能够找到section header table,这个文件是干什么的呢?ELF header 存储的整个ELF文件的头信息,那该字段自然就是存储的Section header的信息。每个 section 都会对应一个 section header,我们通过section header就能找到对应的section

Section Header 数据结构定义如下:

typedef struct elf64_shdr {
   Elf64_Word sh_name;        /* Section name, index in string tbl */
   Elf64_Word sh_type;        /* Type of section */
   Elf64_Xword sh_flags;        /* Miscellaneous section attributes */
   Elf64_Addr sh_addr;        /* Section virtual addr at execution */
   Elf64_Off sh_offset;        /* Section file offset */
   Elf64_Xword sh_size;        /* Size of section in bytes */
   Elf64_Word sh_link;        /* Index of another section */
   Elf64_Word sh_info;        /* Additional section information */
   Elf64_Xword sh_addralign;    /* Section alignment */
   Elf64_Xword sh_entsize;    /* Entry size if section holds table */
} Elf64_Shdr;

根据对应的Section header 我们来解读对应的Section信息 readelf -S main1

There are 25 section headers, starting at offset 0x1c8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000401000  00001000
       000000000009050c  0000000000000000  AX       0     0     16
  [ 2] .rodata           PROGBITS         0000000000492000  00092000
       000000000004b5ca  0000000000000000   A       0     0     32
  [ 3] .shstrtab         STRTAB           0000000000000000  000dd5e0
       00000000000001bc  0000000000000000           0     0     1
  [ 4] .typelink         PROGBITS         00000000004dd7a0  000dd7a0
       0000000000000a9c  0000000000000000   A       0     0     32
  [ 5] .itablink         PROGBITS         00000000004de240  000de240
       0000000000000050  0000000000000000   A       0     0     8
  [ 6] .gosymtab         PROGBITS         00000000004de290  000de290
       0000000000000000  0000000000000000   A       0     0     1
  [ 7] .gopclntab        PROGBITS         00000000004de2a0  000de2a0
       000000000006fa5c  0000000000000000   A       0     0     32
  [ 8] .go.buildinfo     PROGBITS         000000000054e000  0014e000
       0000000000000020  0000000000000000  WA       0     0     16
  [ 9] .noptrdata        PROGBITS         000000000054e020  0014e020
       000000000000e080  0000000000000000  WA       0     0     32
  [10] .data             PROGBITS         000000000055c0a0  0015c0a0
       0000000000007110  0000000000000000  WA       0     0     32
  [11] .bss              NOBITS           00000000005631c0  001631c0
       0000000000029950  0000000000000000  WA       0     0     32
  [12] .noptrbss         NOBITS           000000000058cb20  0018cb20
       0000000000002828  0000000000000000  WA       0     0     32
  [13] .zdebug_abbrev    PROGBITS         0000000000590000  00164000
       0000000000000119  0000000000000000           0     0     8
  [14] .zdebug_line      PROGBITS         0000000000590119  00164119
       000000000001a8d7  0000000000000000           0     0     8
  [15] .zdebug_frame     PROGBITS         00000000005aa9f0  0017e9f0
       0000000000006194  0000000000000000           0     0     8
  [16] .zdebug_pubnames  PROGBITS         00000000005b0b84  00184b84
       0000000000001547  0000000000000000           0     0     8
  [17] .zdebug_pubtypes  PROGBITS         00000000005b20cb  001860cb
       00000000000033ac  0000000000000000           0     0     8
  [18] .debug_gdb_script PROGBITS         00000000005b5477  00189477
       000000000000002a  0000000000000000           0     0     1
  [19] .zdebug_info      PROGBITS         00000000005b54a1  001894a1
       000000000003068d  0000000000000000           0     0     8
  [20] .zdebug_loc       PROGBITS         00000000005e5b2e  001b9b2e
       00000000000167d4  0000000000000000           0     0     8
  [21] .zdebug_ranges    PROGBITS         00000000005fc302  001d0302
       0000000000008970  0000000000000000           0     0     8
  [22] .note.go.buildid  NOTE             0000000000400f9c  00000f9c
       0000000000000064  0000000000000000   A       0     0     4
  [23] .symtab           SYMTAB           0000000000000000  001d9000
       000000000000f630  0000000000000018          24   126     8
  [24] .strtab           STRTAB           0000000000000000  001e8630
       000000000000f818  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific) 

以上信息将每个节的内容清晰地展示出来了,我们如果要知道每个section的具体内容,可以通过 readelf -x [index] main 就可以看到具体内容了。通过 objdump –d hello 能够看到反汇编代码。代码量过大,但是汇编代码其实很简单。截取一小段我们看看我最后的mian函数的汇编代码:

0000000000491410 <main.main>:
  491410:	64 48 8b 0c 25 f8 ff 	mov    %fs:0xfffffffffffffff8,%rcx
  491417:	ff ff
  491419:	48 3b 61 10          	cmp    0x10(%rcx),%rsp
  49141d:	0f 86 df 00 00 00    	jbe    491502 <main.main+0xf2>
  491423:	48 83 ec 68          	sub    $0x68,%rsp
  491427:	48 89 6c 24 60       	mov    %rbp,0x60(%rsp)
  49142c:	48 8d 6c 24 60       	lea    0x60(%rsp),%rbp
  491431:	48 c7 04 24 0c 00 00 	movq   $0xc,(%rsp)
  491438:	00
  491439:	e8 62 7c f7 ff       	callq  4090a0 <runtime.convT64>
  49143e:	48 8b 44 24 08       	mov    0x8(%rsp),%rax
  491443:	0f 57 c0             	xorps  %xmm0,%xmm0
  491446:	0f 11 44 24 50       	movups %xmm0,0x50(%rsp)
  49144b:	48 8d 0d 6e df 00 00 	lea    0xdf6e(%rip),%rcx        # 49f3c0 <type.*+0xd3c0>
  491452:	48 89 4c 24 50       	mov    %rcx,0x50(%rsp)
  491457:	48 89 44 24 58       	mov    %rax,0x58(%rsp)
  49145c:	48 8b 05 ad 1d 0d 00 	mov    0xd1dad(%rip),%rax        # 563210 <os.Stdout>
  491463:	48 8d 15 36 a2 04 00 	lea    0x4a236(%rip),%rdx        # 4db6a0 <go.itab.*os.File,io.Writer>
  49146a:	48 89 14 24          	mov    %rdx,(%rsp)
  49146e:	48 89 44 24 08       	mov    %rax,0x8(%rsp)
  491473:	48 8d 44 24 50       	lea    0x50(%rsp),%rax
  491478:	48 89 44 24 10       	mov    %rax,0x10(%rsp)
  49147d:	48 c7 44 24 18 01 00 	movq   $0x1,0x18(%rsp)
  491484:	00 00
  491486:	48 c7 44 24 20 01 00 	movq   $0x1,0x20(%rsp)
  49148d:	00 00
  49148f:	e8 6c 99 ff ff       	callq  48ae00 <fmt.Fprintln>
  491494:	90                   	nop
  491495:	48 c7 04 24 0d 00 00 	movq   $0xd,(%rsp)
  49149c:	00
  49149d:	e8 fe 7b f7 ff       	callq  4090a0 <runtime.convT64>
  4914a2:	48 8b 44 24 08       	mov    0x8(%rsp),%rax
  4914a7:	0f 57 c0             	xorps  %xmm0,%xmm0
  4914aa:	0f 11 44 24 40       	movups %xmm0,0x40(%rsp)
  4914af:	48 8d 0d 0a df 00 00 	lea    0xdf0a(%rip),%rcx        # 49f3c0 <type.*+0xd3c0>
  4914b6:	48 89 4c 24 40       	mov    %rcx,0x40(%rsp)
  4914bb:	48 89 44 24 48       	mov    %rax,0x48(%rsp)
  4914c0:	48 8b 05 49 1d 0d 00 	mov    0xd1d49(%rip),%rax        # 563210 <os.Stdout>
  4914c7:	48 8d 0d d2 a1 04 00 	lea    0x4a1d2(%rip),%rcx        # 4db6a0 <go.itab.*os.File,io.Writer>
  4914ce:	48 89 0c 24          	mov    %rcx,(%rsp)
  4914d2:	48 89 44 24 08       	mov    %rax,0x8(%rsp)
  4914d7:	48 8d 44 24 40       	lea    0x40(%rsp),%rax
  4914dc:	48 89 44 24 10       	mov    %rax,0x10(%rsp)
  4914e1:	48 c7 44 24 18 01 00 	movq   $0x1,0x18(%rsp)
  4914e8:	00 00
  4914ea:	48 c7 44 24 20 01 00 	movq   $0x1,0x20(%rsp)
  4914f1:	00 00
  4914f3:	e8 08 99 ff ff       	callq  48ae00 <fmt.Fprintln>
  4914f8:	48 8b 6c 24 60       	mov    0x60(%rsp),%rbp
  4914fd:	48 83 c4 68          	add    $0x68,%rsp
  491501:	c3                   	retq
  491502:	e8 d9 85 fc ff       	callq  459ae0 <runtime.morestack_noctxt>
  491507:	e9 04 ff ff ff       	jmpq   491410 <main.main>

以上汇编代码无非就是执行了一些将寄存器的数据mov,cmp,add等操作,有一个很神奇的语法就是callq,通过该语法进行函数调用.汇编语法就暂时不做讲解了。我们已经知道了Section的代码规范了,当然具体的内容还需要和符号表对应上。内容过多,我们只用知道这些规范,需要的时候去查就可以了。这里就不进行详细解析了。

程序表

typedef struct elf64_phdr {
   Elf64_Word p_type;
   Elf64_Word p_flags;
   Elf64_Off p_offset;        /* Segment file offset */
   Elf64_Addr p_vaddr;        /* Segment virtual address */
   Elf64_Addr p_paddr;        /* Segment physical address */
   Elf64_Xword p_filesz;        /* Segment size in file */
   Elf64_Xword p_memsz;        /* Segment size in memory */
   Elf64_Xword p_align;        /* Segment alignment, file & memory */
} Elf64_Phdr;

readelf -l main 结果如下:

Elf file type is EXEC (Executable file)
Entry point 0x45d310
There are 7 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x0000000000000188 0x0000000000000188  R      0x1000
  NOTE           0x0000000000000f9c 0x0000000000400f9c 0x0000000000400f9c
                 0x0000000000000064 0x0000000000000064  R      0x4
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x000000000009150c 0x000000000009150c  R E    0x1000
  LOAD           0x0000000000092000 0x0000000000492000 0x0000000000492000
                 0x00000000000bbcfc 0x00000000000bbcfc  R      0x1000
  LOAD           0x000000000014e000 0x000000000054e000 0x000000000054e000
                 0x00000000000151c0 0x0000000000041348  RW     0x1000
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x8
  LOOS+0x5041580 0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000         0x8

 Section to Segment mapping:
  Segment Sections...
   00
   01     .note.go.buildid
   02     .text .note.go.buildid
   03     .rodata .typelink .itablink .gosymtab .gopclntab
   04     .go.buildinfo .noptrdata .data .bss .noptrbss
   05
   06

补充

  • text section是可执行指令的集合,.data和.text都是属于PROGBITS类型的section,这是将来要运行的程序与代码。查询段表可知.text section的位偏移为0x0000320,size为0x0000192。
  • strtab section是属于STRTAB类型的section,可以在文件中看到,它存着字符串,储存着符号的名字。位偏移为0x000106f,size为0x0000106
  • symtab section存放所有section中定义的符号名字,比如“data_items”,“start_loop”。 .symtab section是属于SYMTAB类型的section,它描述了.strtab中的符号在“内存”中对应的“内存地址”。 位偏移为0x0001628,size为0x0000430。
  • rodata section,ro代表read only。位偏移为0x000050c,size为0x00000b0。