程序员自我修养阅读笔记——目标文件里有什么

Posted grayondream

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了程序员自我修养阅读笔记——目标文件里有什么相关的知识,希望对你有一定的参考价值。

测试环境:

➜  tmp uname --version
uname (GNU coreutils) 8.25
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by David MacKenzie.
➜  tmp gcc --version
gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

1 目标文件格式

  PC平台的目标文件格式大都是COFF的变种,比如Windows的PE(Portable Executable)格式和Linux的ELF(Executable Linkable Format)格式。并且我们一般讲的目标文件格式多指可执行文件,但是实际上编译过程中的静态库文件、动态库文件和.o或者.obj文件都属于目标文件。常见的目标文件分类:

目标文件类型说明举例
可执行文件可以直接执行的程序windows的exe文件、linux的可执行文件、macOs的app文件
共享目标文件包含了程序的代码和数据,可以在链接阶段和其他可重定位目标文件或者共享目标文件链接生成可执行文件;作为动态共享的链接库,在程序运行时进行装载Linux的so,windows的dll,macOs的dylib
核心转储文件当程序意外终止时,系统保存的进程的地址空间等信息的转储文件linux的core dump
可重定位文件包含程序的代码和数据,可被用来链接为目标文件静态库,.o文件或者.obj文件

2 目标文件内容

  目标文件中无疑包含程序运行的代码和数据,只是如何对这些内容进行管理?目标文件管理这些内容通过段的方式进行,不同类型的数据等信息通过段进行区分。分段的好处:

  • 程序中代码是只读,部分数据可读可写,通过分段能够方便进行权限管理;
  • 计算机的八二原则,不同数据和代码分开存储能够有效利用计算机的缓存功能;
  • 有利于资源共享,通常计算机中代码是只读的,因此当又多个程序需要使用同一份代码时,可以将共享的内容区分开方便共享,节省资源。

尝试使用相关命令查看目标文件的内容:
  使用的文件示例,文件中包含常量字符串、静态初始化变量、静态未初始化变量、局部初始化变量、局部未初始化变量、全局初始化变量和全局未初始化变量以及简单函数调用(文件中带a的都是初始化过的,带b的都是未经初始化的)。查看使用的命令的简单用法见Linux objdump使用

int add(int a, int b){
    return a + b;
}

const char *file = "main.o";
int glob_a = 15;
int glob_b;
//test
int main(){
    static char static_a = 16;
    static char static_b;
    long long a = 3;
    long long b;
    add(a, b);
}

  使用gcc -c main.cpp -o main.o编译生成main.o。使用objdmup -h main.o查看各个段的大小:

Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000003e  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000005  0000000000000000  0000000000000000  00000080  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000005  0000000000000000  0000000000000000  00000088  2**2
                  ALLOC
  3 .rodata       00000007  0000000000000000  0000000000000000  00000088  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .data.rel.local 00000008  0000000000000000  0000000000000000  00000090  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, DATA
  5 .comment      0000002a  0000000000000000  0000000000000000  00000098  2**0
                  CONTENTS, READONLY
  6 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000c2  2**0
                  CONTENTS, READONLY
  7 .eh_frame     00000058  0000000000000000  0000000000000000  000000c8  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

  size main.o能够查看数据段和代码段的大小:

   text    data     bss     dec     hex filename
    152      16       8     176      b0 main.o

  从上面的结果中:第一列为段的索引;第二列为段的名称;第三列为段的尺寸;第三列为段的虚拟内存地址;第四段为局部内存地址;第五列为段在程序中的偏移;每个段再买呢的字段CONTENTS表示该段在文件中存在,READONLY表示只读,ALLOC表示表示有该标记的节会在运行时分配并装载进入内存。根据文件中的偏移画出的文件结构图如下:

  从输出的段结构图中能够看到bssrodata的偏移一致,且二者都有各自的尺寸,并且虽然有.note.GUN-stack但是该段没有尺寸:

  • .text:代码段,存储程序的代码,可以通过objdump -s -d main.o反汇编查看;
  • .data:数据段,存储已经初始化了的全局静态变量和局部静态变量,从图中查看刚好一个int和char的尺寸;
  • .bss:存储未经初始化的全局变量和局部静态变量,尺寸计算同.data
  • .rodata:存放只读数据,程序中main.o的字符串长度为6,而该段长度为7推断包含最后的\\0
  • .comment:存放编译版本信息;
  • .note.GNU-stack:堆栈提示段;
  • .eh_frame:主要用于系统运行时调试使用的,便于栈展开调试。

  使用objdump -s main.o查看每个段的具体内容,能够看到data段中0f10刚好对应15和16:

Contents of section .data:
 0000 0f000000 10                          .....
Contents of section .rodata:
 0000 6d61696e 2e6f00                      main.o.
Contents of section .data.rel.local:
 0000 00000000 00000000                    ........
Contents of section .comment:
 0000 00474343 3a202855 62756e74 7520372e  .GCC: (Ubuntu 7.
 0010 352e302d 33756275 6e747531 7e31382e  5.0-3ubuntu1~18.
 0020 30342920 372e352e 3000               04) 7.5.0.

  上面的内容中并未看到bss的内容,通过查看符号表objdump -t main.o能够看到未经初始化的static_bglob_b存储在bss中。但是这也不是很绝对,因为全局符号存在强符号和弱符号的区分,未经初始化的全局变量可能初始化为COMMON在链接时再分配内存。

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 main.cpp
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .rodata        0000000000000000 .rodata
0000000000000000 l    d  .data.rel.local        0000000000000000 .data.rel.local
0000000000000004 l     O .data  0000000000000001 _ZZ4mainE8static_a
0000000000000004 l     O .bss   0000000000000001 _ZZ4mainE8static_b
0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame      0000000000000000 .eh_frame
0000000000000000 l    d  .comment       0000000000000000 .comment
0000000000000000 g     F .text  0000000000000014 _Z3addii
0000000000000000 g     O .data.rel.local        0000000000000008 file
0000000000000000 g     O .data  0000000000000004 glob_a
0000000000000000 g     O .bss   0000000000000004 glob_b
0000000000000014 g     F .text  000000000000002a main

  下面时将源文件使用c进行编译得到的未初始化的全局符号的存储方式,时典型的弱符号存储方式:

0000000000000004       O *COM*  0000000000000004 glob_b

  ELF文件还包含很多其他段,比如调试信息相关的段不再赘述。

3 ELF文件结构

  ELF文件的格式大致如下,其中比较重要的时文件头和段表:文件头描述文件的基本信息;段表类似所有段即section的指针表。

ELF Header:
  可以使用readelf -h main.o查看可执行文件中的header,ELF Header 中定义了 ELF Magic Code、文件机器字节长度、数据存储方式、版本、运行平台、ABI 版本、ELF 重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口与长度、Section Header 的偏移位置和长度以及 Section 数量等。

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:          1000 (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:         15
  Section header string table index: 14

段表:
  段表顾名思义,存储不同段的地方,实际存储的时段的描述符,该描述符会描述段的类型,大小等信息。可通过readelf -S main.o查看,因为下面需要用到一些段因此贴到这里。

There are 15 section headers, starting at offset 0x3e8:

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
       000000000000003e  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000310
       0000000000000018  0000000000000018   I      12     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000080
       0000000000000005  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  00000088
       0000000000000005  0000000000000000  WA       0     0     4
  [ 5] .rodata           PROGBITS         0000000000000000  00000088
       0000000000000007  0000000000000000   A       0     0     1
  [ 6] .data.rel.local   PROGBITS         0000000000000000  00000090
       0000000000000008  0000000000000000  WA       0     0     8
  [ 7] .rela.data.rel.lo RELA             0000000000000000  00000328
       0000000000000018  0000000000000018   I      12     6     8
  [ 8] .comment          PROGBITS         0000000000000000  00000098
       000000000000002a  0000000000000001  MS       0     0     1
  [ 9] .note.GNU-stack   PROGBITS         0000000000000000  000000c2
       0000000000000000  0000000000000000           0     0     1
  [10] .eh_frame         PROGBITS         0000000000000000  000000c8
       0000000000000058  0000000000000000   A       0     0     8
  [11] .rela.eh_frame    RELA             0000000000000000  00000340
       0000000000000030  0000000000000018   I      12    10     8
  [12] .symtab           SYMTAB           0000000000000000  00000120
       0000000000000198  0000000000000018          13    12     8
  [13] .strtab           STRTAB           0000000000000000  000002b8
       0000000000000051  0000000000000000           0     0     1
  [14] .shstrtab         STRTAB           0000000000000000  00000370
       0000000000000076  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

重定位表:
  重定位表主要记录了目标文件中所有需要重定位的符号所在的段以及相对(相对于该段开始)偏移位置。可以使用objdump -r main.o查看该表的内容,从内容中能够看到存储的时相关函数和变量的在目标文件中的相对位置。

Relocation section '.rela.text' at offset 0x310 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000033  000c00000002 R_X86_64_PC32     0000000000000000 _Z3addii - 4

Relocation section '.rela.data.rel.local' at offset 0x328 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000000  000500000001 R_X86_64_64       0000000000000000 .rodata + 0

Relocation section '.rela.eh_frame' at offset 0x340 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000020  000200000002 R_X86_64_PC32     0000000000000000 .text + 0
000000000040  000200000002 R_X86_64_PC32     0000000000000000 .text + 14

字符串表:
  字符串表中存储ELF文件中使用到的字符串,一般有三种字符串表分别为shstrtab保存section头中保存的字符串;strtab保存elf中使用到的字符串;dynstr保存了动态链接字符串表,表中存放了一系列字符串,这些字符串代表了符号名称,以空字符作为终止符。

4 链接中的符号

4.1 符号

  程序需要链接的原因时因为程序的每个文件特别是C类的语言时单独分模块编译的,每个编译单元仅仅知道当前编译单元中的信息,当引用到其他编译单元的函数或者变量时无法明确该变量或者函数的地址。因此需要在连接时将这些符号的地址明确,一般函数和变量统称为符号,函数名和变量名为符号名。
  编译时每个编译单元都会有一个符号表表明对应的符号在当前编译单元中的地址和值,因此在链接时需要将多个编译单元的符号表合并。
  使用readelf -s main.o查看符号表,能够看到符号表中包含符号的名称、索引、值、尺寸、作用域等信息。

Symbol table '.symtab' contains 17 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.cpp
     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    6
     7: 0000000000000004     1 OBJECT  LOCAL  DEFAULT    3 _ZZ4mainE8static_a
     8: 0000000000000004     1 OBJECT  LOCAL  DEFAULT    4 _ZZ4mainE8static_b
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    9
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT   10
    11: 0000000000000000     0 SECTION LOCAL  DEFAULT    8
    12: 0000000000000000    20 FUNC    GLOBAL DEFAULT    1 _Z3addii
    13: 0000000000000000     8 OBJECT  GLOBAL DEFAULT    6 file
    14: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 glob_a
    15: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 glob_b
    16: 0000000000000014    42 FUNC    GLOBAL DEFAULT    1 main

特殊符号:链接生成可执行文件时会连接器会定义很多特殊符号:

  • executable_start:程序起始地址;
  • etext,_etext,__etext:代码段的结束地址;
  • edata,_edata:数据段的结束地址;
  • end,_end:程序的结束地址。
#include <stdio.h>

extern char __executable_start[];
extern char etext[], _etext[], __etext[];
extern char edata[], _edata[];
extern char end[], _end[];

int main(){
    printf("executable start %X\\n", __executable_start);
    printf("text end %X %X %X\\n", etext, _etext, __etext);
    printf("data end %X %X\\n", edata, _edata);
    printf("executable end %X %X\\n", end, _end);

    return 0;
}

  运行结果:

executable start CB200000
text end CB20075D CB20075D CB20075D
data end CB401010 CB401010
executable end CB401018 CB401018

4.1 函数签名

  编译器为了更好的引用其他模块中的符号对模块中使用到的符号进行符号修饰,即符号签名。签名规则:

  • 所有的符号都以"_Z"开头,对于嵌套的名字(在名称空间或在类里面的),后面紧跟"N";
    -然后是各个名称空间和类的名字,每个名字前是名字字符串长度,再以"E"结尾。比如N::C::func经过名称修饰以后就是_ZN1N1C4funcE;
  • 对于一个函数来说,它的参数列表紧跟在"E"后面,对于int类型来说,就是字母"i"。所以整个N::C::func(int)函数签名经过修饰为_ZN1N1C4funcEi。

  符号签名中包含参数类型也是C++实现函数重载的基础,但是C++也常常需要使用C的接口,如果使用C++的符号签名则无法找到对应的接口。可利用C++中的extern "C"关键字保证对应的函数的符号签名使用C的规则。

4.2 弱符号和强符号

  C中存在强符号和弱符号,强符号不允许多重定义,弱符号允许多个定义但是实际运行时只有一个实体。对于C语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号(C++并没有将未初始化的全局符号视为弱符号)。

对于它们,下列三条规则使用:

  • 同名的强符号只能有一个,否则编译器报"重复定义"错误;
  • 允许一个强符号和多个弱符号,但定义会选择强符号的;
  • 当多个弱符号时,选择占用空间最大的;
  • 当有多个弱符号相同时,链接器选择最先出现那个,也就是与链接顺序有关。

  强引用和弱引用主要针对函数,强引用如果未找到定义则报错,二弱引用未找到定义则不报错。如果未定义,连接器会将弱引用设定为0或者特殊值,弱引用可以用于接口设计。

5 reference

以上是关于程序员自我修养阅读笔记——目标文件里有什么的主要内容,如果未能解决你的问题,请参考以下文章

《程序员的自我修养》读书笔记 -- 第三章

读书笔记第三周《程序员的自我修养》

《程序员自我修养》阅读笔记-静态链接

程序员自我修养阅读笔记——动态链接

程序员自我修养阅读笔记——动态链接

读书笔记|《程序员的自我修养》- 02 目标文件