c编译流程

写在前面:由于工作中需要接处到大量c/c++代码,本身python、java出身的我,还是有必要系统了解一下c/c++代码的编译流程。

以gcc为例,clang的话可以参考这几篇文章

clang到底是什么?gcc和clang到底有什么区别?-CSDN博客

What is the differences between gcc and Clang/LLVM? (zhonguncle.github.io)
GCC | 爱编程的大丙 (subingwen.cn)

首先我们得知道编译器到底是什么,或者说是编译什么。通常我们说的编译器,是将源代码转换成可执行程序的程序。比如gcc编译得到可执行程序。在编译原理里面是这样定义的:

简单来说,编译器就是一个程序能读取某种语言的一个程序(这个语言是源语言),然后将其翻译转换成另一种语言的等价程序(这个语言被称为目标语言)。编译器最重要的一个规定就是报告在翻译过程中发现“源”程序的错误。如果这个目标程序是机器语言,那么这个目标程序就是可执行程序。

但其实,gcc这种现代“编译器”是一个工具集合,包含了预处理器、编译器,而且会自己调用汇编器、连接器或加载器等多种工具,而不是单单的一个编译器,按照近几十年的标准编译流程来说,编译器指的是从将.c等文件转换成.s文件的程序。为了方便解释,除非特地说明,下文中的“编译器”都是按这个定义。

从源代码到可执行文件的过程

可以看到从源代码到可执行程序要经过预处理器(preprocessor)、编译器(compiler)、汇编器(assembler)和连接器(linker)或加载器(loader),而编译器只是负责将源代码转换成对应的汇编代码的功能。

上面的几种处理转换程序除了编译器和汇编器,其他三个估计都很很少听到。下面就用最经典的 C 语言和gcc来介绍这个过程,gcc包含的预处理器为cpp,还会调用汇编器as、连接器ld

gcc支持在将源代码转换成可执行程序的过程中的某一步停止,也就是限定终点(如果依次手动操作的话,也就是只进行某一步),我们就先用这个机制来展示整个编译过程。

比如我有三个文件,为main.ccalc.ccalc.h ,新建build目录,在build目录下测试,方便处理生成文件

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
30
31
//main.c
#include <stdio.h>
#include "calc.h"

#define MAXNUM 1000

int main()
{
int a = calc(12, 13, 14);

if (a >= MAXNUM)
printf("Over Limit!\n");
else
printf("Result: %d", a);

return 0;
}


//calc.c
#include <stdio.h>
#include "calc.h"

int calc(int a, int b, int c)
{
return a*b*c;
}


//calc.h
int calc(int, int, int);

1. 预处理阶段

首先是预处理源代码,如果需要在预处理之后停止需要使用选项-E:

1
2
gcc -E ../calc.c -o calc.i
gcc -E ../main.c -o main.i

这里需要使用-o来指定输出文件名称,这是因为gcc调用的预处理器cpp会将处理后的内容输出到标准输出,而不是生成某个文件。预处理之后的文件后缀为.i,所以这里把后缀都换成.i。此时使用ls查看如下:

1
2
ls
calc.i main.i

2. 编译阶段

接下来是编译阶段。如果你想编译但不进行汇编,那么使用-S选项:

1
gcc -S main.i calc.i

这里会对两个文件进行编译,并生成汇编语言文件,生成文件名是将原文件的.c.i等后缀替代成.s(因为是从某一步开始,到编译这步停止,不同后缀表示开始的阶段),此时使用ls查看如下:

1
2
ls 
calc.i calc.s main.i main.s

3. 汇编阶段

接下来是汇编阶段。这里使用选项-c,这个选项是进行编译和汇编,但是不进行连接。上文提到过,是根据后缀判断从整个编译流程的哪一步开始,然后进行到汇编之后、连接之前这个阶段。刚才是进行了编译,但是没有汇编,所以这里使用-c相当于只使用了汇编器:

1
gcc -c calc.s main.s

-S选项一样,它会自动生成对象文件(object file),文件名为用.o替代掉源文件名的.c.i.s等后缀,此时使用ls查看如下:

1
calc.i  calc.o  calc.s  main.i  main.o  main.s

4. 连接阶段

这是最后的连接阶段,使用-o直接输出即可,因为从汇编之后的对象文件到可执行程序和从源代码到可执行程序这个结果是一样的,gcc这些选项只是限定“终点”。这里我们将输出文件名设定为calc

1
gcc -o calc main.o calc.o

使用ls看到如下内容:

1
2
ls
calc calc.i calc.o calc.s main.i main.o main.s

这时候我们运行calc看看:

1
2
./calc
Over Limit!

上述流程也可以用cpp、gcc、as、ld依次进行预处理、编译、汇编、连接操作,但是ld的命令会很复杂,实际中不会这样用!这里就不赘述了,细节可以看参考文章。

gcc介绍

前面我们已经讲过,gcc是一个工具集合,其从源代码到可执行文件的过程中,分为四个阶段,gcc命令可以将这四个步骤合并成一个

  • 预处理: 在这个阶段主要做了三件事:展开头文件 、宏替换 、去掉注释行,这个阶段需要gcc调用预处理器来完成, 最终得到的还是源文件, 文本格式

  • 编译: 这个阶段需要gcc调用编译器对文件进行编译,最终得到一个汇编文件

  • 汇编: 这个阶段需要gcc调用汇编器对文件进行汇编,最终得到一个二进制文件

  • 链接: 这个阶段需要gcc调用链接器对程序需要调用的库进行链接, 最终得到一个可执行的二进制文件

gcc常用参数

gcc和g++区别

  • 在代码编译阶段(第二个阶段):

    • 后缀为 .c 的,gcc 把它当作是C程序,而 g++ 当作是 C++ 程序
    • 后缀为.cpp的,两者都会认为是 C++ 程序,C++ 的语法规则更加严谨一些
    • g++会调用gcc,对于C++代码,两者是等价的, 也就是说 gcc 和 g++ 都可以编译 C/C++代码
  • 在链接阶段(最后一个阶段):

    • gcc 和 g++ 都可以自动链接到标准C库

    • g++ 可以自动链接到标准C++库, gcc如果要链接到标准C++库需要加参数 -lstdc++

  • 关于 __cplusplus宏的定义

    • g++ 会自动定义__cplusplus宏,但是这个不影响它去编译C程序

    • gcc 需要根据文件后缀判断是否需要定义 __cplusplus 宏 (规则参考第一条)

综上所述:
不管是 gcc 还是 g++ 都可以编译 C 程序,编译程序的规则和参数都相同
g++可以直接编译C++程序, gcc 编译 C++程序需要添加额外参数 -lstdc++
不管是 gcc 还是 g++ 都可以定义 __cplusplus宏