我们从一个简单的hello程序来了解,通过跟踪hello程序的生命周期——从它被程序员创建开始,到系统上运行,输出简单的消息,然后终止。沿着这个生命周期,简要的介绍编译过程,了解这个过程中干了什么。

1
2
3
4
5
6
#include <stdio.h> 
int main()
{
printf("hello, world\n");
return 0;
}

我们通过编辑器创建并编写了该文件,并取名为hello.c。而这个文件,归根到底是由一串比特来表示的。

但仅仅如此远远不够,因为这个文件只能被我们读懂,机器是无法读懂的。为了运行该程序,每条C语句都必须被其他程序转化为一系列的低级机器语言指令。这些指令按照可执行目标程序的格式打包好,并以二进制磁盘文件的形式存放起来。目标程序称为可执行目标文件。

如何转化?在linux上是由编译器驱动程序完成的,我们可以执行如下指令:

1
linux> gcc -o hello hello.c

gcc就是该编译器驱动程序,它读取源程序文件hello.c,并把它翻译成一个可执行目标文件hello。这个具体过程可分为四个阶段完成,如下:

img

在linux上可以分开进行这四个过程

1
2
3
4
gcc -E hello.c -o hello.i         // 预处理 
gcc -S hello.i -o hello.s // 编译
gcc -c hello.s -o hello.o // 汇编
gcc hello.o -o hello // 链接

预处理阶段

预处理器(cpp)以字符“#”开头的命令,修改原始的C程序。

比如#include 命令告诉预处理器读取系统头文件stdio.h的内容,并直接插入程序文本当中,就得到了hello.i这个文件,如下:

1
2
3
4
5
6
7
# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "hello.c"
......

编译阶段

编译器ccl将文本文件hello.i翻译成文本文件hello.s,它包含了一个汇编语言程序,该程序包含了函数main的定义,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $.LC0, %edi
call puts
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc

汇编阶段

接下来,汇编器as将hello.s翻译成机器语言指令,这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在文件hello.o中,该文件是一个二进制文件,我们无法直接打开,可以用十六进制编辑器打开。

链接阶段

hello程序调用了printf函数,这个函数是标准C库里的一个函数,printf函数存在一个printf.o的单独的预编译好了的目标文件中。这个文件要合并到hello.o程序中,是通过链接器(ld)负责处理这种合并。结果就得到了hello文件,它是一个可执行目标文件(可执行文件),可以加载到内存中,由系统执行。

参考

本篇主要参考《深入理解计算机系统》