在Linux操作系统中,一段C程序从被写下到最终被CPU执行,要经过一段漫长而又复杂的过程。下图展示了这个过程

目录

  1. 编译
  2. 目标文件的格式
  3. 链接
  4. 装载
  5. 运行

1. 编译

编译就是把程序员所写的高级语言代码转化为对应的目标文件的过程。一般来说高级语言的编译要经过预处理、编译和汇编这几个过程。

预处理

预编译过程对源代码做了如下的操作

  • 删除所有的注释信息
  • 删除所有的 #define 并展开所有宏定义
  • 插入所有 #include 文件注1的内容到源文件中的对应位置,include过程是递归执行的

gcc可以使用如下命令对C语言进行预编译并且把预编译的结果输出到hello.i文件中

gcc -E hello.c -o hello.i

编译

编译就是对预处理之后的文件进行词法分析、语法分析、语义分析并优化后生成相应的汇编文件。我们使用如下命令来编译预处理之后的文件

gcc -S hello.i -o hello.s

或者我们也可以把预处理和编译合为一步

gcc -S hello.c -o hello.s

汇编

汇编的目的是把汇编代码转化为机器指令,因为几乎每一条汇编指令都对应着一条机器指令,所以汇编的过程相对而言非常的简单。我们可以使用如下命令实现汇编

gcc -c hello.s -o hello.o

或者我们也可以直接把源代码文件编译为目标文件

gcc -c hello.c -o hello.o

汇编操作所生成的文件叫做目标文件(Object File),目标文件的结构与可执行文件是一致的,它们之间只存在着一些细微的差异。目标文件是无法被执行的,它还需要经过链接这一步操作,目标文件被链接之后才可以产生可执行文件。

下面我们了解一下目标文件的格式以及链接这一步具体做了哪些工作。

2. 目标文件的格式

Linux下的目标文件格式叫做ELF(Executable Linkable Format),ELF的格式如下图所示:

ELF header是ELF文件中最重要的一部分,header中保存了如下的内容

  • ELF的magic number
  • 文件机器字节长度
  • ELF版本
  • 操作系统平台
  • 硬件平台
  • 程序的入口地址
  • 段表的位置和长度
  • 段的数量

从header中我们可以得到很多有用的信息,其中的一个尤其重要,那就是段表的位置和长度,通过这一信息我们可以从ELF文件中获取到段表(Section Hedaer Table),在ELF文件中段表的重要性仅次于header。

段表保存了ELF文件中所有的段的基本属性,包括每个段的段名、段在ELF文件中的偏移、段的长度以及段的读写权限等等,段表决定了整个ELF文件的结构。

既然段表决定了所有的段的属性,那么ELF文件中的段究竟是个什么东西呢?其实段只是对ELF文件内的不同类型的数据的一种分类。例如,我们把所有的代码(指令)放到一个段中,并且给这个段起名.text;把所有的已经初始化的数据放在.data段;把所有的未初始化的数据放在.bss段;把所有的只读数据放在.rodata段,等等。

至于为什么要把数据(指令在ELF文件中也算是一种数据,它是ELF文件的数据之一)分为不同的类型,除了方便进行区分之外,还有以下几个原因

  • 便于给段设置读写权限,有些段只需要设置只读权限即可
  • 方便CPU缓存的生效
  • 有利于节省内存,例如程序有多个副本情况下,此时只需要一份代码段即可

既然分段有着诸多的好处,那么接下来我们就近距离的看一看ELF文件中的段信息。有如下的示例文件 hello.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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_var + a + b);

return a;
}

使用如下命令把源代码编译成目标文件

gcc -c hello.c -o hello.o

接下来我们可以使用objdump命令查看ELF文件的内部结构,-h 表示显示ELF文件的头部信息

objdump -h hello.o

得到结果如下