计算机原理系列之六 ——– 可执行文件详解

[toc]

前面的文章我们详细的讲解了.o文件的结构及其编译过程,这篇文章我们从ELF文件的观点来分析可执行文件的结构。

一、生成可执行文件

我们以下面两个文件为例来研究多个文件生成一个可执行文件的过程。一个文件名为add.c,主要内容包含一个add函数,该函数功能为:将调用次数加1,然后,返回传递进来的两个参数之和。

extern int times;

int add(int n1, int n2) {
    times++;
    return n1 + n2;
}

另一个文件名为main.c,其功能为:调用两次add函数,并打印出调用次数和add函数的返回值。

#include <stdio.h>

extern int add(int, int);
int times = 0;

int main(void)
{
    int a = 2;
    int b = 3;

    printf("%d, The sum of a and b is %d\n", times, add(a,b));
    printf("%d, The sum of a and b is %d\n", times, add(a,b));
    return 0;
}

由于这次我们的主要研究对象是可执行文件,因此,使用下面的命令编译这两个文件:

gcc -c add.c -o add.o
gcc -c main.c -o main.o
gcc -o add add.o main.o

这样我们生成了链接前的两个输入文件:add.omain.o,以及链接之后的可执行文件add。其中.o的生成在可重定位文件详解编译过程分析已经做过类似的分析,这里就不再做过多的解释。

二、可执行文件结构

2.1 可执行文件的ELF header

下面我们开看一下生成的可执行文件add的结构,由于可执行文件也属于ELF格式的文件,因此,按照惯例,我们依然先使用readelf工具从文件头开始分析。

$ readelf -h add
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:               0x400430
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6712 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 28

我们分析一下字段:

  • Type: EXEC (Executable file): 该文件是一个可执行文件;
  • Entry point address: 0x400430: 文件的入口地址为0x400430,该地址是代码段的起始地址;
  • xxx of program headers: xxx: 该文件中共有9个program header,每个的大小为56字节;
  • xxx of section headers: xxx: 该文件中共有31个section header,每个的大小为64字节。

这里可以看出,经过链接之后,生成了可执行文件,该文件的结构具有了如详解ELF文件中描述的完整的ELF文件结构。

2.2 可执行文件的section

通过读ELF header的信息可以看出上述生成的可执行文件add共有31个section。下面我们逐个分析各个section的内容。首先,通过readelf命令查看各个section的起始地址和大小,如下:

$ readelf -S add
There are 31 section headers, starting at offset 0x1a38:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       000000000000001c  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002b8  000002b8
       0000000000000060  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400318  00000318
       000000000000003f  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000400358  00000358
       0000000000000008  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400360  00000360
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400380  00000380
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400398  00000398
       0000000000000030  0000000000000018  AI       5    24     8
  [11] .init             PROGBITS         00000000004003c8  000003c8
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         00000000004003f0  000003f0
       0000000000000030  0000000000000010  AX       0     0     16
  [13] .plt.got          PROGBITS         0000000000400420  00000420
       0000000000000008  0000000000000000  AX       0     0     8
  [14] .text             PROGBITS         0000000000400430  00000430
       0000000000000202  0000000000000000  AX       0     0     16
  [15] .fini             PROGBITS         0000000000400634  00000634
       0000000000000009  0000000000000000  AX       0     0     4
  [16] .rodata           PROGBITS         0000000000400640  00000640
       0000000000000022  0000000000000000   A       0     0     4
  [17] .eh_frame_hdr     PROGBITS         0000000000400664  00000664
       000000000000003c  0000000000000000   A       0     0     4
  [18] .eh_frame         PROGBITS         00000000004006a0  000006a0
       0000000000000114  0000000000000000   A       0     0     8
  [19] .init_array       INIT_ARRAY       0000000000600e10  00000e10
       0000000000000008  0000000000000000  WA       0     0     8
  [20] .fini_array       FINI_ARRAY       0000000000600e18  00000e18
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .jcr              PROGBITS         0000000000600e20  00000e20
       0000000000000008  0000000000000000  WA       0     0     8
  [22] .dynamic          DYNAMIC          0000000000600e28  00000e28
       00000000000001d0  0000000000000010  WA       6     0     8
  [23] .got              PROGBITS         0000000000600ff8  00000ff8
       0000000000000008  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000028  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         0000000000601028  00001028
       0000000000000010  0000000000000000  WA       0     0     8
  [26] .bss              NOBITS           0000000000601038  00001038
       0000000000000008  0000000000000000  WA       0     0     4
  [27] .comment          PROGBITS         0000000000000000  00001038
       0000000000000035  0000000000000001  MS       0     0     1
  [28] .shstrtab         STRTAB           0000000000000000  00001925
       000000000000010c  0000000000000000           0     0     1
  [29] .symtab           SYMTAB           0000000000000000  00001070
       0000000000000690  0000000000000018          30    48     8
  [30] .strtab           STRTAB           0000000000000000  00001700
       0000000000000225  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)
  • .interp: 使用可重定位文件详解里介绍的hexdump命令,得到该section的内容是"/lib64/ld-linux-x86-64.so.2"。这是程序运行时需要用到的动态链接器的名字。关于动态链接和静态链接,我们会在后面的博客解释
  • .note.ABI-tag: 这个section存放的是vendor或者编译者指定的一些信息。它是以ELF spec里面规定的SHT_NOTE格式存放的。(具体参考本系列文章详解ELF文件参考阅读[1]
  • .note.gnu.build-id: 这个段主要保存的是build id,是文件的唯一标识符,他可以保存成多种格式,比如uuid形式,MD5, SHA1等,具体参考参考阅读[1]
  • .gnu.hash: 里面保存着一个符号hash表,所有参与动态链接的对象都必须包含一个符号hash表。其用法见参考阅读[2]
  • .dynsym: 这个section保存与动态链接相关的导入导出符号,不包括模块内部的符号。而 .symtab 则保存所有符号,包括 .dynsym中的符号。
    在本例中,通过readelf命令我们知道.dynsym里有四个条目:
    Symbol table '.dynsym' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
  • .gnu.version: .gnu.version里的每一项都对应.dynsym中的一个符号;每个表项的内容为:hash值、glibc版本或global/local绑定属性 。
    在本例中,我们从readelf命令里得到.gnu.version里四个条目如下:
    Version symbols section '.gnu.version' contains 4 entries:
 Addr: 0000000000400358  Offset: 0x000358  Link: 5 (.dynsym)
  000:   0 (*local*)       2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   0 (*local*)  

从其中的内容可以知道,.gnu.version的四个条目解释了.dynsym与之相对应的四个条目的版本。比如,printf这个符号对应的是GLIBC_2.2.5这个版本中的printf函数。

  • .dynstr: 这个section保存了.dynsym中所包含的符号的符号名。
  • .eh_frame_hdr:在可重定位文件详解文章中,我们了解到.eh_frame里面存放的是在发生异常时解析函数调用栈所需要的信息,而.eh_frame_hdrsection保存了.eh_frame对应的header table。这些header table可以在运行时通过调用dl_iterate_phdr函数方便的找到所有的PT_GNU_EH_FRAME segments
  • .init.init_array: 这个section中保存了该可执行程序main函数执行之前的初始化代码, 比如设置环境变量,给main函数传递参数等。关于.init.init_array的关系,我们会在稍后的博客中解释
  • .fini.fini_array: 这个section中保存了该可执行程序main函数正常退出之后执行的代码。
  • .dynamic: 这个section里保存了动态链接器所需要的基本信息,比如依赖哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。它是由是由Elfxx_DynElf32_Dyn或者Elf64_Dyn)组成的数组。Elfxx_Dyn结构由一个类型值加上一个附加的数值或指针,对于不同的类型,后面附加的数值或者指针有着不同的含义。

dynamic

  • .plt.plt.got:plt全名(Procedure Linkage Table)保存了一组16字节代码,用于在动态链接中生成所有外部过程调用符号。
  • .got.got.plt:got全名(Global Offset Table)保存了一个数组,用于配合PLT生成库函数的真正地址并保存该地址。稍后博客中我们会详细解释该过程
  • .rela.plt:这个section保存了.plt中保存的符号的重定位信息。
  • .rela.dyn:这个section保存了除了.plt中保存的符号外的其他符号的重定位信息。
  • .jcr:这个section保存了编译java类所必须的信息。它里面内容是编译器指定的,并且被编译器的初始化函数使用。
  • .text:这个section用来保存可执行程序的代码编译生成的二进制代码。其内容我们通过objdump -C add命令查看后发现,反编译出来的函数不仅包括我们定义的add函数和main函数还包括了其他一些函数,这些函数是链接器加进去的,稍后的博客会详细解释

  • .rodata.eh_frame.data.bss.comment.shstrtab.symtab.strtab可重定位文件详解包含内容的意义是一样的,这里就不再重复介绍。

2.3 可执行文件的segment

前面我们谈到过,segment其实是由section组成的,那么哪些section组成了哪些segment呢?我们可以从readelf -l add输出的program header table中得到这些信息。

$ readelf -l add

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

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000007b4 0x00000000000007b4  R E    200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x0000000000000228 0x0000000000000230  RW     200000
  DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x0000000000000664 0x0000000000400664 0x0000000000400664
                 0x000000000000003c 0x000000000000003c  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x00000000000001f0 0x00000000000001f0  R      1

 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 .plt.got .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

根据上述信息:
1. 一共生成了9个segment,其中第0个segment对应PHDR,第1个segment对应INTERP,第2个segment对应LOAD,以此类推,第8个segment对应GNU_RELRO。
2. 第0个segment(PHDR)保存了各个segment的VirtAddr和MemSiz。
3. 第1个segment是只读的,只保存了.interp section。
4. 第2个segment就是我们常说的一个可执行文件的代码段,这部分是要由加载器加载到内存中执行的,因此其属性是可读可执行的,保存了.interp .note.ABI-tag .note.gnu.build-id ... 等18个section。
5. 第3个segment是我们常说的数据段,它保存了和可执行程序执行过程相关的所有数据,这部分也是要加载到内存中的,因此其属性是可读可写的,保存了.init_array .fini_array ...等8个section。
6. 第4个segment是可读可写的,保存了.dynamic section。
7. 第5个segment是只读的,保存了.note.ABI-tag .note.gnu.build-id这两个section。
8. 第6个segment是只读的,保存了.eh_frame_hdr section。
9. 第7个segment是可读可写的,它告诉系统,当加载这个可执行文件的时候,如何设置其栈。
10. 第8个segment是只读的,保存了.init_array .fini_array .jcr .dynamic .got这5个section。

三、参考阅读

  1. build id:
    1)Installing Debuginfo Packages for Core Files Analysis
    2)Command Line Options
  2. GNU Hash ELF Sections

发表回复

您的电子邮箱地址不会被公开。