03目标文件里有什么
东东 Lv4

本文主要介绍编译器生成的目标文件里有什么内容

1. 目标文件格式

目标文件就是源代码编译后但未进行链接的那些中间文件

目标文件与可执行文件、动态库链接库文件、静态链接库文件的内容结构很相似, 因此他们使用相同的文件格式, 例如

  • Windows 的 PE-COFF (Portbale Executable) 格式
  • Linux 的 ELF (Executable Linkable Format)

这两种文件格式都是 COFF (Common file format) 格式的变种.

目标文件与可执行文件格式的小历史

目标文件与可执行文件格式跟操作系统和编译器密切相关,所以不同的系统平台下会有不同的格式,但这些格式又大同小异,目标文件格式与可执行文件格式的历史几乎是操作系统的发展史。

COFF是由Unix System V Release 3首先提出并且使用的格式规范,后来微软公司基于COFF格式,制定了PE格式标准,并将其用于当时的Windows NT系统。System V Release 4 在COFF的基础上引入了ELF格式,目前流行的Linux系统也以ELF作为基本可执行文件格式。这也就是为什么目前PE和ELF如此相似的主要原因,因为它们都是源于同一种可执行文件格式COFF。

Unix最早的可执行文件格式为a.out格式,它的设计非常地简单,以至于后来共享库这个概念出现的时候,a.out格式就变得捉襟见肘了。于是人们重新设计了COFF格式来解决这些问题,这个设计非常通用,以至于COFF的继承者到目前还在被广泛地使用。

COFF的主要贡献是在目标文件里面引入了“段”的机制,不同的目标文件可以拥有不同数量及不同类型的“段”。另外,它还定义了调试数据格式。

对于系统使用的 ELF 格式可归为以下4种类型

ELF文件类型 说明 实例
可重定位文件 包含代码和数据, 可以被用来链接成可执行文件或共享目标文件, 静态链接库也可以归为这一类(静态链接库只是把很多目标文件捆绑在一起形成一个文件, 可理解为包含有很多目标文件的文件包) 如 Linux 的 .o 文件或 Windows 的 .obj 文件
可执行文件 包含了可直接执行的程序, 其代表就是ELF可执行文件, 他们一般没有扩展名 比如/bin/bash或Windows 的 .exe
共享目标文件 包含代码和数据, 可在以下两种情况下使用。一种是链接器可使用这种文件跟其他可重定位或共享目标文件链接, 产生新的目标文件。第二种是动态链接器可以将这几种共享目标文件与可执行文件结合, 作为进程映像的一部分来运行。 比如Linux的.so或Windows 的 .dll
核心转储文件 当进程意外终止时, 系统可将该进程的地址空间及终止时的信息转储到核心转储文件 比如Linux的 core dump

2. 目标文件是什么样子

目标文件除指令和数据外还包括链接时所必须的一些信息, 如符号表, 调试信息, 字符串等. 目标文件将这些信息按不同属性以 "节" (Section) 的形式存储 (有时也叫段 Segment).

通常

  • 指令被放在代码段 (Code Section), 其常见的段名有 .code.text
  • 初始化不为 0 的全局或静态变量放在数据段 (Data Section), 段名为 .data
  • 未初始化或初始化为0的全局或静态变量放在 .bss (Block Started by Symbol) 段

局部变量不需要在目标文件中声明段, 因为它们的本质其实是随程序执行过程中动态获取和释放的栈内存. 生命周期一般很短.

下面是一个例子 demo.c

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
#include <stdio.h>

int gvar_init = 84; // 全局已初始化变量 --> .data 段
int gvar_init_0 = 0; // 全局初始化为0变量 --> .bss 段
int gvar_uninit; // 全局未初始化变量 --> .bss 段

// 代码段 --> .text 段
void func(int i)
{
printf("%d\n", i);
}

int main(int argc, char const *argv[])
{
static int svar_init = 12; // 静态已初始化变量 --> .data 段
static int svar_init_0 = 0; // 静态初始化为0变量 --> .bss 段
static int svar_uninit; // 静态未初始化变量 --> .bss 段

// 放在栈中
int a = 1;

// 代码段 --> .text 段
func(gvar_init + svar_init + a);
return 0;
}

对于未初始化的全局或静态变量, 因为程序加载器在将程序加载到内存时会将它们初始化为 0, 因此那些初始化为 0 的全局或静态变量本质上和未初始化的是一样的, 因此它们都被分到 bss 段中.

另外 bss 段其实只是用来给它们预留个位置而已, 它并没有内容, 因此它在文件中也不占据空间.

BSS历史

BSS (Block Started by Symbol) 这个词最初是UA-SAP汇编器(United Aircraft Symbolic Assembly Program)中的一个伪指令,用于为符号预留一块内存空间。该汇编器由美国联合航空公司于20世纪50年代中期为BM704大型机所开发。

后来BSS这个词被作为关键字引入到了IBM709和7090/94机型上的标准汇编器FAP(Fortran Assembly Program),用于定义符号并且为该符号预留给定数量的未初始化空间。

数据和指令分开带来的好处

  • 可设置两个区域的读写权限, 从而防止指令有意或无意地改写代码区
  • CPU 的 Cache 具有空间局部性, 分开的代码和数据有利于 Cache 命中率
  • 当系统运行该程序的多个副本时, 可以直接复用只读的代码区, 节省空间

3. 深入ELF文件格式

对于前一节 demo.c 我们进行编译

1
gcc -c demo.c -o demo.o

得到一个 882 字节的目标文件

objdump 命令

objdump 工具可查看目标文件的内部结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
objdump -h demo.o

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000070 0000000000000000 0000000000000000 0000012c 2**4
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000010 0000000000000000 0000000000000000 0000019c 2**4
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000010 0000000000000000 0000000000000000 00000000 2**4
ALLOC
3 .rdata 00000010 0000000000000000 0000000000000000 000001ac 2**4
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .xdata 00000018 0000000000000000 0000000000000000 000001bc 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .pdata 00000018 0000000000000000 0000000000000000 000001d4 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
6 .rdata$zzz 00000030 0000000000000000 0000000000000000 000001ec 2**4
CONTENTS, ALLOC, LOAD, READONLY, DATA

-h 作用是 Display the contents of the section headers, 打印各个段的基本信息

源文件来自于

 评论