一、前言
虽然用vs这样的集成工具开发C/C++程序很简单,只需要点一下就能完成代码的编译、生成可执行文件,但其底层完成的却并不容易。
而有些时候知道了底层原理,就能更加容易的找到bug、也能对代码理解的更加深入。
而本文就是来介绍一下C/C++整个编译过程的,主要分为4步:预处理、编译、汇编、链接。
这四部分的流程如下图所示:
graph TB
A[C/C++源码]--预处理-->B[预处理代码,以.i结尾]
B--编译-->C[得到汇编代码,以.s结尾]
C--汇编-->D[得到二进制代码,以.o结尾]
D--链接-->E[得到可执行文件,windows系统中以exe、dll结尾]
本文将以下面这份代码为例进行介绍:
这份代码只有三个文件,非常简单,分别为:
fun.h
:定义函数的头文件fun.cpp
:实现函数的源文件main.cpp
:入口源文件
为了方便查看每一步的处理结果,这三个文件我也在linux系统中放了一份,用于查看每一步编译后的结果文件。
二、预处理
首先第一步是预处理,它的作用是尽量去除掉对下一步编译无用的东西:
- 去除掉注释
- 将宏替换
- 将头文件展开
- 执行预处理指令
完成上面几个步骤后,前面的代码大概就是下面这样了:
主要就是:
- 将注释内容去除掉
- 将头文件在
.cpp
中原地展开 - 将宏替换掉
- 执行预处理指令排除掉不必要的代码
这一步只会检查预处理指令是否正确,其它的一概不管,直接无脑操作执行。
这里的预处理指令,指的就是以#
作为开头的命令,比如代码中的#include
、#define
等等。
其生成的文件一般以.i
作为后缀名。
如果使用linux系统上的g++编译器,那么对应的指令可能为:
g++ -E main.cpp -o main.i
g++ -E fun.cpp -o fun.i
-o
用于指定生成的结果存放在哪个文件中,上述两个指令就将分别得到两个C++源码文件的预处理文件。
然后进入这两个文件,查看一下这一步处理后的结果:
左边的是main.i
,右边的是fun.i
,可以看到预处理指令、注释等东西都已经被处理完毕了,只剩下了代码。
之所以main.i
中的内存如此繁杂,主要还是因为其包含了iostream这个标准库,虽然我们只使用了其中一个函数,但实际上它会将其内所有的源码都复制进来,所以代码量非常大。
三、编译
完成了上面预处理之后,就需要开始进行编译,目的是将我们的C++代码转换为汇编代码。
对汇编代码感兴趣的可以去查看动态逆向分析实战。
这里只需要知道的是,它会将项目中用到的所有cpp
源文件都编译成存放汇编代码的文件,一般以.s
作为后缀名。
比如上面我们有两个cpp文件,那么就会被编译成两个汇编文件:
main.s
fun.s
这两个文件中存放的就是其内部C++代码所对应的汇编代码。
如果使用linux系统中的g++编译器,对应的指令可能为:
g++ -S main.i -o main.s
g++ -S fun.i -o fun.s
效果如下:
此时查看其内的内容,就会发现已经都是汇编代码了:
注意,这里是这份C++源码在linux环境下的汇编代码,不同系统的汇编代码并不完全相同,C/C++跨平台,只是其源码跨平台,也就是同一份C/C++源码可以在不同系统上进行编译成为对应平台的汇编代码。
四、汇编
计算机只认识二进制的0和1,也就是由0和1组成的机器码,比如一个加法指令的机器码可能为:010101010
。
而汇编这一过程,就是将前面得到的汇编代码进一步转换为这样的机器码,其生成的文件一般以.o
作为后缀名。
比如上面我们得到的两个.s
汇编代码文件,将会被编译为两个.o
文件。
main.o
fun.o
这两个文件中存放的就是两个文件中汇编代码所对应的机器码了。
如果使用linux系统中的g++编译器,对应的指令可能为:
g++ -c main.s -o main.o
g++ -c fun.s -o fun.o
效果如下:
由于其已经编译为了二进制,所以直接用vim文本编辑器查看其内容,就是乱码:
一般我们会使用一些十六进制查看器来查看二进制文件。
五、链接
最后一步,我们还需要将前面得到的所有.o
文件,以及其用到的其它库文件代码,进行链接,生成最终的可执行程序。
比如代码中的iostream
就是标准库,最终就会去链接标准库的这些代码。
这其实可以看做是一个“打包”的过程。
将这些所有的.o
文件,以及系统库、标准库等等代码进行链接,最终就会生成我们看到的可执行文件,widnows系统下以.exe
作为后缀,linux系统下不使用后缀名。
对应的linux系统g++指令为:
g++ fun.o main.o -o executeable
效果如下:
此时它将所有.o文件链接为了一个可执行文件,然后我在linux系统中执行这个可执行文件,就得到了执行结果。
六、简单总结
从上面可以看到,这个过程可以说是非常繁琐的,只不过在windows系统下有vs这个神器让我们可以很舒服的进行开发。
而在linux系统下,一般我们就需要借用其它工具来组织代码编译了,比如make、cmake等等。
毕竟随便一个中小型项目可能都得有数十上百个源文件,这样亲手一个一个编译那就太繁琐了。