ELF文件分析之1 - 文件格式

0 前言

上一篇,我们知道了ELF文件是什么,以及怎么用,本篇重点介绍ELF的文档规范

1 整体结构

ELF适用于3种文件结构

  1. 重定向文件
    也就是经常见到的.o文件,用于保存代码和数据,链接器用使用这个文件来创建可执行文件或者共享文件
  2. 可执行文件
    也就是linux下面的.out文件,用于执行
  3. 共享文件
    常见的静态库.lib或者动态库.so,就是这个文件格式,就是由多个.o压缩成的,可以用于保存代码或者数据,节省编译时间

上面三种文件,再综合一下,可以分为两种

  1. 链接文件(中间态)
  2. 可执行文件(最终态)

ELF分别用两种视图来表示二者,如下所示

整体示意图

2 准备工作

在介绍各部分含义之前,我们需要先做一下准备工作,在树莓派上生成几个需要的文件
这里,我采用的是树莓派Raspbian系统自带的gcc编译器,CPU为ARM-A系列内核

  1. .o文件

    1
    root@raspberrypi:/home/pi/tmp# gcc -c -O0 -g main.c
    root@raspberrypi:/home/pi/tmp# gcc -c -O0 -g fun.c
  2. .so文件

    1
    root@raspberrypi:/home/pi/tmp# gcc -shared -fPIC -O0 -g -o libfun.so fun.c
  3. .out文件

    1
    root@raspberrypi:/home/pi/tmp# gcc main.o fun.o

现在,你的目录下,应该有如下几个文件:

1
root@raspberrypi:/home/pi/tmp# ls
a.out  fun.c  fun.o  libfun.so	main.c	main.o

ok, 我们开始分析ELF结构

3 ELF header

它主要为系统提供基本信息,具体包括:

  1. 这是什么文件
  2. 大小多少
  3. 程序入口在哪里
  4. header表在哪里
  5. header表里面有多少项
  6. 等等

规范里面,提供了一个结构体定义,如下所示
整体示意图

我们来看看.o.so.out三者的header分别是怎么样的,如下所示

  1. fun.o ELF header

    1
    root@raspberrypi:/home/pi/tmp# readelf -h fun.o
    ELF Header:
      Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
      Class:                             ELF32
      Data:                              2's complement, little endian
      Version:                           1 (current)
      OS/ABI:                            UNIX - System V
      ABI Version:                       0
      Type:                              REL (Relocatable file)           # 0 注意文件类型
      Machine:                           ARM
      Version:                           0x1
      Entry point address:               0x0                              # 1 注意入口地址
      Start of program headers:          0 (bytes into file)
      Start of section headers:          1220 (bytes into file)
      Flags:                             0x5000000, Version5 EABI
      Size of this header:               52 (bytes)
      Size of program headers:           0 (bytes)
      Number of program headers:         0
      Size of section headers:           40 (bytes)
      Number of section headers:         20
      Section header string table index: 17
  2. libfun.so ELF header

    1
    root@raspberrypi:/home/pi/tmp# readelf -h libfun.so
    ELF Header:
      Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
      Class:                             ELF32
      Data:                              2's complement, little endian
      Version:                           1 (current)
      OS/ABI:                            UNIX - System V
      ABI Version:                       0
      Type:                              DYN (Shared object file)           # 0 注意文件类型
      Machine:                           ARM
      Version:                           0x1
      Entry point address:               0x414                              # 1 注意入口地址
      Start of program headers:          52 (bytes into file)
      Start of section headers:          4680 (bytes into file)
      Flags:                             0x5000402, has entry point, Version5 EABI, hard-float ABI
      Size of this header:               52 (bytes)
      Size of program headers:           32 (bytes)
      Number of program headers:         5
      Size of section headers:           40 (bytes)
      Number of section headers:         32
      Section header string table index: 29
    root@raspberrypi:/home/pi/tmp#
  3. a.out ELF header

    1
    root@raspberrypi:/home/pi/tmp# readelf -h a.out
    ELF Header:
      Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
      Class:                             ELF32
      Data:                              2's complement, little endian
      Version:                           1 (current)
      OS/ABI:                            UNIX - System V
      ABI Version:                       0
      Type:                              EXEC (Executable file)        # 0 注意文件类型
      Machine:                           ARM
      Version:                           0x1
      Entry point address:               0x102f8                       # 1 注意入口地址
      Start of program headers:          52 (bytes into file)
      Start of section headers:          5808 (bytes into file)
      Flags:                             0x5000402, has entry point, Version5 EABI, hard-float ABI
      Size of this header:               52 (bytes)
      Size of program headers:           32 (bytes)
      Number of program headers:         8
      Size of section headers:           40 (bytes)
      Number of section headers:         36
      Section header string table index: 33

我们可以看到,不同的文件,readelf工具读出来的文件类型入口地址是不一样的,具体细节,请对照着ELF规范来逐个字节分析
哦,忘了告诉你,怎么通过看原始HEX信息了,请使用xxd工具,如下所示

1
root@raspberrypi:/home/pi/tmp# xxd a.out 

0000000: 7f45 4c46 0101 0100 0000 0000 0000 0000  .ELF............
0000010: 0200 2800 0100 0000 f802 0100 3400 0000  ..(.........4...
0000020: b016 0000 0204 0005 3400 2000 0800 2800  ........4. ...(.
0000030: 2400 2100 0100 0070 1805 0000 1805 0100  $.!....p........
0000040: 1805 0100 0800 0000 0800 0000 0400 0000  ................
0000050: 0400 0000 0600 0000 3400 0000 3400 0100  ........4...4...
0000060: 3400 0100 0001 0000 0001 0000 0500 0000  4...............
0000070: 0400 0000 0300 0000 3401 0000 3401 0100  ........4...4...
0000080: 3401 0100 1900 0000 1900 0000 0400 0000  4...............
0000090: 0100 0000 0100 0000 0000 0000 0000 0100  ................
00000a0: 0000 0100 2405 0000 2405 0000 0500 0000  ....$...$.......
00000b0: 0000 0100 0100 0000 2405 0000 2405 0200  ........$...$...
...

对于一个可执行文件来说,系统最关心的是程序入口地址,是否需要动态库,数据区大小,代码区大小,size等信息;而对于一个重定向文件来说,最关心的是里面有哪些函数,哪些符号,对应的地址是多少,符号类型等信息;elf分别用下面的program headersection header来描述这些信息

4 Program header table

我们知道,嵌入式系统中,包含RAM和FLASH,无论我们的程序里面包含了多少个段,最终还是要映射到FLASH或者RAM中,链接脚本决定了如何映射各种段(.bss, .data .init 等),但对于加载器来说,根本不需要知道这些段的名字,加载器只需要知道段的地址和大小,以及将要映射的区域,就足够了。这也是ELF为什么要区分为两个视图的原因,链接器需要知道详细的段信息,就给它详细信息;加载器需要简单信息,就单独提供一个program header给它,各取所需,分工明确。

我们先看看三个文件里面的program header信息

  1. a.out program header

    1
    root@raspberrypi:/home/pi/tmp# readelf -l a.out 
    
    Elf file type is EXEC (Executable file)
    Entry point 0x102f8
    There are 8 program headers, starting at offset 52
    
    Program Headers:
      Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
      EXIDX          0x000518 0x00010518 0x00010518 0x00008 0x00008 R   0x4
      PHDR           0x000034 0x00010034 0x00010034 0x00100 0x00100 R E 0x4
      INTERP         0x000134 0x00010134 0x00010134 0x00019 0x00019 R   0x1
          [Requesting program interpreter: /lib/ld-linux-armhf.so.3]
      LOAD           0x000000 0x00010000 0x00010000 0x00524 0x00524 R E 0x10000
      LOAD           0x000524 0x00020524 0x00020524 0x0011c 0x00124 RW  0x10000
      DYNAMIC        0x000530 0x00020530 0x00020530 0x000e8 0x000e8 RW  0x4
      NOTE           0x000150 0x00010150 0x00010150 0x00044 0x00044 R   0x4
      GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
    
     Section to Segment mapping:
      Segment Sections...
       00     .ARM.exidx 
       01     
       02     .interp 
       03     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .ARM.exidx .eh_frame 
       04     .init_array .fini_array .jcr .dynamic .got .data .bss 
       05     .dynamic 
       06     .note.ABI-tag .note.gnu.build-id 
       07
  2. fun.o program header

    1
    root@raspberrypi:/home/pi/tmp# readelf -l fun.o 
    
    There are no program headers in this file.
  3. libfun.so program header

    1
    root@raspberrypi:/home/pi/tmp# readelf -l libfun.so 
    
    Elf file type is DYN (Shared object file)
    Entry point 0x414
    There are 5 program headers, starting at offset 52
    
    Program Headers:
      Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
      LOAD           0x000000 0x00000000 0x00000000 0x005c4 0x005c4 R E 0x10000
      LOAD           0x0005c4 0x000105c4 0x000105c4 0x00118 0x00120 RW  0x10000
      DYNAMIC        0x0005d0 0x000105d0 0x000105d0 0x000e0 0x000e0 RW  0x4
      NOTE           0x0000d4 0x000000d4 0x000000d4 0x00024 0x00024 R   0x4
      GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
    
     Section to Segment mapping:
      Segment Sections...
       00     .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .eh_frame 
       01     .init_array .fini_array .jcr .dynamic .got .data .bss 
       02     .dynamic 
       03     .note.gnu.build-id 
       04

可以看到a.outlibfun.so都有program header信息,fun.o没有program header信息,这是因为前两者中各个元素地址信息已经确定下来了,可以加载到系统中执行,但fun.o仅仅作为中间产物,地址信息没有确定,无法加载到系统运行,所以对加载器来说,该文件完全没有有用信息,所以就不存在对应的program header。

需要注意的: Section to Segment mapping下面的几个条目和Program Headers几个条目分别对应,后面的信息,表示哪几个段分配到哪几个节中,以libfun.so 为例,如果整合到一起,则如下所示

1
Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align    Segment  Sections
LOAD           0x000000 0x00000000 0x00000000 0x005c4 0x005c4 R E 0x10000  00       .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .eh_frame 
LOAD           0x0005c4 0x000105c4 0x000105c4 0x00118 0x00120 RW  0x10000  01       .init_array .fini_array .jcr .dynamic .got .data .bss 
DYNAMIC        0x0005d0 0x000105d0 0x000105d0 0x000e0 0x000e0 RW  0x4      02       .dynamic 
NOTE           0x0000d4 0x000000d4 0x000000d4 0x00024 0x00024 R   0x4      03       .note.gnu.build-id 
GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10     04

有了感性认识,我们来看看规范里面对program header的定义
从上文可以知道,ELF header里面包含了program header的偏移地址和size信息

1
Start of program headers:          52 (bytes into file)
Size of program headers:           32 (bytes)
Number of program headers:         8

表示program header在文件的52字节偏移处,然后总共8个元素,每个元素32字节大小

元素的定义如下所示
Program Header 元素定义

结构体所描述的信息,可以通过readelf来读出来,上面已经展示过了,是一一对应的
其中,最重要的是LOAD类型的节,这部分是用来加载到内存中,我们可以将这部分复制出来,从而导出bin文件

5 Section header table

则是从链接器的视角来管理内容,我们先看看a.outfun.olibfun.so这三个的section header信息,从而有一个感性的认识

  1. a.out section header

    1
    root@raspberrypi:/home/pi/tmp# readelf -S a.out
    There are 36 section headers, starting at offset 0x16b0:
    
    Section Headers:
      [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
      [ 0]                   NULL            00000000 000000 000000 00      0   0  0
      [ 1] .interp           PROGBITS        00010134 000134 000019 00   A  0   0  1
      [ 2] .note.ABI-tag     NOTE            00010150 000150 000020 00   A  0   0  4
      [ 3] .note.gnu.build-i NOTE            00010170 000170 000024 00   A  0   0  4
      [ 4] .gnu.hash         GNU_HASH        00010194 000194 00002c 04   A  5   0  4
      [ 5] .dynsym           DYNSYM          000101c0 0001c0 000050 10   A  6   1  4
      [ 6] .dynstr           STRTAB          00010210 000210 000043 00   A  0   0  1
      [ 7] .gnu.version      VERSYM          00010254 000254 00000a 02   A  5   0  2
      [ 8] .gnu.version_r    VERNEED         00010260 000260 000020 00   A  6   1  4
      [ 9] .rel.dyn          REL             00010280 000280 000008 08   A  5   0  4
      [10] .rel.plt          REL             00010288 000288 000020 08  AI  5  12  4
      [11] .init             PROGBITS        000102a8 0002a8 00000c 00  AX  0   0  4
      [12] .plt              PROGBITS        000102b4 0002b4 000044 04  AX  0   0  4
      [13] .text             PROGBITS        000102f8 0002f8 000208 00  AX  0   0  4
      [14] .fini             PROGBITS        00010500 000500 000008 00  AX  0   0  4
      [15] .rodata           PROGBITS        00010508 000508 000010 00   A  0   0  4
      [16] .ARM.exidx        ARM_EXIDX       00010518 000518 000008 00  AL 13   0  4
      [17] .eh_frame         PROGBITS        00010520 000520 000004 00   A  0   0  4
      [18] .init_array       INIT_ARRAY      00020524 000524 000004 00  WA  0   0  4
      [19] .fini_array       FINI_ARRAY      00020528 000528 000004 00  WA  0   0  4
      [20] .jcr              PROGBITS        0002052c 00052c 000004 00  WA  0   0  4
      [21] .dynamic          DYNAMIC         00020530 000530 0000e8 08  WA  6   0  4
      [22] .got              PROGBITS        00020618 000618 000020 04  WA  0   0  4
      [23] .data             PROGBITS        00020638 000638 000008 00  WA  0   0  4
      [24] .bss              NOBITS          00020640 000640 000008 00  WA  0   0  4
      [25] .comment          PROGBITS        00000000 000640 00003d 01  MS  0   0  1
      [26] .ARM.attributes   ARM_ATTRIBUTES  00000000 00067d 000031 00      0   0  1
      [27] .debug_aranges    PROGBITS        00000000 0006ae 000040 00      0   0  1
      [28] .debug_info       PROGBITS        00000000 0006ee 000106 00      0   0  1
      [29] .debug_abbrev     PROGBITS        00000000 0007f4 0000b0 00      0   0  1
      [30] .debug_line       PROGBITS        00000000 0008a4 00006d 00      0   0  1
      [31] .debug_frame      PROGBITS        00000000 000914 00005c 00      0   0  4
      [32] .debug_str        PROGBITS        00000000 000970 0000cb 01  MS  0   0  1
      [33] .shstrtab         STRTAB          00000000 000a3b 000157 00      0   0  1
      [34] .symtab           SYMTAB          00000000 000b94 0007a0 10     35  95  4
      [35] .strtab           STRTAB          00000000 001334 00037a 00      0   0  1
    Key to Flags:
      W (write), A (alloc), X (execute), M (merge), S (strings)
      I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
      O (extra OS processing required) o (OS specific), p (processor specific)
  2. fun.o section header

    1
    root@raspberrypi:/home/pi/tmp# readelf -S fun.o
    There are 20 section headers, starting at offset 0x4c4:
    
    Section Headers:
      [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
      [ 0]                   NULL            00000000 000000 000000 00      0   0  0
      [ 1] .text             PROGBITS        00000000 000034 000030 00  AX  0   0  4
      [ 2] .data             PROGBITS        00000000 000064 000000 00  WA  0   0  1
      [ 3] .bss              NOBITS          00000000 000064 000000 00  WA  0   0  1
      [ 4] .debug_info       PROGBITS        00000000 000064 00006e 00      0   0  1
      [ 5] .rel.debug_info   REL             00000000 000454 000048 08   I 18   4  4
      [ 6] .debug_abbrev     PROGBITS        00000000 0000d2 00005b 00      0   0  1
      [ 7] .debug_aranges    PROGBITS        00000000 00012d 000020 00      0   0  1
      [ 8] .rel.debug_arange REL             00000000 00049c 000010 08   I 18   7  4
      [ 9] .debug_line       PROGBITS        00000000 00014d 000035 00      0   0  1
      [10] .rel.debug_line   REL             00000000 0004ac 000008 08   I 18   9  4
      [11] .debug_str        PROGBITS        00000000 000182 000066 01  MS  0   0  1
      [12] .comment          PROGBITS        00000000 0001e8 000020 01  MS  0   0  1
      [13] .note.GNU-stack   PROGBITS        00000000 000208 000000 00      0   0  1
      [14] .debug_frame      PROGBITS        00000000 000208 000030 00      0   0  4
      [15] .rel.debug_frame  REL             00000000 0004b4 000010 08   I 18  14  4
      [16] .ARM.attributes   ARM_ATTRIBUTES  00000000 000238 000031 00      0   0  1
      [17] .shstrtab         STRTAB          00000000 000269 0000b2 00      0   0  1
      [18] .symtab           SYMTAB          00000000 00031c 000120 10     19  16  4
      [19] .strtab           STRTAB          00000000 00043c 000015 00      0   0  1
    Key to Flags:
      W (write), A (alloc), X (execute), M (merge), S (strings)
      I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
      O (extra OS processing required) o (OS specific), p (processor specific)

像其它的header table一样,section

  1. libfun.so section header
    1
    root@raspberrypi:/home/pi/tmp# readelf -S libfun.so
    There are 32 section headers, starting at offset 0x1248:
    
    Section Headers:
      [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
      [ 0]                   NULL            00000000 000000 000000 00      0   0  0
      [ 1] .note.gnu.build-i NOTE            000000d4 0000d4 000024 00   A  0   0  4
      [ 2] .gnu.hash         GNU_HASH        000000f8 0000f8 000050 04   A  3   0  4
      [ 3] .dynsym           DYNSYM          00000148 000148 000130 10   A  4   3  4
      [ 4] .dynstr           STRTAB          00000278 000278 0000cb 00   A  0   0  1
      [ 5] .gnu.version      VERSYM          00000344 000344 000026 02   A  3   0  2
      [ 6] .gnu.version_r    VERNEED         0000036c 00036c 000020 00   A  4   1  4
      [ 7] .rel.dyn          REL             0000038c 00038c 000040 08   A  3   0  4
      [ 8] .rel.plt          REL             000003cc 0003cc 000010 08  AI  3  10  4
      [ 9] .init             PROGBITS        000003dc 0003dc 00000c 00  AX  0   0  4
      [10] .plt              PROGBITS        000003e8 0003e8 00002c 04  AX  0   0  4
      [11] .text             PROGBITS        00000414 000414 0001a4 00  AX  0   0  4
      [12] .fini             PROGBITS        000005b8 0005b8 000008 00  AX  0   0  4
      [13] .eh_frame         PROGBITS        000005c0 0005c0 000004 00   A  0   0  4
      [14] .init_array       INIT_ARRAY      000105c4 0005c4 000004 00  WA  0   0  4
      [15] .fini_array       FINI_ARRAY      000105c8 0005c8 000004 00  WA  0   0  4
      [16] .jcr              PROGBITS        000105cc 0005cc 000004 00  WA  0   0  4
      [17] .dynamic          DYNAMIC         000105d0 0005d0 0000e0 08  WA  4   0  4
      [18] .got              PROGBITS        000106b0 0006b0 000028 04  WA  0   0  4
      [19] .data             PROGBITS        000106d8 0006d8 000004 00  WA  0   0  4
      [20] .bss              NOBITS          000106dc 0006dc 000008 00  WA  0   0  4
      [21] .comment          PROGBITS        00000000 0006dc 00001f 01  MS  0   0  1
      [22] .ARM.attributes   ARM_ATTRIBUTES  00000000 0006fb 00002f 00      0   0  1
      [23] .debug_aranges    PROGBITS        00000000 00072a 000020 00      0   0  1
      [24] .debug_info       PROGBITS        00000000 00074a 00006e 00      0   0  1
      [25] .debug_abbrev     PROGBITS        00000000 0007b8 00005b 00      0   0  1
      [26] .debug_line       PROGBITS        00000000 000813 000035 00      0   0  1
      [27] .debug_frame      PROGBITS        00000000 000848 000030 00      0   0  4
      [28] .debug_str        PROGBITS        00000000 000878 00006c 01  MS  0   0  1
      [29] .shstrtab         STRTAB          00000000 0008e4 00012e 00      0   0  1
      [30] .symtab           SYMTAB          00000000 000a14 0005b0 10     31  75  4
      [31] .strtab           STRTAB          00000000 000fc4 000281 00      0   0  1
    Key to Flags:
      W (write), A (alloc), X (execute), M (merge), S (strings)
      I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
      O (extra OS processing required) o (OS specific), p (processor specific)

像其它的header表一样,section header也是用基本数组元素来表示的,数组的结构体如下所示
Section Header 元素定义

其中,需要注意,为了便于统一管理和数组元素大小一致,section的sh_name成员,用字符串表里面的索引来表示
这也是ELF header里面单独列出字符串表索引的重要原因

6 总结

我们总结一下,ELF有两种视图,分别为链接视图和加载视图
分析ELF的过程如下所示

  1. 链接视图
    1. 找到ELF Header
    2. 在ELF Header中找到Section Header入口地址和元素大小,数量
    3. 找到字符串表的索引和入口地址
    4. 分析Section Header,找到各个表项的入口地址和基本信息
  2. 加载视图
    1. 找到ELF Header
    2. 在ELF Header中找到Program Header入口地址和元素大小,数量
    3. 找到字符串表的索引和入口地址
    4. 对于动态链接来说,先调用系统加载器,加载所需要的so文件;对于静态来说,构建基本运行环境
    5. 加载LOAD节到系统内存
    6. 加载调试信息给debuger
    7. 开始运行或者调试

7 参考资料

  1. ELF规范1.1
  2. ELF规范1.2
  3. ARM ELF 规范