目标文件里有什么?

122 阅读14分钟

目标文件的格式

可执行文件格式。常用的有 PE 和 ELF。都继承自COFF格式,COFF的主要贡献就是在目标文件中引入了“段”的机制。另外,它还定义了调试数据格式。

不光是可执行文件按照可执行文件存储,动态链接库及静态链接库文件都按照可执行文件格式存储。静态链接库稍有不同,它是把很多目标文件捆绑在一起形成一个文件,再加上一些索引,你可以简单地把它理解为一个包含有很多目标文件的文件包。

ELF文件类型说明实例
可重定位文件包含代码和数据,可以被用来链接成可执行文件或共享目标文件Linux的.o
可执行文件包含可以直接执行的程序,它的代表就是ELF可执行文件/bin/bash文件、Windows的.exe
共享目标文件包含代码和数据。第一种可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分。Windows的DLL
核心转储文件当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件Linux下的core dump

目标文件是什么样的

包含机器指令代码、数据。还有链接时需要的一些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按照不同的属性,以段的形式存储。

  • 代码段(.text或.code):源代码编译后的机器指令
  • 数据段(.data):全局变量或静态变量
  • .bss: 未初始化的全局变量和局部静态变量,或者初始化为0的。更准备的说法应该是,为它们预留了空间而已。

总体来说,就是分成程序指令和程序数据。代码段属于程序指令,而数据段和.bss段属于程序数据。

ELF 文件头(File Header)

描述了整个文件的文件属性,包括文件是否可执行,是静态链接还是动态链接及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息,文件头还包括一个段表,段表其实是一个描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置及段的属性等,从段表里面可以得到每个段的所有信息。文件头后面就是各个段的内容。

为什么要把数据和指令分开?

  • 程序被加载后,数据和指令被映射到两个虚存区域。由于数据是可读写的,而指令是可读的,所以两个虚存区域的权限可以被分别设置成可读写和只读。这样可以防止程序的指令被有意或无意的改写。
  • 提高缓存命中率,指令区和数据区的分开有利于提高程序的局部性。
  • 最重要的是,当系统中运行着多个该程序的副本时,他们的指令是一样的,所以内存中只需要保存一份该程序的指令部分。特别是在有动态链接的系统中,可以节省大量的内存。(内存共享将在后面在深入讨论)

挖掘 SimpleSection.o

真正了不起的程序员对自己的程序的每一个字节都了如指掌。

除了上述说的代码段、数据段、.bss,还有

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

段的几个重要的属性

gcc -c SimpleSection.c
objdump -h SimpleSection.o

SimpleSection.o:	file format mach-o 64-bit x86-64

Sections:
Idx Name             Size     VMA              Type
  0 __text           00000066 0000000000000000 TEXT
  1 __data           00000008 0000000000000068 DATA
  2 __cstring        00000004 0000000000000070 DATA
  3 __bss            00000004 0000000000000120 BSS
  4 __compact_unwind 00000040 0000000000000078 DATA
  5 __eh_frame       00000068 00000000000000b8 DATA
size SimpleSection.o

__TEXT	__DATA	__OBJC	others	dec	hex
210	12	0	64	286	11e
  • 名称(Name)
  • 长度(Size)
  • 所在的位置(File Offset)
  • 读写权限
  • 属性,如CONTENTS、ALLOC

我们可以发现 .bss 段没有 CONTENTS,表示它实际在 ELF 文件中不存在内容。

代码段

objdump -s -d SimpleSection.o

SimpleSection.o:	file format mach-o 64-bit x86-64

Contents of section __TEXT,__text:
 0000 554889e5 4883ec10 897dfc8b 75fc488d  UH..H....}..u.H.
 0010 3d5b0000 00b000e8 00000000 4883c410  =[..........H...
 0020 5dc3662e 0f1f8400 00000000 0f1f4000  ].f...........@.
 0030 554889e5 4883ec10 c745fc00 000000c7  UH..H....E......
 0040 45f80100 00008b3d 00000000 033d0000  E......=.....=..
 0050 0000037d f8037df4 e8000000 008b45f8  ...}..}.......E.
 0060 4883c410 5dc3                        H...].
Contents of section __DATA,__data:
 0068 54000000 55000000                    T...U...
Contents of section __TEXT,__cstring:
 0070 25640a00                             %d..
Contents of section __DATA,__bss:
<skipping contents of bss section at [0120, 0124)>
Contents of section __LD,__compact_unwind:
 0078 00000000 00000000 22000000 00000001  ........".......
 0088 00000000 00000000 00000000 00000000  ................
 0098 30000000 00000000 36000000 00000001  0.......6.......
 00a8 00000000 00000000 00000000 00000000  ................
Contents of section __TEXT,__eh_frame:
 00b8 14000000 00000000 017a5200 01781001  .........zR..x..
 00c8 100c0708 90010000 24000000 1c000000  ........$.......
 00d8 28ffffff ffffffff 22000000 00000000  (.......".......
 00e8 00410e10 8602430d 06000000 00000000  .A....C.........
 00f8 24000000 44000000 30ffffff ffffffff  $...D...0.......
 0108 36000000 00000000 00410e10 8602430d  6........A....C.
 0118 06000000 00000000                    ........

Disassembly of section __TEXT,__text:

0000000000000000 <_func1>:
       0: 55                           	pushq	%rbp
       1: 48 89 e5                     	movq	%rsp, %rbp
       4: 48 83 ec 10                  	subq	$16, %rsp
       8: 89 7d fc                     	movl	%edi, -4(%rbp)
       b: 8b 75 fc                     	movl	-4(%rbp), %esi
       e: 48 8d 3d 5b 00 00 00         	leaq	91(%rip), %rdi  # 70 <_main+0x40>
      15: b0 00                        	movb	$0, %al
      17: e8 00 00 00 00               	callq	0x1c <_func1+0x1c>
      1c: 48 83 c4 10                  	addq	$16, %rsp
      20: 5d                           	popq	%rbp
      21: c3                           	retq
      22: 66 2e 0f 1f 84 00 00 00 00 00	nopw	%cs:(%rax,%rax)
      2c: 0f 1f 40 00                  	nopl	(%rax)

0000000000000030 <_main>:
      30: 55                           	pushq	%rbp
      31: 48 89 e5                     	movq	%rsp, %rbp
      34: 48 83 ec 10                  	subq	$16, %rsp
      38: c7 45 fc 00 00 00 00         	movl	$0, -4(%rbp)
      3f: c7 45 f8 01 00 00 00         	movl	$1, -8(%rbp)
      46: 8b 3d 00 00 00 00            	movl	(%rip), %edi  # 4c <_main+0x1c>
      4c: 03 3d 00 00 00 00            	addl	(%rip), %edi  # 52 <_main+0x22>
      52: 03 7d f8                     	addl	-8(%rbp), %edi
      55: 03 7d f4                     	addl	-12(%rbp), %edi
      58: e8 00 00 00 00               	callq	0x5d <_main+0x2d>
      5d: 8b 45 f8                     	movl	-8(%rbp), %eax
      60: 48 83 c4 10                  	addq	$16, %rsp
      64: 5d                           	popq	%rbp
      65: c3                           	retq

数据段和只读数据段

这个存储还会有一个字节序的问题。

BSS 段

简单上讲我们把全部未初始化的全局变量和局部静态变量。

有些编译器会将全部的未初始化变量存放在目标文件.bss段,有些则不存放,只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在.bss段分配空间。(将在强弱符号和COMMON块深入分析)

其他段

Q: 如果我们要将一个二进制文件,比如图片、MP3音乐一类东西作为目标文件中的一个段,该怎么做?
A: 可以使用 objcopy 工具。

自定义段

有时候你可能希望变量或某些代码能够放到你所指定的段中去,以实现某些特定的功能。

__attribute__((section("FOO"))) int global = 42;

ELF 文件结构描述

ELF文件头

描述了整个文件的基础属性,比如 ELF 文件版本、目标机器型号、程序入口地址等。

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

这种魔数用来确认文件的类型,操作系统在加载可执行文件的时候会确认魔数是否正确,如果不正确会拒绝加载。

各种魔数的由来:马屁股决定航天飞机。

段表

每个段的段名、段的长度、在文件中的偏移、读写权限及其他属性。也就是说ELF文件的结构就是由段表决定的,编译器、链接器和装载器都是依靠段表来定位和访问各个段的属性。

重定位表

链接器在处理目标文件时,需要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。这些重定位的信息都记录在ELF文件的重定位表里面。

字符串表

ELF 文件用到了很多字符串,比如段名、变量名。由于这些字符串的长度往往是不定的,所以用固定的结构表示他们比较困难。一种常见的做法就是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。

通过这种方法,在ELF文件中引用字符串只需要给出一个数字下标即可,不用考虑字符串长度问题。

字符串表:.strtab 段表字符串表:.shstrtab

链接的接口--符号

在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。我们将函数和亦是统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)

链接中很重要的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(Symbol Table),这个表里面记录了目标文件中所用到的所有符号。这个表里面记录了目标文件中所用到的所有符号。每个定义的符号都有一个对应的值,叫做符号值(Symbol Value),对于变量和函数来说,符号值就是它们的地址。

类型

  • 定义在本目标文件的全局符号,可以被其他目标文件引用。
  • 在本目标文件引用的全局符号,却没有定义在本目标文件,一般叫做外部符号,也就是我们讲的符号引用。
  • 段名,这种符号往往由编译器产生,它的值就是该段的起始地址。
  • 行号信息,即目标文件指令与源代码中代码行的对应关系。
  • 局部符号,这类符号只在编译单元内部可见。调试器可以使用这些符号来分析程序或者崩溃时的核心转储文件。在链接时没有作用,链接器往往也忽略它们。
nm SimpleSection.o
0000000000000000 T _func1
0000000000000068 D _global_init_var
0000000000000004 C _global_uninit_var
0000000000000030 T _main
000000000000006c d _main.static_var
0000000000000120 b _main.static_var2
                 U _printf

符号表的结构

一个结构体

特殊符号

  • __executable_start,该符号为程序起始地址,注意,不是入口地址,是程序最开始的地址。
  • __etext或_etext或etext,该符号为代码段结束地址,即代码段最末尾的地址。
  • _edata或edata,该符号为数据段结束地址,即数据段最末尾的地址
  • _end或end,该符号为程序结束的地址
  • 以上地址都为程序被装载时的虚拟地址。

符号修饰和函数签名

函数签名: 包含了函数名、参数类型、返回值类型、所在的在和名称空间。可以用于识别不同的函数。

extern "C"

C++ 编译器会将 extern "C" 的大括号内部的代码当作 C 语言代码处理。所以很明显 C++ 的符号修饰机制不会起作用。

很多时候我们会碰到有些头文件声明一些C语言函数和全局变量,但是这个头文件可能会被C语言代码或C++代码包含。

#ifdef __cplusplus
extern "C" {
#endif
void *memset(void *, int, size_t);
#ifdef __cplusplus
}
#endif

上述这个技巧几乎在所有的系统头文件里面都被用到。

弱符号和强符号

多个目标文件中含有相同名字全局符号定义,那么这些目标文件链接的时候将出现符号重复定义的错误。

比如我们在目标文件A和目标文件B都定义了一个全局整形亦是global,并将它们都初始化,那么链接器将A和B进行链接的时候就会报错。

这种符号定义被称为强符号

有些符号的定义可以被称为弱符号。对于C/C++语言来说,编译器黑夜函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。也可以能过__attribute__((weak))来指定一个强符号为弱符号。

规则

  • 不允许强符号被多次定义。
  • 如果一个符号在目标文件中是强符号,在其他文件中是弱符号,那么选择强符号。
  • 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。

强引用和弱引用

  • 强引用:在被链接成可执行文件的时候需要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误
  • 弱引用:在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果未定义也不会报错。
__attribute__ ((weakref)) void foo();
int main() {
  if (foo) foo();
}
作用
  • 库中定义的弱符号可以被用户定义的强符号覆盖,从而使用程序可以使用自定义版本的库函数;
  • 程序可以对某些扩展功能模块的引用定义为弱引用,当我们将扩展模块链接在一起时,模块可以正常使用;如果去掉某些功能模块,那么程序也可以正常链接,只是缺少相应功能。

调试信息

我们可以设置断点、监听变量变化,可以单步行进,前提是编译器必须提前将源代码与目标代码之间的关系等,比如目标代码中的地址对应源代码中的哪一行,函数和变量的类型、结构体的定义、字符串保存到目标文件里。

gcc -g SimpleSection.c

可以看到有很多debug相关的段,这些段中保存的就是调试信息。现在的ELF文件中采用一个叫DWARF的标准的调试信息格式。

调试信息在目标文件和可执行文件中占用很大的空间,往往比程序的代码和数据本身大好几倍。当我们发布程序的时候,需要把这些对于用户没有用的调试信息去掉,以节省大量的空间。

strip foo