目标文件中到底有什么?

来源:互联网 发布:淘宝网个人资料 编辑:程序博客网 时间:2024/04/28 05:46

目标文件到底有什么?

本系列文章主要是讲述c++编译链接的那点事,这个对于刚入门的程序员来说是必须修炼的内功之一。第一篇文章将主要配合示例来说明c++编译后的.o文件究竟有些什么?

前言

在了解.o文件之前,有必要理清楚基本的编译流程, 如下图所示:

基本编译流程

  • 预处理阶段:预处理器主要是处理源代码中以”#”开始的命令,比如头文件等等,生成.i中间文件。预处理的命令:
$gcc -E hello.c -o hello.i
  • 编译阶段: 编译器会将.i文件翻译成.s文件,具体过程是通过一系列的词法分析,语义分析,以及优化后生成相应的汇编文件。其处理命令为:
$gcc -S hello.i -o hello.s
  • 汇编阶段: 汇编器是将汇编代码翻译成机器语言指令,并打包成一个可重定位目标程序,并将结果保留在目标文件中。它的字节编码是机器语言指令,而不是字符。
$gcc -c hello.s -o hello.o
  • 链接阶段: 链接器会把所有的.o文件进行链接,并生成最终的可执行文件。那么为什么不直接在汇编过程中就完成链接过程呢?为什么需要链接?
    这里先给出一个粗略的回答:首先,我们要明白链接器主要作用是符号解析和地址的重定位,这个具体是什么意思,后续系列会慢慢涉及到。其次,由于链接器处理了符号的解析和地址的重定位,那么使得我们在构造大型软件工程时,自然会思考我们可否借鉴建房子的过程,一块一块砖垒起来,水泥就是中间的粘合剂。于是,写代码就能够分模块完成,最后由链接器完成类似水泥的工作就行了。链接器使得每个模块的分离编译成为可能,当我们需要改变其中一个模块时,只需要重新编译和链接,而不必再编译其他模块文件了,大大节约了时间成本。

总之,编译的基本流程如上所述,在链接实现的过程中,时间点可以是在编译时链接,可以是加载时链接,甚至是运行时链接。这三种的不同会在下面以及后续文章慢慢涉及到。

目标文件的神秘面纱

从上述编译的流程我们可以大概猜到,目标文件至少是包括相关的机器指令代码和数据的,除此之外,也会包括链接时所需要的一些信息,比如符号表、调试信息等等。一般而言,目标文件是以节(section) 为单位进行组织的,一个典型的可重定位目标文件如下:

可重定位目标文件 ELF头: 描述生成该文件的系统的字的大小和字节顺序、头大小、目标文件类型、机器类型、节头部表的文件偏移、节头部表中的条目大小和数量 .text : 编译后的机器代码 .rodata : 只读数据,一般是程序里面的只读变量和字符串常量 .data : 初始化后的全局变量和初始化局部静态变量 .bss : 未初始化的全局变量和未初始化的局部静态变量 .symtab : 符号表,里面记录了程序中的函数和全局变量相关信息 .rel.text : 重定位.text的表,里面记录了.text的位置信息等等,方便链接器进行符号的重定位 .rel.data : 重定位全局变量的表 .strtab : 字符串表,单纯记录了相关符号名称

代码示例分析

源代码SimpleSection.c如下:

int printf(const char* format, ...);int g_init_var = 25;    //初始化的全局变量int g_uninit_var;       //未初始化的全局变量void callPrintf(int i) {    printf("%d\n", i);}int main(void) {    static int static_var = 70;     //局部的初始化静态变量    static int static_var_uninit;   //局部的未初始化的静态变量    int a = 1;  //局部的初始化变量    int b;      //局部的未初始化的变量    callPrintf(static_var + static_var_uninit + a + b);    return a;}

我们先编译生成SimpleSection.o, 然后使用下面的命令查看

objdump -h SimpleSection.o

SimpleSectionObject描述

注意到.data起始位置是从0x00000094而不是0x00000091开始的,原因在于.text和.data的对齐方式是4字节,即必须要被4整除:91=9*16+1; 94=9*16+4,可以明白94能被4整除,所以.data是从0x00000094开始的。

根据偏移量可画出相应的图如下(注意地址是向上增长的):

SimpleSectionObject结构

为了看清楚每个section具体里面是什么,我们采用下面命令:

objdump -s -d SimpleSection.o

得到如下的内容:

SimpleSection.o:     file format elf64-x86-64Contents of section .text: 0000 554889e5 4883ec10 897dfc8b 45fc89c6  UH..H....}..E... 0010 bf000000 00b80000 0000e800 000000c9  ................ 0020 c3554889 e54883ec 10c745f8 01000000  .UH..H....E..... 0030 8b150000 00008b05 00000000 8d040203  ................ 0040 45f80345 fc89c7e8 00000000 8b45f8c9  E..E.........E.. 0050 c3                                   .               Contents of section .data: 0000 19000000 46000000                    ....F...        Contents of section .rodata: 0000 25640a00                             %d..            Contents of section .comment: 0000 00474343 3a202847 4e552920 342e342e  .GCC: (GNU) 4.4. 0010 36203230 31313037 33312028 52656420  6 20110731 (Red  0020 48617420 342e342e 362d3429 00        Hat 4.4.6-4).   Contents of section .eh_frame: 0000 14000000 00000000 017a5200 01781001  .........zR..x.. 0010 1b0c0708 90010000 1c000000 1c000000  ................ 0020 00000000 21000000 00410e10 8602430d  ....!....A....C. 0030 065c0c07 08000000 1c000000 3c000000  .\..........<... 0040 00000000 30000000 00410e10 8602430d  ....0....A....C. 0050 066b0c07 08000000                    .k......        Disassembly of section .text:0000000000000000 <callPrintf>:   0:   55                      push   %rbp   1:   48 89 e5                mov    %rsp,%rbp   4:   48 83 ec 10             sub    $0x10,%rsp   8:   89 7d fc                mov    %edi,-0x4(%rbp)   b:   8b 45 fc                mov    -0x4(%rbp),%eax   e:   89 c6                   mov    %eax,%esi  10:   bf 00 00 00 00          mov    $0x0,%edi  15:   b8 00 00 00 00          mov    $0x0,%eax  1a:   e8 00 00 00 00          callq  1f <callPrintf+0x1f>  1f:   c9                      leaveq   20:   c3                      retq   0000000000000021 <main>:  21:   55                      push   %rbp  22:   48 89 e5                mov    %rsp,%rbp  25:   48 83 ec 10             sub    $0x10,%rsp  29:   c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)  30:   8b 15 00 00 00 00       mov    0x0(%rip),%edx        # 36 <main+0x15>  36:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 3c <main+0x1b>  3c:   8d 04 02                lea    (%rdx,%rax,1),%eax  3f:   03 45 f8                add    -0x8(%rbp),%eax  42:   03 45 fc                add    -0x4(%rbp),%eax  45:   89 c7                   mov    %eax,%edi  47:   e8 00 00 00 00          callq  4c <main+0x2b>  4c:   8b 45 f8                mov    -0x8(%rbp),%eax  4f:   c9                      leaveq   50:   c3                      retq

可以看到,“Contents of section .text”长度确实为51个字节,其反汇编形成的正好是函数callPrintf( )和main( )。在数据段.data中,内容为“19000000 46000000”,为8个字节。注意到数据段储存的是初始化的全局变量和局部静态变量。我们的代码中有g_init_var = 25, static_var = 70, 都属于int,而int所占字节为4,所以两个变量占据8个字节。由于机器是小端储存的形式,00000019对应25, 00000046对应70。

重头戏节头表和段表

在基本了解了ELF文件的轮廓后,我们需要更加细致地研究下其中某些重要的表,这些表中会记录更多信息,以方便链接器进行链接。

  • 文件头(ELF Header)
    文件头记录了描述整个文件的基本属性值,比如ELF文件版本、目标机器型号、程序入口地址等等。我们可以通过如下命令查看文件头的信息:
readelf -h SimpleSection.o

得到示例代码的如下信息:

SimpleSectionObject头信息

我着重描述下几个重要的属性:程序入口地址,这个规定了ELF程序的入口虚拟地址,操作系统在加载完该程序后从这个地址开始执行进程的指令。Start of section headers, 段表在文件中的偏移值,该示例是400,也就是段表是从文件的第401个字节开始的。Size of section headers: 每个段表描述符的大小; Number of section headers: 段表的个数。

  • 段表: 段表是除了文件头外最重要的结构,主要描述了ELF中的各个段的信息,比如每个段的段名,段的长度,段在文件中的偏移、读写权限等等。ELF文件的段结构就是由段表来决定的,编译器、链接器和装载器都是根据段表来定位和访问各个段的属性等等。并且通过文件头和段表信息,我们可以画出ELF更加细致的结构。
    通过如下命令查看示例代码的段表结构:
readelf -S SimpleSection.o

结果如下所示:

There are 13 section headers, starting at offset 0x190: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       0000000000000051  0000000000000000  AX       0     0     4  [ 2] .rela.text        RELA             0000000000000000  000006b8       0000000000000078  0000000000000018          11     1     8  [ 3] .data             PROGBITS         0000000000000000  00000094       0000000000000008  0000000000000000  WA       0     0     4  [ 4] .bss              NOBITS           0000000000000000  0000009c       0000000000000004  0000000000000000  WA       0     0     4  [ 5] .rodata           PROGBITS         0000000000000000  0000009c       0000000000000004  0000000000000000   A       0     0     1  [ 6] .comment          PROGBITS         0000000000000000  000000a0       000000000000002d  0000000000000001  MS       0     0     1  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000cd       0000000000000000  0000000000000000           0     0     1  [ 8] .eh_frame         PROGBITS         0000000000000000  000000d0       0000000000000058  0000000000000000   A       0     0     8  [ 9] .rela.eh_frame    RELA             0000000000000000  00000730       0000000000000030  0000000000000018          11     8     8  [10] .shstrtab         STRTAB           0000000000000000  00000128       0000000000000061  0000000000000000           0     0     1  [11] .symtab           SYMTAB           0000000000000000  000004d0       0000000000000180  0000000000000018          12    11     8  [12] .strtab           STRTAB           0000000000000000  00000650       0000000000000067  0000000000000000           0     0     1Key to Flags:  W (write), A (alloc), X (execute), M (merge), S (strings)  I (info), L (link order), G (group), x (unknown)  O (extra OS processing required) o (OS specific), p (processor specific)

readelf 相比于objdump,可以查看到更加细致的结构。我们注意到第一个段类型为NULL,表示无效的,所以实际上SimpleSection.o只有12个有效的段。结合所有的节头表,得到完整的ELF结构如下:
SimpleSectionObject完整结构

  • 重定位表:在上述分析中,我们看到有个段表叫做.rela.text,那么它的作用是什么呢?我们知道,链接器在处理目标文件时,需要对目标文件某些部分进行重定位,比如在我们的源码中,main函数中调用了callPrintf函数,这里表明.text中有一处对于该绝对地址的引用。

最后在详解静态链接时,我们再看看符号表里面的内容,为以后分析静态链接做准备。命令如下:

readelf -s SimpleSection.o

信息如下:

Symbol table '.symtab' contains 16 entries:   Num:    Value          Size Type    Bind   Vis      Ndx Name     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND      1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS SimpleSection.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: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 static_var.1601     7: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 static_var_uninit.1602     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7      9: 0000000000000000     0 SECTION LOCAL  DEFAULT    8     10: 0000000000000000     0 SECTION LOCAL  DEFAULT    6     11: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 g_init_var    12: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM g_uninit_var    13: 0000000000000000    33 FUNC    GLOBAL DEFAULT    1 callPrintf    14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf    15: 0000000000000021    48 FUNC    GLOBAL DEFAULT    1 main
0 0