C/C++ 编译流程简析

在 Linux 中我们执行 gcc --help 可以看到简要的 gcc 帮助文档。这份文档中的几个参数激发了我一探究竟的好奇心。这几个参数是:

-E        Preprocess only; do not compile, assemble or link.
          只预处理;不编译,组装 和 链接库。
-S        Compile only; do not assemble or link.
          只编译成汇编语言;不组装 和 链接库。
-c        Compile and assemble, but do not link.
          编译并组装,但是不链接库。

我们分别来看一下在这几个参数下输出的文件内容都有些啥。

一、预处理阶段

首先我们先创建一个叫 compile-project 的项目文件夹。在文件夹下创建一个程序文件:main.c。目录结构如下:

compile-project/
  └─ main.c

main.c 中简单写一个 Hello World:

#include <stdio.h>

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

然后执行 gcc -E 的预处理命令,查看输出文件的内容。

gcc -E main.c -o main.1

mian.1 文件内容:

# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "main.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 1 3 4

... 省略中间的700多行

extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
# 873 "/usr/include/stdio.h" 3 4

# 2 "main.c" 2


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

可见,预处理这一阶段主要是将 #include #define #ifdef 等宏指令替换掉,把 #include 指向的头文件如 stdio.h 中的内容拷贝到源文件中。

二、编译阶段

还是之前的 main.c 文件,我们执行 gcc -S 的编译命令,查看输出文件内容。

gcc -S main.c -o main.2

mian.2 文件内容:

    .file "main.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
    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 9.3.0-17ubuntu1~20.04) 9.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:

可见,编译阶段 gcc 把之前预处理好的源文件编译成了汇编语言。

三、组装阶段

接着对 main.c 文件执行 gcc -c 命令,查看输出的文件内容。

gcc -c main.c -o main.3

mian.3 文件内容:

7f45 4c46 0201 0100 0000 0000 0000 0000
0100 3e00 0100 0000 0000 0000 0000 0000
0000 0000 0000 0000 1803 0000 0000 0000
0000 0000 4000 0000 0000 4000 0e00 0d00
f30f 1efa 5548 89e5 488d 3d00 0000 00b8
0000 0000 e800 0000 00b8 0000 0000 5dc3
4865 6c6c 6f20 576f 726c 6421 0000 4743
433a 2028 5562 756e 7475 2039 2e33 2e30
2d31 3775 6275 6e74 7531 7e32 302e 3034
2920 392e 332e 3000 0400 0000 1000 0000

... 省略中间的100行左右

2802 0000 0000 0000 2a00 0000 0000 0000
0000 0000 0000 0000 0100 0000 0000 0000
0000 0000 0000 0000 1100 0000 0300 0000
0000 0000 0000 0000 0000 0000 0000 0000
a002 0000 0000 0000 7400 0000 0000 0000
0000 0000 0000 0000 0100 0000 0000 0000
0000 0000 0000 0000 

在组装阶段,gcc 把汇编指令翻译成了二进制机器码。但这个时候还没有链接运行二进制所需要的库文件,如果这个时候直接执行这个二进制会提示错误。

~/compile-project$ ./main.3
-bash: ./main.3: cannot execute binary file: Exec format error

四、链接库文件

直接执行 gcc 完整编译命令:

gcc main.c -o main.o

mian.o 文件内容:

7f45 4c46 0201 0100 0000 0000 0000 0000
0300 3e00 0100 0000 6010 0000 0000 0000
4000 0000 0000 0000 7839 0000 0000 0000
0000 0000 4000 3800 0d00 4000 1f00 1e00
0600 0000 0400 0000 4000 0000 0000 0000
4000 0000 0000 0000 4000 0000 0000 0000
d802 0000 0000 0000 d802 0000 0000 0000
0800 0000 0000 0000 0300 0000 0400 0000
1803 0000 0000 0000 1803 0000 0000 0000
1803 0000 0000 0000 1c00 0000 0000 0000

... 省略中间的1020行左右

0000 0000 0000 0000 0100 0000 0000 0000
0100 0000 0000 0000 0100 0000 0200 0000
0000 0000 0000 0000 0000 0000 0000 0000
4030 0000 0000 0000 1806 0000 0000 0000
1d00 0000 2e00 0000 0800 0000 0000 0000
1800 0000 0000 0000 0900 0000 0300 0000
0000 0000 0000 0000 0000 0000 0000 0000
5836 0000 0000 0000 0402 0000 0000 0000
0000 0000 0000 0000 0100 0000 0000 0000
0000 0000 0000 0000 1100 0000 0300 0000
0000 0000 0000 0000 0000 0000 0000 0000
5c38 0000 0000 0000 1a01 0000 0000 0000
0000 0000 0000 0000 0100 0000 0000 0000
0000 0000 0000 0000 

可见,链接完库文件后生成的二进制比之前大了很多,链接完库文件之后此时的二进制文件就可以执行了,下面我们运行看一下打印结果:

~/compile-project$ ./main.o
Hello World!

通过对这几个参数执行结果的分析,我们大概能看出 gcc 编译的流程是怎样的了。

本作品采用《CC 协议》,转载必须注明作者和本文链接
xiaer
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!