首页 目标文件结构
文章
取消

目标文件结构

格式

目标文件就是源代码编译(与汇编)后但未进行链接的那些中间文件(Windows 的.obj 和 Linux 下的.o),它跟可执行文件的内容与结构很相似,只是其中可能有些符号或有些地址还没有被调整,所以一般跟可执行文件格式采用一种格式存储。

可执行文件格式 (Executable) 主要是 Windows 下的 PE ( Portable Executable ) 和 Linux 的 ELF (Executable Linkable Format )。它们都是 COFF (Common file format ) 格式的变种。

动态链接库 (DLL,Dynamic Linking Library ) ( Windows 的.dll 和 Linux 的.so) 及静态链接库(Static Linking Library)(Windows 的.lib 和 Linux 的.a ) 文件都按照可执行文件格式存储。

我们可以在 Linux 下使用 file 命令来查看相应的文件格式:

1
2
3
4
5
6
qmmms@qmmms-virtual-machine:~/shared$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
qmmms@qmmms-virtual-machine:~/shared$ file /bin/bash
/bin/bash: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=33a5554034feb2af38e8c75872058883b2988bc5, for GNU/Linux 3.2.0, stripped
qmmms@qmmms-virtual-machine:~/shared$ file /lib/klibc-K8e6DOmVI9JpyGMLR7qNe5iZeBk.so
/lib/klibc-K8e6DOmVI9JpyGMLR7qNe5iZeBk.so: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=90b2b0762fa22373420b0bcade8de5a36fa81172, stripped

目标文件将这些信息按不同的属性,以“段”(Segment) 的形式存储。

  • ELF 文件的开头是一个“文件头”,描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息。
  • 文件头还包括一个段表(Section Table),段表其实是一个描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置及段的属性等,从段表里面可以得到每个段的所有信息。文件头后面就是各个段的内容,比如代码段,数据段。
  • 程序源代码编译后的机器指令经常被放在代码段(Code Section ) 里,代码段常见的名 字有 .code 或 ` .text`
  • 已经初始化的全局变量和局部静态变量经常放在数据段 (Data Section), 数据段的名字叫 .data
  • 未初始化的全局变景和局部静态变量一般放在一个叫.bss 的段里,.bss 段只是为未初始化的全局变量和局部静态变量预留位置而己,它并没有内容,所以它在文件中也不占据空间。

示例

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int printf (const char* format, ... );

int global_init_var = 84;
int global_uninit_var;

void func1(int i){
    printf("%d\n", i);
}

int main(void){
    static int static_var = 85;
    static int static_var2;

    int a = 1;
    int b;

    func1(static_var + static_var2 + a + b);

    return a;
}

我们使用 GCC 来编译这个文件(参数 -c 表示只编译不链接):

1
gcc -c SimpleSection.c

我们可以使用 binutils 的工具 objdump 来查看 object 内部的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
qmmms@qmmms-virtual-machine:~/shared$ objdump -h SimpleSection.o

SimpLeSection.o:     文件格式 elf64-x86-64

节:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000062  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  000000a4  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000008  0000000000000000  0000000000000000  000000ac  2**2
                  ALLOC
  3 .rodata       00000004  0000000000000000  0000000000000000  000000ac  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      0000002e  0000000000000000  0000000000000000  000000b0  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000de  2**0
                  CONTENTS, READONLY
  6 .note.gnu.property 00000020  0000000000000000  0000000000000000  000000e0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .eh_frame     00000058  0000000000000000  0000000000000000  00000100  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

除了最基本的代码段、数据段和BSS 段以外,还有几个段,包括:

  • 只读数据段(.rodata )
  • 注释信息段(.comment )
  • 堆栈提 示段(note.GNU-stack)

几个重要的段的属性:

  • 是段的长度 (Size )
  • 段所在的位W (File Offset )

每个段的 第 2 行中的 “CONTENTS ”、“ ALLOC” 等表示段的各种域性,“CONTENTS” 表示该段在文件中存在。

有一个专门的命令叫做 size, 它可以用来査看 ELF 文件的代码段、数据段和 BSS 段的长度( dec 表示 3 个段长度的和的十进制,hex 表示长度和的十六进制 ):

1
2
3
qmmms@qmmms-virtual-machine:~/shared$ size SimpleSection.o
   text	   data	    bss	    dec	    hex	filename
    222	      8	      8	    238	     ee	SimpleSection.o

objdump 的 -s 参数可以将所有段的内容以十六进制的方式打印出来,-d 参数可以将所有包含指令的段反汇编。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
qmmms@qmmms-virtual-machine:~/shared$ objdump -d -s SimpleSection.o

SimpleSection.o:     文件格式 elf64-x86-64

Contents of section .text:
 0000 f30f1efa 554889e5 4883ec10 897dfc8b  ....UH..H....}..
 0010 45fc89c6 488d0500 00000048 89c7b800  E...H......H....
 0020 000000e8 00000000 90c9c3f3 0f1efa55  ...............U
 0030 4889e548 83ec10c7 45f80100 00008b15  H..H....E.......
 0040 00000000 8b050000 000001c2 8b45f801  .............E..
 0050 c28b45fc 01d089c7 e8000000 008b45f8  ..E...........E.
 0060 c9c3                                 ..              
Contents of section .data:
 0000 54000000 55000000                    T...U...        
Contents of section .rodata:
 0000 25640a00                             %d..            
Contents of section .comment:
 0000 00474343 3a202855 62756e74 75203131  .GCC: (Ubuntu 11
 0010 2e332e30 2d317562 756e7475 317e3232  .3.0-1ubuntu1~22
 0020 2e30342e 31292031 312e332e 3000      .04.1) 11.3.0.  
Contents of section .note.gnu.property:
 0000 04000000 10000000 05000000 474e5500  ............GNU.
 0010 020000c0 04000000 03000000 00000000  ................
Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 01781001  .........zR..x..
 0010 1b0c0708 90010000 1c000000 1c000000  ................
 0020 00000000 2b000000 00450e10 8602430d  ....+....E....C.
 0030 06620c07 08000000 1c000000 3c000000  .b..........<...
 0040 00000000 37000000 00450e10 8602430d  ....7....E....C.
 0050 066e0c07 08000000                    .n......        

Disassembly of section .text:

0000000000000000 <func1>:
   0:	f3 0f 1e fa          	endbr64 
   4:	55                   	push   %rbp
   5:	48 89 e5             	mov    %rsp,%rbp
   8:	48 83 ec 10          	sub    $0x10,%rsp
   c:	89 7d fc             	mov    %edi,-0x4(%rbp)
   f:	8b 45 fc             	mov    -0x4(%rbp),%eax
  12:	89 c6                	mov    %eax,%esi
  14:	48 8d 05 00 00 00 00 	lea    0x0(%rip),%rax        # 1b <func1+0x1b>
  1b:	48 89 c7             	mov    %rax,%rdi
  1e:	b8 00 00 00 00       	mov    $0x0,%eax
  23:	e8 00 00 00 00       	call   28 <func1+0x28>
  28:	90                   	nop
  29:	c9                   	leave  
  2a:	c3                   	ret    

000000000000002b <main>:
  2b:	f3 0f 1e fa          	endbr64 
  2f:	55                   	push   %rbp
  30:	48 89 e5             	mov    %rsp,%rbp
  33:	48 83 ec 10          	sub    $0x10,%rsp
  37:	c7 45 f8 01 00 00 00 	movl   $0x1,-0x8(%rbp)
  3e:	8b 15 00 00 00 00    	mov    0x0(%rip),%edx        # 44 <main+0x19>
  44:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax        # 4a <main+0x1f>
  4a:	01 c2                	add    %eax,%edx
  4c:	8b 45 f8             	mov    -0x8(%rbp),%eax
  4f:	01 c2                	add    %eax,%edx
  51:	8b 45 fc             	mov    -0x4(%rbp),%eax
  54:	01 d0                	add    %edx,%eax
  56:	89 c7                	mov    %eax,%edi
  58:	e8 00 00 00 00       	call   5d <main+0x32>
  5d:	8b 45 f8             	mov    -0x8(%rbp),%eax
  60:	c9                   	leave  
  61:	c3                   	ret    

文件头

我们可以用 readelf 命令来详细査看 ELF 文件头:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
qmmms@qmmms-virtual-machine:~/shared$ readelf -h SimpleSection.o
ELF 头:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              REL (可重定位文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0x0
  程序头起点:          0 (bytes into file)
  Start of section headers:          1040 (bytes into file)
  标志:             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:         14
  Section header string table index: 13

ELF 的文件头中定义了 ELF 魔数、文件机器字节长度、 数据存储方式、版本、 运行平台、ABI 版本、ELF 重定位类型、 硬件平台、硬件平台版本、 入口地址、程序头入口和长度、段表的位置和长度及段的数量等。

段表

显示段表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
qmmms@qmmms-virtual-machine:~/shared$ readelf -S SimpleSection.o
There are 14 section headers, starting at offset 0x410:

节头:
  [号] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000062  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  000002f0
       0000000000000078  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  000000a4
       0000000000000008  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  000000ac
       0000000000000008  0000000000000000  WA       0     0     4
  [ 5] .rodata           PROGBITS         0000000000000000  000000ac
       0000000000000004  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  000000b0
       000000000000002e  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000de
       0000000000000000  0000000000000000           0     0     1
  [ 8] .note.gnu.pr[...] NOTE             0000000000000000  000000e0
       0000000000000020  0000000000000000   A       0     0     8
  [ 9] .eh_frame         PROGBITS         0000000000000000  00000100
       0000000000000058  0000000000000000   A       0     0     8
  [10] .rela.eh_frame    RELA             0000000000000000  00000368
       0000000000000030  0000000000000018   I      11     9     8
  [11] .symtab           SYMTAB           0000000000000000  00000158
       0000000000000138  0000000000000018          12     8     8
  [12] .strtab           STRTAB           0000000000000000  00000290
       0000000000000060  0000000000000000           0     0     1
  [13] .shstrtab         STRTAB           0000000000000000  00000398
       0000000000000074  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),
  D (mbind), l (large), p (processor specific)

段表是 ELF 文件中除了文件头以外最重要的结构,它描述了 ELF 的各个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。

链接器和装载器都是依靠段表来定位和访问各个段的属性的。

符号

在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量地址的引用。

整个链接过程正是基于符号才能够正确完成。 每一个目标文件都会有一个相应的符号表 ( Symbol Table ),这个表里记录了目标文件中所用到的所有符号。

每个定义的符号有一个对应的符号值(Symbol Value )。 对子变量和函数来说,符号值就是它们的地址。 除了函数和变量之外,还存在其他几种常用到的符号,我们将符号表中所有的符号进行分类,它们有可能是下面这些类型中的一种:

  • 定义在本目标文件的全局符号,可以被其他目标文件引用。
  • 在本目标文件中引用的全局符号,却没有定义在本目标文件,这叫做外部符号 (External Symbol )或符号引用。比如printf
  • 段名
  • 局部符号
  • 行号信息

查看SimpleSection.o 的符号结果如下:

1
2
3
4
5
6
7
8
qmmms@qmmms-virtual-machine:~/shared$ nm SimpleSection.o
0000000000000000 T func1
0000000000000000 D global_init_var
0000000000000000 B global_uninit_var
000000000000002b T main
                 U printf
0000000000000004 d static_var.1
0000000000000004 b static_var2.0

符号表

ELF 文件中的符号表往往是文件屮的一个段,段名一般叫 .symtab。以 SiinpleSection.o 里面的符号为例子,分析各个符号在符号表中的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
qmmms@qmmms-virtual-machine:~/shared$ readelf -s SimpleSection.o

Symbol table '.symtab' contains 13 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 .text
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 .data
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 .bss
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .rodata
     6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 static_var.1
     7: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 static_var2.0
     8: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 global_uninit_var
    10: 0000000000000000    43 FUNC    GLOBAL DEFAULT    1 func1
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    12: 000000000000002b    55 FUNC    GLOBAL DEFAULT    1 main

符号修饰

考虑一个问题,如果一个 C 语言的目标文件耍用到一个使用 Fortran 语言编写的目标文件,我们必须防止它们的名称冲突。

为了防止类似的符号名冲突,UNIX 下的 C 语言就规定,C 语言源代码文件中的所有全局的变量和函数经过编译以后,相对应的符号名前加上下划线“ _”。而 Fortran 语言的源代码经过编译以后,所有的符号名前加上后面也加上“_”。比如一个 C 语言函数 “ foo”,那么它编译后的符号名就是 “ _foo”;如果是 Fortran 语言,就是“_foo_”。

当程序很大时,不同的模块由多个部门(个人)开发,它们之间的命名规范如果不 严格,则有可能导致冲突。于是像 C++这样的后来设计的语言开始考虑到了这个问题,增加 了名称空间(Namespace) 的方法来解决多模块的符号冲突问题。

为了支持 C++的复杂特性,人们发明了符号修饰(Name Decoration ) 或符号改编(Name Mangling ) 的机制。我们引入一个术语叫做函数签名 ( Function Signature ),函数签名包含了一个函数的信息,包括函数名、它的参数类型、它所在的类和名称空间及其他信息。函数签名用于识别不 冋的函数,就像签名用于识别不同的人一样,函数的名字只是函数签名的一部分。

在编译器及链接器处理符号时,它们使用某种名称修饰的方法,使得毎个函数签名对应一个 修饰后名称(Decorated Name)。编译器在将 C++源代码编泽成目标文件时,会将函数和 变量的名字进行修饰,形成符号名,也就是说,C++的源代码编译后的目标文件中所使用的 符号名是相应的函数和变量的修饰后名称,C++编译器和链接器都使用符号来识别和处理函 数和变景,所以对于不同函数签名的函数,即使函数名相同,编译器和链接器都认为它们是 不同的函数。

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

强符号和弱符号

对于 C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。比如我们有下面这段程序:

1
2
3
4
5
6
7
extern int ext;
int weak;
int strong = 1;

int main(){
	return 0;
}

上面这段程序中,“ weak ” 是弱符号,“strong” 和 “main” 是强符号,而 “ext” 既非强符号也非弱符号,因为它是一个外部变量的引用。针对强弱符号的概念,链接器就会按如下规则处理与选择被多次定义的全局符号:

  1. 不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号),如果有多个强符号定义,则链接器报符号重复定义错误。
  2. 如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。
  3. 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。比如目标文件 A 定义全局变量 global 为 int 型,占 4 个字节:目标文件 B 定义 global 为 double 型,占 8 个字节,那么目标文件 A 和 B 链接后,符号 global 占 8 个字节(尽 量不要使用多个不同类型的弱符号,否则容易导致很难发现的程序错误)

对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们须要被正确决议.如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用(Strong Reference)。

与之相对应还有一种弱引用(Weak Reference ), 在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,则链接器对于该引用不报错。链接器 默认其为 0, 或者是一个特殊的值,以便于程序代码能够识别。

这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数;或者程序可以对某些扩展功能模块的引用定义为弱引用,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用; 如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能,这使得 程序的功能更加容易裁剪和组合。

本文由作者按照 CC BY 4.0 进行授权

帆软Q&A

GCC、CMake、CMakelist、Make、Makefile、Ninja啥关系?一图讲透!