24.深入解析C/C++编译过程:预处理、编译、汇编与链接

一、前言

虽然用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结尾]

本文将以下面这份代码为例进行介绍:

image-20231125100501679

这份代码只有三个文件,非常简单,分别为:

  • fun.h:定义函数的头文件
  • fun.cpp:实现函数的源文件
  • main.cpp:入口源文件

为了方便查看每一步的处理结果,这三个文件我也在linux系统中放了一份,用于查看每一步编译后的结果文件。

二、预处理

首先第一步是预处理,它的作用是尽量去除掉对下一步编译无用的东西:

  1. 去除掉注释
  2. 将宏替换
  3. 将头文件展开
  4. 执行预处理指令

完成上面几个步骤后,前面的代码大概就是下面这样了:

image-20231125100820826

主要就是:

  • 将注释内容去除掉
  • 将头文件在.cpp中原地展开
  • 将宏替换掉
  • 执行预处理指令排除掉不必要的代码

这一步只会检查预处理指令是否正确,其它的一概不管,直接无脑操作执行。

这里的预处理指令,指的就是以#作为开头的命令,比如代码中的#include#define等等。

其生成的文件一般以.i作为后缀名。

如果使用linux系统上的g++编译器,那么对应的指令可能为:

g++ -E main.cpp -o main.i
g++ -E fun.cpp -o fun.i

-o用于指定生成的结果存放在哪个文件中,上述两个指令就将分别得到两个C++源码文件的预处理文件。

image.png

然后进入这两个文件,查看一下这一步处理后的结果:

image.png

左边的是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

效果如下:

image.png

此时查看其内的内容,就会发现已经都是汇编代码了:

image.png

注意,这里是这份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

效果如下:

image.png

由于其已经编译为了二进制,所以直接用vim文本编辑器查看其内容,就是乱码:

image.png

一般我们会使用一些十六进制查看器来查看二进制文件。

五、链接

最后一步,我们还需要将前面得到的所有.o文件,以及其用到的其它库文件代码,进行链接,生成最终的可执行程序。

比如代码中的iostream就是标准库,最终就会去链接标准库的这些代码。

这其实可以看做是一个“打包”的过程。

将这些所有的.o文件,以及系统库、标准库等等代码进行链接,最终就会生成我们看到的可执行文件,widnows系统下以.exe作为后缀,linux系统下不使用后缀名。

对应的linux系统g++指令为:

g++ fun.o main.o -o executeable

效果如下:

image.png

此时它将所有.o文件链接为了一个可执行文件,然后我在linux系统中执行这个可执行文件,就得到了执行结果。

六、简单总结

从上面可以看到,这个过程可以说是非常繁琐的,只不过在windows系统下有vs这个神器让我们可以很舒服的进行开发。

而在linux系统下,一般我们就需要借用其它工具来组织代码编译了,比如make、cmake等等。

毕竟随便一个中小型项目可能都得有数十上百个源文件,这样亲手一个一个编译那就太繁琐了。

作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux