3. C 语言的编译过程

当一个编写好的 .c 文件经过编译变成二进制的可执行文件一共经过四个阶段。

这四个阶段分别是:

  1. 预处理(Preprocessing)。
  2. 编译(Compilation)。
  3. 汇编(Assembly)。
  4. 连接(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>
指定编译标准,如:c89c99c11 等。
--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!,使用上述四个步骤最终生成可执行文件并运行。