前言
? 对于我们平时写代码运行,我们很少去关注编译和链接的过程,因为现在的开发环境都是集成(IDE)的,这些IDE一般都会将编译和链接的过程一步搞定,这一过程又被称为构建。但若经常写代码,经常会有很多莫名其妙的错误让我们不知所措,对于这些错误若我们能知其原因,那是再好不过了。因此本系列就是带你了解这些编译器和链接器在背后的工作
梦开始的地方
? 让我们先来看一个最最最经典的例子
//hello.c
#include <stdio.h>
int main()
{
printf("hello world");
return 0;
}
? 事实上,运行以上过程,可以被分解为四步:预处理、编译、汇编、链接
预编译
? 预处理器在预编译阶段会将源代码文件.c和相关的头文件编译为一个.i文件,其中主要处理“#”开头的预编译指令
? 预编译过程主要处理规则:
- 删除所有注释 "//" 和 "/**/"
- 处理所有条件预编译指令 "#if"、"#ifdef"等
- 删除所有 "#define",且展开所有宏定义
- 处理预编译指令"#include",将被包含的文件插入到相应的预编译指令位置。当然可能插入的文件还包含其他文件
- 添加行号和文件名标识,以便于编译器在编译时产生调试所用的行号信息和产生编译错误或警告时可以显示行号
- 保留所有#pragma指令,便于编译器使用
编译
? 所谓编译,就是将上一个预处理阶段处理完的文件进行词法分析、语法分析、语义分析和优化后生成的汇编代码文件。其过程最为关键且复杂。但是现在版本的GCC已经把预编译和编译合并为一个步骤,使用cc1来完成
? 现有如下片段:
array[index] = (index + 4) * ( 2 + 6 )
词法分析
? 首先,源代码程序被输入到扫描器,运用类似有限状态机的算法将源代码的字符序列分割为一系列记号(token)
记号 | 类型 |
---|---|
array | 标识符 |
[ | 左方括号 |
index | 标识符 |
] | 右方括号 |
= | 赋值 |
( | 左圆括号 |
index | 标识符 |
+ | 加号 |
4 | 数字 |
) | 右圆括号 |
* | 乘号 |
( | 左圆括号 |
2 | 数字 |
+ | 加号 |
6 | 数字 |
) | 右圆括号 |
- 词法分析的记号可分为:关键字、标识符、字面量(数字、字符串等)、特殊符号(加号等)
- 识别记号时,扫描器也会将标识符放入符号表,将字面量常量放入文字表,以备往后的步骤使用
语法分析
? 接下来时语法分析。语法分析器对扫描器产生的记号进行语法分析,再产生语法树,语法树时以表达式为节点的树。语法分析过程会采用上下文无关语法
- 语法分析过程,会确定运算符号的优先级和含义。此时若出现表达式不合法,编译器则会报告语法分析阶段的错误
语义分析
? 语法分析会对完成了表达式的语法层面的分析,但其并不知道这个语句是否有意义,因此需要语义分析器进行语义分析,从而对整个语法树的表达式标识类型;若有类型需要做隐式转换,会在语法树中插入相应的转换节点
? 语义分析分为两类,静态语义是编译器再编译器可以确定的语义;动态语义则是在运行期才能确定的语义
- 静态语义包括声明、类型的匹配和类型的转换。比如浮点类型赋值给整型,需要进行类型转换
生成中间语言
? 编译器有很多不同的优化,其中一种便是在源代码级别用源码级优化器进行优化。直接在语法树上优化较为困难,因此源代码优化器将语法树转换为中间代码
-
虽然中间代码看上去已经十分接近目标代码了,但中间代码和机器以及运行时环境无关
-
中间代码类型:三地址码,P-代码
//三地址码 //表示将y z进行op操作后赋值给x x = y op z t1 = 2 + 6 t2 = index + 4 t3 = t2 * t1 array[index] = t3; //优化 //优化程序会计算2 + 6 t2 = index + 4 t2 = t2 * 8 array[index] = t2;
-
中间代码可以将编译器分为前后端。前端由编译器产生机器无关的中间代码;而后端由编译器将中间代码转换为目标机器代码。前端关注的是正确反映代码含义的静态结构,而后端关注让代码良好运行的动态结构。好处是对于跨平台的编译器,它们可以针对不同平台使用同一个前端和针对不同机器的数个后端
目标代码生成于优化
? 编译器后端包括代码生成器和目标代码优化器。代码生成器将中间代码转换为目标机器代码,目标代码优化器会将目标代码进行优化
革命尚未结束
? 也许你觉得到这里我们已经万事俱备,已经形成可执行文件。但其实之前的步骤只是将源代码文件编译为目标文件,但在目标文件中我们还未确定index和array的地址,若index和array的地址在另一个程序模块,便没法确定地址,我们还需其他手段
? 这个问题由链接解决。事实上,定义在其他模块的全局变量和函数最终运行时的绝对地址都需要在链接时才能确定。编译器将一个源代码文件编译为一个未链接的目标文件,随后由链接器最终将目标文件链接未可执行文件。编译器只是暂时搁置调用地址的指令,最后等到链接时由链接器去修正地址
汇编
? 汇编器将汇编代码转变成机器指令(机器可以执行的指令)
- 由于每个汇编语句几乎都对应一条机器指令,因此汇编过程较为简单,没有复杂语法、语义、指令优化,只需根据汇编指令和机器指令的对照表一一翻译便好
深挖中间目标文件
? 中间目标文件又简称目标文件(object文件):编译器编译源代码后生成的文件。从结构上来说,目标文件是已经编译后的可执行文件格式,只是没有经过链接,其中某些符号和地址还没有调整,但其本身是按照可执行文件格式存储的
? 往后我们将深入分析目标文件格式,介绍ELF文件的重要段及文件头、段表、重定位表、字符串表、符号表、调试信息等相关结构;我们会了解到可执行文件、目标文件、库都是以段为基础的文件,不仅是数据和代码存放在相应段中,编译器也会将一些辅助信息按照表的方式存储
目标文件格式
? 现如今的pc平台流行的可执行文件格式为windows的PE和Linux的ELF,都是COFF格式的变种
? 目标文件格式和编译器和操作系统有关,不同平台下的格式各有不同
? ELF格式的文件类型分为四类:
ELF文件类型 | 说明 | 实例 |
---|---|---|
可重定位文件(relocatable file) | 包含数据和代码,可被用来链接成可执行文件或共享目标文件,静态链接库也属于此类 | Linux .o windows .obj |
可执行文件(executable file) | 包含可直接执行的程序,一般没有扩展名 | /bin/bash文件 windows .exe |
共享目标文件(shared object file) | 包含数据和代码,可在以下两种情况使用。一是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,生成新的目标文件;二是动态链接器可以将几个这样的共享目标文件与可执行文件结合,作为进程映像的一部分运行 | Linux .so,如/lib/glibc-2.5.so windows的DLL |
核心转储文件(core dump file) | 当进程意外终止时,系统可将该进程的地址空间的内容及终止时的其 |
首页 上一页 1 2 3 4 5 6 7 下一页 尾页 1/7/7 | |
【大 中 小】【打印】 【繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部】 | |
上一篇:C++:explicit关键字 | 下一篇:【Visual Leak Detector】配置项 .. |