一、 前言
高度封装的事物(如各种IDE)在提供便捷操作的同时也失去了许多美好的内部细节,往往让让使用者只知道how to use 而不知道how to achieve,因而在出现一些封装内部的错误时就会让使用者手足无措,因此了解其内部的大致运行过程将有助于处理一些集成环境不提示的错误。
二、基本概念
编译: 编译器对源代码进行编译,是将以文本形式存在的源代码翻译为机器语言形式的目标文件的过程。
编译单元:对于C++来说,每一个cpp文件就是一个编译单元。从之前的编译过程的演示可以看出,各个编译单元之间是互相不可知的。
目标文件:由编译所生成的文件,以机器码的形式包含了编译单元里所有的代码和数据,以及一些其他的信息。
三、 程序执行宏观流程
众所周知,计算机和人类世界的关系就如同男人和女人的关系一样,前者永远无法基于一个同等规则的世界中理解后者,因此人类要想和计算机交流就只能使用机器码,但作为一名普通人类,显然用一串串01相互交流是不太现实的,我们是好逸恶劳的生物,更喜欢使用人类自己的语言,这样一来就需要一些中间媒介(编译器)承担一个类似于adapter(适配器)的角色。语言从低级到高级发展而来,有着自己不同的语法规则,因此不得不通过一层层的转译来实现和机器交流的过程(是不是很像网络协议栈这货)。这个由高级语言代码->低级语言代码->机器码->计算机识别运行的过程就是编译运行的过程。
例如C语言的源文件(**.c)->程序运行过程(**.exe)如下:
1、 源文件(hello.c) -> 预处理器 –> hello.i(文本文件)
将include包含的头文件进行解析,处理宏定义和条件编译命令,屏蔽无效的代码段,最后生成新的源代码hello.i
2、 hello.i -> 编译器 ->hello.s(汇编文本文件)
经过编译原理中的词法和语法分析优化并生成汇编源文件
3、 hello.s -> 汇编器 -> hello.o(目标二进制文件)
生成机器码并且包含编译后生成的一些辅助表(后文会提及),封装成目标文件hello.o
4、 hello.o -> 连接器 -> hello.exe
连接将要用到的库文件(.lib)和用户自定义的头文件中的内容(可能存在通过extern关键字相互共用的变量和函数),将其内容进行补充(函数和具体变量内容),最终 生成可执行文件由机器执行。
四、 程序执行微观过程
这里主要摘取三中例子作为详细分析。
三.1~三.3(嘿嘿,像不像对象实例调用内部成员)阶段:
Gcc编译器是以每个编译单元之内进行编译工作的,也就是说如果存在多个cpp和相应的.h文件则无法在这个阶段相互之间进行数据共享,而且为了合乎一定的机器处理数据规则(数据对齐那篇有提到http://blog.csdn.net/zhang360896270/article/details/39340587)尽量要求各程序段从0号(偶数)地址位开始,显然会有人质疑,根本不可能会有这么多0号地址位,所以这当然是一个相对地址(在link阶段会有一个重定向的过程)。既然编译阶段没有数据的共享,那么如extern这种关键字(下一篇文章将详细介绍几个常用却不太熟悉的关键字,说了这篇是前戏嘛,高潮在后面)怎么起作用呢?答案是此阶段不处理被extern标记的数据或函数而交由下一个link阶段执行。总之,此阶段主要是针对每一个编译单元将其转译为汇编语言,同时针对需要数据共享的地方建立一些辅助表,而不是真正去处理这些单元,感觉可能类似于map-reduce的map阶段(不是很熟,希望各位路过大神举个更好的例子)。那么有哪些辅助表呢?先引入一个符号的概念,符号就是标识(比如extern int n在表中符号就为n,而函数在object文件中的标识更加复杂,因为涉及到重载及其他复杂关系,如果需要标识函数fun(int x, int y)则C++编译器将其转译为fun_int_int,这样函数名参数类型和数量就都一目了然了,因此可以支持函数重载,同样也很好地解释了为什么仅有返回值不同是无法实现函数重载的,因为link的时候不知道要调用哪一个函数),这样可以简单地将这些表与理解成一个符号与地址的映射(hashmap)。
Object目标文件中有如下三个表:
提供共享数据信息的表:
1、 未解析符号表(unresolved symbol table):此表记录当前编译单元未被定义的变量,有可能来自系统库或者用户其他用extern标识的数据(符号+地址)。
2、 导出符号表(export symbol table):此表用于记录自己能为其他编译单元提供的共享数据(符号+地址),与1是一进一出(好邪恶~~)的关系。
重定位地址的表:
3、地址重定位表(address redirect table):此表提供本编译单元对自身地址 的记录,用于找到真正的物理地址(直接加上偏移地址)。
三.4阶段:
此阶段在实际编译运行过程中其实比较复杂(数据、代码都分为不同区域),这里只重点点明一下原理:
首先链接器找到每个object目标文件的位置,通过地址重定向表(art表)对每个地址重定位,然后依次遍历其ust表,从而知道了具体缺少哪些数据,然后从所有的est表中通过符号找到数据存放的地址并在相应位置处填上找到的具体地址,再做一些其他工作,最后生成一个可执行文件exe。
这样可能更能便于各位Geek们看懂:
For(every element i of ust)
If (lack_datum(ust[i].symbol)){
For(every element j of est){
If (symbol(ust[i] .symbol == est[j] .symbol)) Ust[i].setAddress(est[j].address);
//实际上这里是将编译单元中每一个缺少ust[i].address的地方填上地址,因为汇编基本都是直接和地址打交道。
}
}
五、总结
事物都是一步步由简单到复杂逐步发展起来的,看似轻松的编译,其实也曾经历了这么多复杂的过程,向计算机的鼻祖和先驱们表示无限的敬意。
参考资料来源:
http://blog.csdn.net/hitprince/article/details/7880241(推荐,讲得很详细)
http://blog.csdn.net/dlutxie/article/details/6776936
http://blog.sina.com.cn/s/blog_4ea497b70100hw9r.html