计算机原理系列之四 ——– 可重定位文件详解

上篇文章我们从整体上介绍了从C文件到可执行文件的编译过程,并逐个分析了单步编译时生成的中间文件的类型。为了搞清楚编译和链接过程中主要做了哪些工作, 我们应该首先明白编译前后,链接前后文件内容的改变。根据上篇文章内容,编译前的文件格式是汇编文件,编译后的文件是可重定位文件。汇编文件就是简单的文本文件,而可重定位文件是一个ELF格式的二进制文件。因此,本章我们将从ELF文件格式入手分析可重定位文件的结构。

一、生成中间文件

我们依然使用计算机原理系列之三 ——– 如何编译目标文件中的hello.c文件来研究,使用下列命令生成hello.o:

gcc -E hello.c -o hello.i
gcc -S hello.i -o hello.s
gcc -c hello.s -o hello.o

二、可重定位文件分析

2.1 解析文件头,说明文件构成

由于可重定位文件类型是ELF格式(关于ELF文件的知识,请阅读详解ELF文件),我们可以使用READELF命令查看其构成。

$ readelf -h hello.o
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:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          672 (bytes into file)
  Flags:                             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: 10

其中,我们关注以下字段:

  1. Type: REL (Relocatable file) : 可以看出.o文件的类型为可重定位文件
  2. Number of program headers: 0:可以看出可重定位文件program header table的长度为0。这是因为program header table保存的是segment信息,而segment是为了给加载器提供可执行程序在加载时所需的信息的,又因为可重定位文件本身并不能直接执行,因此在可重定位文件里不需要program header table
  3. Entry point address: 0x0: 同上,由于可重定位文件不能直接执行,因此其入口地址为0(默认值);
  4. *** of section headers:***:从ELF文件起始地址偏移672个字节处是section header table的起始地址,section header table中共有13项,每项的大小为64 byte
  5. Size of this header: 64: ELF文件头大小为64 byte

2.2 分析ELF文件各部分

可重定位文件属于二进制文件。在linux机器上,可以使用hexdump命令来查看二进制文件的内容。

$ hexdump -C hello.o
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  01 00 3e 00 01 00 00 00  00 00 00 00 00 00 00 00  |..>.............|
00000020  00 00 00 00 00 00 00 00  a0 02 00 00 00 00 00 00  |................|
00000030  00 00 00 00 40 00 00 00  00 00 40 00 0d 00 0a 00  |....@.....@.....|
00000040  55 48 89 e5 bf 00 00 00  00 e8 00 00 00 00 b8 00  |UH..............|
00000050  00 00 00 5d c3 68 65 6c  6c 6f 2c 20 77 6f 72 6c  |...].hello, worl|

... snip ...

这实际上就是hello.o文件的实际内容。

注:
1. 最前面一列是hexdump命令添加的,并非ELF文件的内容。它是一个十六进制的数字,表示字节序号。例如,00000040 55 48 89 e5 bf 00 00 00 00 e8 00 00 00 00 b8 00,其中00000400x40,十进制为64。表示hello.o文件的第64个字节是‘55’
2. 最后一部分由两个‘|’包含的数字和字符也是hexdump命令添加的,它将其左侧的十六进制数字转化成了对应的ASCII字符,所有的控制字符表示为‘.’,所有的可显示字符表示为对应的字符或图形。

2.2.1 ELF header

根据ELF文件的结构,ELF文件最开始的部分是ELF header,它是一个64字节大小的结构体,也就是对应了hello.o的前64个字符,即从0000000 - 00000040的部分。前十六个字节应该是对应其magic number的部分。我们注意到,从0000000 - 0000000F正好就是使用readelf读出来的magic的值。剩下的部分只要结合struct ElfN_Ehdr的成员信息和hexdump命令输出的内容即可一一验证。

2.2.2 section headers table及sections

由于ELF文件头中说明了在该可重定位文件中不存在program header table,因此,根据ELF文件的结构,接下来就应该是各个section了。section的信息是由section header table描述的,我们可以通过以下命令查看section header table

$ readelf -S hello.o
There are 13 section headers, starting at offset 0x2a0:

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         0000000000000000  00000040
       0000000000000015  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  000001f0
       0000000000000030  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000055
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  00000055
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  00000055
       000000000000000d  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  00000062
       0000000000000036  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  00000098
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  00000098
       0000000000000038  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  00000220
       0000000000000018  0000000000000018   I      11     8     8
  [10] .shstrtab         STRTAB           0000000000000000  00000238
       0000000000000061  0000000000000000           0     0     1
  [11] .symtab           SYMTAB           0000000000000000  000000d0
       0000000000000108  0000000000000018          12     9     8
  [12] .strtab           STRTAB           0000000000000000  000001d8
       0000000000000013  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

根据上述信息,该ELF文件一共有13个section header table,具体分布如下:
relocatable file
图1. 可重定位文件结构

各个section的详细内容如下:

  1. .text段保存了可执行代码经过汇编之后的内容,后面的文章我们会详细介绍;
  2. .data.bss并没有占据任何空间,原因是代码中并未定义局部变量或者全局变量;
  3. .rodata的起始地址是0x55,占据0xd个字节的空间,根据hexdump命令输出的信息,其内容应该是:68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 00,根据ASCII表,其对应的字符为:h e l l o , w o r l d NULL。正好就是我们的C代码中唯一的一个需要保存到.rodata段的字符串常量”hello, world”;
  4. .commont的起始地址是0x62,大小为0x36 byte,将hexdump内容截取出来,并转化成ASCII字符为:GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609,可以看出正好就是我们在前面介绍开发环境的时候使用的GCC版本信息
  5. .strtab指的是string table,起始地址是0x1d8,大小为0x13 byte,将hexdump内容截取出来,并转化成ASCII字符为:hello.c main puts,可见其中保存了原C文件中的文件名和函数名等信息;
  6. .shstrtab指的是section header string table,经过同上分析,其中保存了各个section的名字;
  7. .symtab保存了符号表,其中包括了.strtab里面定义的三个符号;每个符号对应的符号表是一个Elf64_Symbol结构体,详细信息参考参考阅读[2]。除了包含.strtab外,符号表中还包含了一些section的符号表条目,这些条目给链接的时候需要和其他可重定位文件或者库的对应的section合并时提供了必要的信息。在上述例子中,其.symtab的内容如下:
$ readelf -s hello.o

Symbol table '.symtab' contains 11 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
     9: 0000000000000000    21 FUNC    GLOBAL DEFAULT    1 main
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts
  1. .eh_frame:其内部存放了以DWARF格式保存的一些调试信息。关于eh_frame详细分析,可以查看参考阅读[3]和[4]
  2. .rel.text:包含了代码段中引用的外部函数和全局变量的重定位条目。上述hello.o文件中我们得到它的.rel.text的内容包括:
Relocation section '.rela.text' at offset 0x1f0 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000005  00050000000a R_X86_64_32       0000000000000000 .rodata + 0
00000000000a  000a00000002 R_X86_64_PC32     0000000000000000 puts - 4
  1. .rela.eh_frame:这个section同.rel.text一样属于重定位信息的section,只不过它包含的是eh_frame的重定位信息,其内容可以参考阅读[3]

注:各个段之间并未严格的首位相接,考虑到对齐的因素,他们之间会存在”空洞”(如图中.shstrtab段和Section Header Table之间的padding分区);

至此,我们可以看到经过编译之后,人类可以理解的汇编文件,变成了上图所示的计算机可以理解的可重定位文件。

三、参考阅读

  1. SYSTEM V APPLICATION BINARY INTERFACE
  2. Symbol Table
  3. eh_frame相关
    1) c++ 异常处理(2)
    2) Linux Standard Base Core Specification 3.0RC1
    3) CFI directives in assembly files
  4. DWARF相关
    1) DWARF, 说不定你也需要它哦
    2) Introduction to the DWARF Debugging Format
  5. 实例分析ELF文件静态链接
  6. http://oiwjee823.bkt.clouddn.com/ELF%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F%E5%88%86%E6%9E%90.pdf
  7. ELF 格式解析

发表评论

电子邮件地址不会被公开。 必填项已用*标注