3. C 语言的编译过程
当一个编写好的 .c 文件经过编译变成二进制的可执行文件一共经过四个阶段。
这四个阶段分别是:
- 预处理(Preprocessing)。
- 编译(Compilation)。
- 汇编(Assembly)。
- 连接(Linking)。
下面我们使用 Linux 下的 gcc 编译器来了解一下这四个阶段。这也是C语言开发者必须知道的编译过程。
GCC 编译器
GCC(GNU Compiler Collection)是GNU 开源组织开发的编译器集合。 它是开源领域最重要、应用最广泛的编译器之一,支持多种编程语言和硬件平台。
gcc 命令格式如下:
gcc [选项] 文件路径
常用选项
选项
说明
-o <文件路径>指定输出路径,代替
a.out。-g加入调试信息(用于GDB调试)。
-I<文件夹路径>指定头文件搜索路径。
-l<库名>指定库文件名称。
-L<路径>指定库文件搜索路径。
-static静态链接。
-shared生成动态库(.so)。
-fPIC生成位置无关代码(用于动态库)。
-D<宏>定义宏。
-E仅预处理,不编译、汇编和连接。
-S仅编译,不汇编和连接。
-c编译和汇编,但不连接。
-std=<standard>指定编译标准,如:
c89、c99、c11 等。--version显示版本信息。
-O<n>设置优化级别,n为 0~3。
-Wall报告所有警告。
1. 预处理
预处理是将 C 语言中以井号(#) 开头的预处理指令(如:#include <xxx.h>、#ifdef XXX等)进行处理后生成可以编译的 C 程序。
在 GCC 中使用 -E 选项可以进行预处理但不执行后续步骤。如下所示:
weimingze@mzstudio:~$ gcc -E -o hello.i hello.c
weimingze@mzstudio:~$ ls
hello.c hello.i
上述运行后生成的 hello.i 的部分内容如下:
# 0 "hello.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
... # 此处省略多行
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (1)));
# 959 "/usr/include/stdio.h" 3 4
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
# 983 "/usr/include/stdio.h" 3 4
# 2 "hello.c" 2
# 3 "hello.c"
int main(int argc, char *argv[]) {
printf("Hello World!\n");
return 0;
}
2. 编译。
编译是将预处理后的 C 语言信息进行编译,将其生成汇编代码。
在 GCC 中使用 -S 选项可以进行编译,但不执行后续步骤。以下对 hello.i 进行编译,生成文件 hello.s。
weimingze@mzstudio:~$ gcc -S hello.i -o hello.s
weimingze@mzstudio:~$ ls
hello.c hello.i hello.s
hello.s 的内容如下:
.file "hello.c"
.text
.section .rodata
.LC0:
.string "Hello World!"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movq %rsi, -16(%rbp)
leaq .LC0(%rip), %rax
movq %rax, %rdi
call puts@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
3. 汇编。
编译是将上述汇编程序的指令等进行替换,替换成二进制的机器指令,通常生成的 .o 文件,我们称之为目标文件。.o 文件是二进制文件,用文本编辑器无法阅读。
以下将 hello.s 汇编后生成 hello.o
weimingze@mzstudio:~$ gcc -c -o hello.o hello.s
weimingze@mzstudio:~$ ls
hello.c hello.i hello.o hello.s
文件
hello.o是汇编后的二进制文件。
4. 链接。
链接是将一个或多个 .o 文件连接成为一个可执行程序程序。
以下将 hello.o 链接成为可执行程序 hello。
weimingze@mzstudio:~$ gcc -o hello hello.o
weimingze@mzstudio:~$ ls
hello hello.c hello.i hello.o hello.s
weimingze@mzstudio:~$ ./hello
Hello World!
经历上述四步,我们将 hello.c 最终生成了可执行程序 hello。
练习:
写一个程序,打印两行 Hello World!,使用上述四个步骤最终生成可执行文件并运行。