我们在 Linux 系统写下一段代码(C/C++),
以下面的代码为例:
文件名为:main.c
要想让它变成一个可以执行的文件,
那么就少不了 编译、链接 的过程。
那细分的话:
编译 又可以分为:预编译、编译、汇编 三个阶段。
1. 预编译(preprocessing)
完成的事情:
1)删除所有的 “#define”,并展开所有的宏定义
2)处理所有的条件预编译指令,“#if”、“#ifdef”、“#endif” 等;
3)处理 “#include” 预编译指令,将被包含的文件插入到预编译指令所在的文件;
4)删除所有的注释;
5)添加行号和文件名标识,以便于编译器产生调试用的符号信息及纠正错误和警告时显示行号;
6)保留所有的 #pragma 编译器指令,编译时需要使用。
gcc -E main.c -o main.i
其中的 main.c ------》源文件
main.i ------》生成的预编译后的文件
我们可以发现,预编译以后生成了一个名为 main.i 的文件,我们进去看看:
我们不难发现:
上面800多行的代码其实就是我们头文件 <stdio.h> 里的内容(#include的展开);
最终输出时定义的宏 MAX 被直接替换成了数值(宏替换);
第二个输出语句后面的注释被删除了(删除注释);
其实如果有#if,#endif这样的语句的话也会被处理(处理预编译指令)。
2. 编译(compilation)
完成的事情:
1)将源码转成汇编指令
2)进行词法、语法、语义的解析
3)代码优化
4)汇总符号 {[数据(全局、静态变量)、函数] 会生成符号}
gcc -S(大写) main.i
----> 生成了 main.s(小写) 的文件
同样的,我们可以通过 vim 指令进去看看 main.s 文件里都有什么:
可以看到:
编译后生成的文件中包含了汇编指令:push、call等等;(生成汇编指令)
还有一些这个例子不太能体现的,但是也在编译阶段做的事情:
进行代码优化、词法、语法解析,汇总符号。
3. 汇编(assembly)
完成的事情:
1)将汇编指令翻译成二进制格式
2)生成各个 section(段)
3)生成符号表
gcc -c main.s
----> 生成了 main.o 的文件
main.o----可重定位的二进制目标文件
同理我们通过 vim 指令进去看一看:
我们会发现,这里面全部是“乱码”,根本无法查阅,读取;
这是因为经过汇编后生成了二进制文件,
那有什么方法可以看 main.o 文件呢?
其实也是有的——objdump、readelf(这俩方法下次我们单独讨论)
经过前面编译的准备,最终来到了链接部分:
完成的事情:
1)合并所有文件的各个 section,调整段的大小及段的起始位置。合并符号表,进行符号解析,并给符号分配一个虚拟地址。
2)进行符号重定位,在使用符号的地方,全部替换成符号的虚拟地址。
链接:
- gcc 所有生成的中间文件 ------》 a.out
- gcc 所有生成的中间文件 -o 自己命名的文件(可执行文件名)
其实,我们在链接的时候,就自动完成了前面的编译工作;
所以,日常操作中,我们为了提高工作效率,
一般不会挨个进行编译,然后最后再链接,
我们可以通过链接,一次性完成编译链接的工作,生成可执行文件。
如上图所示,是我们一般习惯用的方式,
也可以用 gcc -o main main.c 的方法,效果是一样的:
That’s all , thanks for watching !