Linux下gcc的编译流程

基本概念

在linux系统上,从源文件到目标文件的转化是由编译器完成的。以hello.c程序的编译为例,如下:

1
tao@tao-R408P~$ gcc -o hello hello.c

在这里,gcc编译器读取源文件hello.c,并把它翻译成一个可执行文件 hello。
这个翻译过程可分为四个阶段逐步完成 :预处理编译汇编链接,如下图所示。

详细分析

在未编译前,hello.c 的源代码如下:

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

第一步:预处理阶段

执行命令: gcc -o hello.i -E hello.c
或者执行: cpp -o hello.i hello.c (这里cpp不是值c plus plus,而是预处理器the C Preprocessor)


预处理器cpp根据以字符开头#开头的命令,修改原始C程序:比如hello.c中的第一行为 #include,预处理器便将stdio.h的内容直接插入到程序中。预处理之后得到文本文件hello.i,打开如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1 "hello.c"
# 1 ""
# 1 "<命令行>"
# 1 "hello.c"
# 1 "/usr/include/stdio.h" 1 3 4
...
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 943 "/usr/include/stdio.h" 3 4
# 2 "hello.c" 2
int main()
{
printf("hello, world\n");
return 0;
}

在源代码的前面插入了stdio.h,整个hello.i 的行数由hello.c的6行变到了849行.

第二步:编译阶段

执行命令: gcc -o hello.s -S hello.i
或者执行:ccl -o hello.s hello.i
编译器ccl 将文本文件hello.i 翻译为hello.s,这个文件里面包含一个汇编程序,如下:

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
.file   "hello.c"
.section .rodata
.LC0:
.string "hello world"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
movl $.LC0, (%esp)
call puts
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
.section .note.GNU-stack,"",@progbits

汇编语言是非常有用的,因为它给不同高级语言的不同编译器提供了通用的输出语言。例如,C和Fortran 的在此步编译产生的输出文件都是一样的汇编语言。

第三步:汇编阶段

执行命令: gcc -o hello.o -c hello.s
或者执行: as -o hello.o hello.s
汇编器as 将hello.s 翻译成机器语言保存在hello.o 中。这是个二进制文件

第四步:链接阶段

执行命令: gcc -o hello hello.o
或者执行: ld -o hello hello.o
注意:hello程序调用了printf 函数,这个函数是标准C库中的一个函数,他保存在一个名为printf.o 的文件中,这个文件必须以某种方式合并到我们的hello.o的程序中。链接器ld 负责处理这种合并。结果得到hello 可执行文件,可以被加载到内存中由系统执行。

最后总结

编译器的编译过程:
源文件–>预处理–>编译/优化–>汇编–>链接–>可执行文件。