我们通过编译一个程序test.c,代码如下,来了解编译程序的过程。

#include <stdio.h>

int main(){
printf("Hello world!");
return 0;
}

1.预处理(Preprocessing)

~/Desktop/test$ gcc -E test.c -o test.i
~/Desktop/test$ ls
test.c test.i

-E选项:让编译器在预处理后停止,并输出预处理结果。

在本例中预处理结果就是将stdio.h 文件中的内容插入到test.c中,我们可以通过cat命令查看一下test.i文件的内容:

~/Desktop/test$ cat test.i
# 1 "test.c"
...

extern int printf (const char *__restrict __format, ...);

...
# 2 "test.c" 2


# 3 "test.c"
int main(){
printf("Hello world!");
return 0;
}

瞧!把头文件stdio.h的内容都插入到test.c文件中来了。

2.编译(Compilation)

~/Desktop/test$ gcc -S test.i -o test.s
~/Desktop/test$ ls
test.c test.i test.s

预处理之后,可直接对生成的test.i文件编译,生成汇编代码。
-S选项:表示在程序编译期间,在生成汇编代码后停止,
-o选项:输出汇编代码文件。

编译成的汇编代码如下:

.file   "test.c"
.text
.section .rodata
.LC0:
.string "Hello world!"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0"
.section .note.GNU-stack,"",@progbits
~

3.汇编(Assembly)

~/Desktop/test$ gcc -c test.s -o test.o
~/Desktop/test$ ls
test.c test.i test.o test.s

gas汇编器负责将汇编代码文件test.s编译为目标文件。这是一个二进制文件。

4.连接(Linking)

~/Desktop/test$ gcc test.o -o test
~/Desktop/test$ ./test
Hello world!

gcc连接器将生成的test.o与C标准输入输出库进行连接,最终生成程序test。
gcc连接器是gas汇编器提供的,负责将程序的目标文件与所需的所有附加的目标文件连接起来,最终生成可执行文件。附加的目标文件包括静态连接库和动态连接库。