2013年10月25日

Linux进程地址空间学习(二)


可执行文件的格式

今天在看ELF文件格式的过程中,想起了之前浩哥在coolshell上贴出的一篇文章:C语言全局变量那些事儿,另外在StackOverFlow上也有相应的讨论,无私的大神们给出了详尽的答案,突然发现在学习ELF的过程中可以顺道窥探出其中的奥秘,将知识连贯起来的兴奋劲不言而喻。

ELF文件

ELF是Executable and Linking Format的缩写,即可执行链接格式,最初由UNIX实验室开发发布的,作为应用程序二进制接口的一部分。简单的来说,ELF就是描述了可执行文件的格式规范,也就是说我们的执行文件到底该是什么样的格式。

在理解具体的文件格式之前,我们需要知道ELF文件格式可以用来描述三种类型的文件:

ELF文件格式提供了两种并行视图,来分别描述链接和执行过程,从不同的角度可以有不同的含义。可以仔细的看下面的图,然后我们辅以实际的文件来讲解其中的格式。

从图中可以知道,左侧的视角是从链接来看的,右侧的视角是执行来看的。总个文件可以分为四个部分:

ELF头部

我们还是使用上一篇中的代码编译出的执行文件来看。

#include<iostream>

using namespace std;

int global_a = 100;
int global_b;
static int global_static_a = 200;

int main(){
    static int main_static_b = 200;
    static int main_static_c;
    cout << "Hello World" << endl;
    return 0;
}

将上面的代码编译之后我们可以得到可执行文件a.out,然后查看该文件的头部,使用readelf -h a.out,结果如下:

choudan@ubuntu:~/coding/cpp$ 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)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x8048560
  Start of program headers:          52 (bytes into file)
  Start of section headers:          4456 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         9
  Size of section headers:           40 (bytes)
  Number of section headers:         31
  Section header string table index: 28

从上面的内容中可以看出文件头主要描述了文件的格式,操作系统的大小端特征,系统位数,硬件相关的体系架构等等,具体的头文件格式我们可以查看后面参考文档列出的第一篇文章。在此头文件中,我们关注以下几个比较重要的点:

sections

在前面提到了section是从链接的角度出发来理解的,文件末尾存在一个section header tables 描述了文件中存在的每一个section,我们可以通过readelf命令查看文件中到底有哪些section,我们之前理解的代码段,数据段和bss段与section有什么关系?

choudan@ubuntu:~/coding/cpp$ readelf -S a.out 
There are 31 section headers, starting at offset 0x1168:

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        08048154 000154 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048168 000168 000020 00   A  0   0  4
  [ 3] .note.gnu.build-i NOTE            08048188 000188 000024 00   A  0   0  4
  [ 4] .gnu.hash         GNU_HASH        080481ac 0001ac 000030 04   A  5   0  4
  [ 5] .dynsym           DYNSYM          080481dc 0001dc 0000c0 10   A  6   1  4
  [ 6] .dynstr           STRTAB          0804829c 00029c 00014b 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          080483e8 0003e8 000018 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         08048400 000400 000050 00   A  6   2  4
  [ 9] .rel.dyn          REL             08048450 000450 000010 08   A  5   0  4
  [10] .rel.plt          REL             08048460 000460 000040 08   A  5  12  4
  [11] .init             PROGBITS        080484a0 0004a0 00002e 00  AX  0   0  4
  [12] .plt              PROGBITS        080484d0 0004d0 000090 04  AX  0   0 16
  [13] .text             PROGBITS        08048560 000560 0001fc 00  AX  0   0 16
  [14] .fini             PROGBITS        0804875c 00075c 00001a 00  AX  0   0  4
  [15] .rodata           PROGBITS        08048778 000778 000014 00   A  0   0  4
  [16] .eh_frame_hdr     PROGBITS        0804878c 00078c 000044 00   A  0   0  4
  [17] .eh_frame         PROGBITS        080487d0 0007d0 000104 00   A  0   0  4
  [18] .init_array       INIT_ARRAY      08049ef8 000ef8 000004 00  WA  0   0  4
  [19] .ctors            PROGBITS        08049efc 000efc 000008 00  WA  0   0  4
  [20] .dtors            PROGBITS        08049f04 000f04 000008 00  WA  0   0  4
  [21] .jcr              PROGBITS        08049f0c 000f0c 000004 00  WA  0   0  4
  [22] .dynamic          DYNAMIC         08049f10 000f10 0000e0 08  WA  6   0  4
  [23] .got              PROGBITS        08049ff0 000ff0 000004 04  WA  0   0  4
  [24] .got.plt          PROGBITS        08049ff4 000ff4 00002c 04  WA  0   0  4
  [25] .data             PROGBITS        0804a020 001020 000014 00  WA  0   0  4
  [26] .bss              NOBITS          0804a040 001034 0000a0 00  WA  0   0 32
  [27] .comment          PROGBITS        00000000 001034 00002a 01  MS  0   0  1
  [28] .shstrtab         STRTAB          00000000 00105e 000108 00      0   0  1
  [29] .symtab           SYMTAB          00000000 001640 000500 10     30  52  4
  [30] .strtab           STRTAB          00000000 001b40 0003c3 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)

从该命令的第一行可以知道section header tables在文件中的偏移量为0x1168,转换成十进制就是elf header中描述的4456。此文件中存在31个section,仔细看看,就可以找到熟悉的.text,.data,.bss。这三个section就是我们常提到的代码段数据段等,除了这些段之外,还有很多其他的,譬如前面提到过的28,是字符串表的header,编码是29的section则是符号表的header。

section header table中的每一个section都由十个属性描述,例如section的名字,类型,标志,地址,在文件中的偏移,section的大小等等,具体每个含义大家参考文章末尾给出的ELF文件格式分析一文。

其中关键的属性解释下: * type: section有多种类型,例如PROGBITS,表示此section的内容由程序来解释,.text和.data都是,还有NOBITS,表示此段不占用文件空间,STRTAB是表示字符串表,SYMTAB表示符号表等等。 * flags: 可以在该命令的最末看出不同flag标志的含义,其中Alloc,表示该section在进程执行过程中占用内存。 * addr : 如果section将出现在进程的内存映像中,此成员给出section的第一个字节应处的位置。

字符串表

我们提到了第29项是字符串表,即索引为28的项,当然还有其他的section也是字符串表,例如索引为30的section,那么字符串表中存的是哪些内容了。我们同样可以使用readelf读取出来。

choudan@ubuntu:~/coding/cpp$ readelf -p 28 a.out

String dump of section '.shstrtab':
  [     1]  .symtab
  [     9]  .strtab
  [    11]  .shstrtab
  [    1b]  .interp
  [    23]  .note.ABI-tag
  [    31]  .note.gnu.build-id
  [    44]  .gnu.hash
  [    4e]  .dynsym
  [    56]  .dynstr
  [    5e]  .gnu.version
  [    6b]  .gnu.version_r
  [    7a]  .rel.dyn
  [    83]  .rel.plt
  [    8c]  .init
  [    92]  .text
  [    98]  .fini
  [    9e]  .rodata
  [    a6]  .eh_frame_hdr
  [    b4]  .eh_frame
  [    be]  .init_array
  [    ca]  .ctors
  [    d1]  .dtors
  [    d8]  .jcr
  [    dd]  .dynamic
  [    e6]  .got
  [    eb]  .got.plt
  [    f4]  .data
  [    fa]  .bss
  [    ff]  .comment

在该section中存取的就是这些字符串,变长的,每个字符串都以0结尾。在section header tables中每一项都有一个name,那么只需要将name设置成其中shstrtab中的索引就可以了,不需要真的存储实际的字符串。

同样,在strtabsection中,也是字符串,只是其中的内容是程序中各种符号的名字,我们截取部分看看:

choudan@ubuntu:~/coding/cpp$ readelf -p 28 a.out

String dump of section '.strtab':
  [     1]  crtstuff.c
  [    9b]  __do_global_ctors_aux
  [    b1]  hello.cpp
  [    bb]  _ZStL8__ioinit
  [    ca]  _ZL15global_static_a
  [   10f]  _GLOBAL__sub_I_global_a
  [   127]  _ZZ4mainE13main_static_c
  [   140]  _ZZ4mainE13main_static_b
  [   159]  _GLOBAL_OFFSET_TABLE_
  [   1d8]  __gmon_start__
  [   202]  _fini
  [   22d]  __libc_start_main@@GLIBC_2.0
  [   2c3]  __data_start
  [   311]  global_a
  [   31a]  __bss_start
  [   326]  _end
  [   398]  global_b
  [   3b8]  main
  [   3bd]  _init

上面的内容只是部分字符串,删除了一些,我们看到一些很熟悉的符号,例如在程序中定义的变量,global_a,函数的符号main等等。

符号表

符号表包含用来定位,重定位程序中符号定义和引用的信息,简单的理解就是符号表记录了该文件中的所有符号,所谓的符号就是经过修饰了的函数名或者变量名,不同的编译器有不同的修饰规则。例如在下面将看到的符号_ZL15global_static_a,就是由global_static_a变量名经过修饰而来。

我们先来看符号表的结构,依然可以使用readelf命令读取文件中的符号表section,具体如下:

choudan@ubuntu:~/coding/cpp$ readelf -p 28 a.out

Symbol table '.dynsym' contains 12 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 FUNC    GLOBAL DEFAULT  UND __cxa_atexit@GLIBC_2.1.3 (2)
     2: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     8: 0804877c     4 OBJECT  GLOBAL DEFAULT   15 _IO_stdin_used

Symbol table '.symtab' contains 80 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
    27: 00000000     0 SECTION LOCAL  DEFAULT   27 
    31: 08049f0c     0 OBJECT  LOCAL  DEFAULT   21 __JCR_LIST__
    32: 08048590     0 FUNC    LOCAL  DEFAULT   13 __do_global_dtors_aux
    36: 00000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    39: 08049f0c     0 OBJECT  LOCAL  DEFAULT   21 __JCR_END__
    41: 00000000     0 FILE    LOCAL  DEFAULT  ABS hello.cpp
    43: 0804a02c     4 OBJECT  LOCAL  DEFAULT   25 _ZL15global_static_a
    45: 08048688    28 FUNC    LOCAL  DEFAULT   13 _GLOBAL__sub_I_global_a
    46: 0804a0dc     4 OBJECT  LOCAL  DEFAULT   26 _ZZ4mainE13main_static_c
    48: 08049ff4     0 OBJECT  LOCAL  DEFAULT   24 _GLOBAL_OFFSET_TABLE_
    50: 08049ef8     0 NOTYPE  LOCAL  DEFAULT   18 __init_array_start
    51: 08049f10     0 OBJECT  LOCAL  DEFAULT   22 _DYNAMIC
    55: 08048560     0 FUNC    GLOBAL DEFAULT   13 _start
    57: 00000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
    70: 0804a028     4 OBJECT  GLOBAL DEFAULT   25 global_a
    73: 00000000     0 FUNC    GLOBAL DEFAULT  UND _ZNSolsEPFRSoS_E@@GLIBCXX
    76: 0804a0d4     4 OBJECT  GLOBAL DEFAULT   26 global_b
    78: 08048614    52 FUNC    GLOBAL DEFAULT   13 main

此文件中存在两张符号表,内容都较多,删除了一些,留下了一些有代表性的数据。从上面的结果知道,符号表包含很多项,每项都由7个属性值描述。注意Bind一列,描述了变量的链接可见性和行为。

在每个符号表中,绑定为local的符号优先级都高于弱符号和全局符号。那么到底什么样的符号会成为弱符号,什么样的会成为全局符号?weak和global都是针对全局变量来说的,若全局变量初始化了,则是global绑定,若没有初始化则是weak绑定。我们还知道,若全局变量没有初始化,则会被编译器放到.bss段,置为0。大家还可以参考这篇文章来加深理解,GCC中的弱符号和强符号,还有参考文档中的第四条,这个文档给出了更详细清楚的解释。

那么,WEAK符号和GLOBAL符号之间的又存在哪些差别了,上面的链接中给出了详细的解释,这儿再将主要的三点区别贴出来:

在符号表中,除了Bind这一列之外,还可以看到符号的类型,有OBJECT,FUNC,FILE等等。Ndx列给出了该符号表项关联的section,即该符号在section header tables中索引,例如第70条,符号是global_a,在section header tables的索引是25,查看section headers table索引值是25的表项,就是我们熟悉的.data段。除了能给出具体的索引值之外,我们发现还有其他的几种情况存在,例如看到的ABS,UND。其实还存在三种特别的索引:

  1. ABS: 此表示符号具有绝对取值,不会因为重定位而发生变化。
  2. COMMON: 对于全局未初始化的变量,先标记为common,该变量也是前面提到的弱符号,具体解释可以参考程序员的自我修养第111页。
  3. UND: 这代表着符号没有定义。例如,我们在文件a中引用了b文件中的变量c,

重定位表

重定位表在ELF文件中扮演很重要的角色,首先我们得理解重定位的概念,程序从代码到可执行文件这个过程中,要经历编译器,汇编器和链接器对代码的处理。然而编译器和汇编器通常为每个文件创建程序地址从0开始的目标代码,但是几乎没有计算机会允许从地址0加载你的程序。如果一个程序是由多个子程序组成的,那么所有的子程序必需要加载到互不重叠的地址上。重定位就是为程序不同部分分配加载地址,调整程序中的数据和代码以反映所分配地址的过程。简单的言之,则是将程序中的各个部分映射到合理的地址上来。

我们可以使用readelf -r 命令查看该文件中的重定位表,这个系统存在两张,如下所列出来的。

choudan@ubuntu:~/coding/cpp$ readelf -r a.out 

Relocation section '.rel.dyn' at offset 0x450 contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
08049ff0  00000206 R_386_GLOB_DAT    00000000   __gmon_start__
0804a040  00000b05 R_386_COPY        0804a040   _ZSt4cout

Relocation section '.rel.plt' at offset 0x460 contains 8 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804a000  00000107 R_386_JUMP_SLOT   00000000   __cxa_atexit
0804a004  00000207 R_386_JUMP_SLOT   00000000   __gmon_start__
0804a008  00000407 R_386_JUMP_SLOT   00000000   _ZNSt8ios_base4InitC1E
0804a00c  00000507 R_386_JUMP_SLOT   00000000   __libc_start_main
0804a010  00000a07 R_386_JUMP_SLOT   08048520   _ZNSt8ios_base4InitD1E
0804a014  00000607 R_386_JUMP_SLOT   00000000   _ZStlsISt11char_traits
0804a018  00000707 R_386_JUMP_SLOT   00000000   _ZNSolsEPFRSoS_E
0804a01c  00000907 R_386_JUMP_SLOT   08048550   _ZSt4endlIcSt11char_tr

这张表描述了哪些符号在链接过程中需要进行重定位,这个表中给出了足够的信息告诉链接器采用什么方的方式计算符号的最终地址。关于这张表的具体描述大家可以参考这个链接,ELF重定位理解

总结

越是对ELF深究,越发现这一块的知识点很多,想通过这一篇博客来描述清楚ELF文件格式几乎是不可能的,但是在尝试理清ELF格式的过程中,发现了不少值得学习的书籍或者有意思的点,充满了趣味。其中最想推荐给大家的是《程序员的自我修养》一书,可以找到关于程序链接装载的所有知识点,并且详细易懂。

对于ELF的理解,我觉的可以尝试顺着这样的思路。一般的,可执行文件是由多个源码文件和各种库文件构成的完整源码,经过编译汇编生成一堆的目标文件,然后目标文件在链接器的辅助下链接成我们期待的可执行文件。那么在此过程中,每个源码文件都单独编译成目标文件,并且目标文件生成的代码都是从地址0开始,目标文件还可能引用了其他目标文件的变量或者函数,这就意味着在单独编译一个源码文件时,该文件引用的其他目标文件的变量和函数地址都是不知道的,所以需要重定位操作,并且将变量和函数统一成符号一种概念来处理了。在最终的链接过程中,还需要将代码都映射到正确的虚拟地址上,所以ELF格式必须提供完成链接过程所需要的全部信息了。当然,上面描述的过程只是简化了的过程,还有许多其他的问题也需要考虑,例如程序的装载,如何完成从磁盘到内存,完成运行。依然推荐程序猿的自我修养,可以加深对这一切的理解。

参考文档

  1. 强烈推荐的书籍: 程序猿的自我修养。
  2. ELF文件格式分析
  3. 程序的链接和装入及Linux下动态链接的实现
  4. ELF格式文件
  5. C语言中的强符号与弱符号
  6. ELF重定位理解
前一篇: Linux进程地址空间再学习 后一篇: Linux进程地址空间学习(一)