首页 > 代码库 > 执行的前戏------编译连接全过程理解

执行的前戏------编译连接全过程理解

一、 前言

         高度封装的事物(如各种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)和用户自己定义的头文件里的内容(可能存在通过externkeyword相互共用的变量和函数),将其内容进行补充(函数和详细变量内容),终于               生成可运行文件由机器运行。

 

四、 程序运行微观过程

这里主要摘取三中样例作为具体分析。

三.1~三.3(嘿嘿,像不像对象实例调用内部成员)阶段

         Gcc编译器是以每个编译单元之内进行编译工作的,也就是说假设存在多个cpp和对应的.h文件则无法在这个阶段相互之间进行数据共享,并且为了合乎一定的机器处理数据规则(数据对齐那篇有提到http://blog.csdn.net/zhang360896270/article/details/39340587)尽量要求各程序段从0号(偶数)地址位開始。显然会有人质疑,根本不可能会有这么多0号地址位,所以这当然是一个相对地址(在link阶段会有一个重定向的过程)。既然编译阶段没有数据的共享,那么如extern这样的keyword(下一篇文章将具体介绍几个经常使用却不太熟悉的keyword。说了这篇是前戏嘛,高潮在后面)怎么起作用呢?答案是此阶段不处理被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

 

执行的前戏------编译连接全过程理解