完全理解连接器:二,符号决议

在连接器可操做的元素一节中咱们提到,全部的应用程序都是连接器将所须要的一个个简单的目标文件聚集起来造成的。你能够将这个过程想象成拼图游戏,每一个拼块就是一个简单的目标文件:程序员


1,拼图游戏当中的每一个拼块都依赖于其它拼块提供的拼接口,这就比如咱们写的程序模块依赖于其它模块提供的编程接口,好比咱们在list.c中实现了一种特定的链表数据结构,其它模块须要使用这种链表,这就是模块间的依赖。而连接器其中一项任务就是要确保提供给连接器进行连接的目标文件集合之间依赖是成立的(也就是说,不会出如今被依赖的模块中连接器找不到须要的接口),这就是后面咱们要讲到的符号决议(Symbol Resolution),开篇提到的第一个问题就来自这个过程。编程



2,咱们在拼图游戏当中一般都是将一整幅图按组成部位一部分一部分拼接好,而后将这些比较完整的大的组成部分拼接成最后一整副图。这就比如连接器会首先将程序每一个模块当中目标文件集合连接成库,而后再将各个库进行连接最终造成可执行程序。这就是后面咱们要讲到的可执行程序的生成(这也是咱们在上一篇文章当中留在本章讨论的)。微信


3,连接器还有一项任务是没法用这个拼图游戏来类比的,可是这项重要的任务对程序员不可见,做为程序员几乎不会在这个过程遇到问题,这项任务就是重定位。数据结构

    

经过拼图这个游戏的类比,咱们给出连接器的工做过程:函数


首先,连接器对给定的目标文件或库的集合进行符号决议以确保模块间的依赖是正确的。url


其次,连接器将给定的目标文件集合进行拼接打包成须要的库或最终可执行文件。spa


最后,连接器对连接好的库或可执行文件进行重定位。.net

    

接下来咱们详细的讲解下每个过程。翻译


首先讲解连接器的符号决议过程。3d


在这个过程中,连接器须要作的工做就是确保全部目标文件中的符号引用都有惟一的定义。要想理解这句话咱们首先来看看一个典型的c文件里都有些什么。


c源文件中都有什么


如图所示是一个典型的c源文件,该文件中的变量能够划分为两类:

  • 全局变量:好比x_global_uninit,x_global_init,fn_c。只要程序没有结束运行,全局变量均可以随时使用。注意,用static修饰的全局变量好比y_global_uninit,其生命周期也等同于程序的运行周期,只是这种全局变量只能在所被定义的文件当中使用,对其它文件不可见。


  • 局部变量:好比y_local_uninit,y_local_init,局部局部变量的生命周期和全局变量不一样,局部变量变量只能在相应的函数内部使用,当函数调用完成后该函数中的局部变量也就没法使用了。由于局部变量只存在于函数运行时的栈帧当中,函数调用完成后相应的栈帧被自动回收(若是你还不能理解这句话是什么意思没有关系,我会在后面的文章当中详细讲解程序运行时的内存模型)。



目标文件里有什么


编译器的任务就是把人类能够理解的代码转换成机器能够执行的机器指令,源文件编译后造成对应的目标文件,这个咱们在以前的章节中已经屡次提到过了。源文件被编译后生成的目标文件中本质上只有两部分:


  • 代码部分:你可能会想,一个源文件中不都是代码吗,这里的代码指的是计算机能够执行的机器指令,也就是源文件中定义的全部函数。好比上图中定义的函数fn_b以及fn_c。


  • 数据部分:源文件中定义的全局变量。若是是已经初始化后的全局变量,该全局变量的值也存在于数据部分。


到目前为止,你能够把一个目标文件简单的理解为由两部分组成,代码部分中保存的是CPU能够执行的机器指令,这些机器指令来自程序员所定义的函数,编译器将这些定义的函数翻译成机器指令并存放在目标文件的代码部分。数据部分存放的是机器指令所操做的数据。所以目前,你能够简单的将目标文件理解为一个只有两部分的文件,如图所示:

你可能会好奇函数中定义的局部变量为何没有放到目标文件的数据段当中,这是由于局部变量是函数私有的,局部变量只能在该函数内部使用而全局变量时没有这个限制的,因此函数私有的局部变量被放在了代码段中,做为机器指令的操做数。


编译器在编译过程当中遇到外部定义的全局变量或函数时,只要编译器能找到相应的变量声明就会在内心默念“all is well, all is well(一切顺利)“,从这里能够看出编译器的要求仍是很低的,至于所使用变量的定义编译器是不会费力去四处搜索,而是愉快的继续接下来的编译。注意,这里再次强调一下,编译器在遇到外部定义的全局变量或者函数时只要能在当前文件找到其声明,编译器就认为编译正确。而寻找使用变量定义的这项任务就被留给了连接器。连接器的其中一项任务就是要肯定所使用的变量要有其惟一的定义。虽然编译器给连接器留了一项任务,但为了让连接器工做的轻松一点编译器仍是多作了一点工做的,这部分工做就是符号表(Symbol table)。


符号表(Symbol table)


咱们在上一节中提到,虽然编译器很不厚道的给连接器留了一项任务,可是编译器为了连接器工做的轻松一点仍是作了一点事情,这就是符号表。那符号表中保存的是什么呢,符号表中保存的信息有两部分:

  • 该目标文件中引用的全局变量以及函数

  • 该目标文件中定义的全局变量以及函数


以上图中的代码为例,编译器在编译过程当中每次遇到一个全局变量或者函数名都会在符号表中添加一项,最终编译器会统计出以下所示的一张符号表:



z_global以及fn_a是未定义的,由于在当前文件中,这两个变量仅仅是声明,编译器并无找到其定义。剩余的变量编译器均可以在当前文件中找到其定义。


fn_b以及fn_c为当前文件定义的函数,由于在代码段。


剩余的符号都是全局变量,所以放在了数据段。

 

有同窗可能会问,为何全局变量y_global_uninit ,y_global_init以及函数fn_b不可被其它目标文件引用,这是由于这些变量用static修饰过了,在C语言中经static修饰过的函数的函数以及变量都是当前文件私有的,对外部不可见,这里必定要注意。因此static这个关键字的用法就是,若是你认为一个变量只应该被当前文件使用而不暴露给外部,那么你就可使用static关键字修饰一下。  


本质上整个符号表只是想表达两件事:

  • 我能提供给其它文件使用的符号

  • 我须要其它文件提供给我使用的符号


这里还有一个问题就是,编译器将统计的这张符号表放在哪里了呢?



符号表存放在哪里


在目标文件里有什么这一小节中,咱们将一个目标文件简单的划分了两段,数据段和代码段,如今咱们要向目标文件中再添加一段,而符号表也被编译器很贴心的放在目标文件中,所以一个目标文件能够理解为如图所示的三段,而符号表中的内容就是上一节当中编译器统计的表格。



有了符号表,连接器就能够进行符号决议了。


符号决议的过程


在上一节符号表中,咱们知道符号表给连接器提供了两种信息,一个是当前目标文件能够提供给其它目标文件使用的符号,另外一个其它目标文件须要提供给当前目标文件使用的符号。有了这些信息连接器就能够进行符号决议了。如图所示,假设连接器须要连接三个目标文件:


连接器会依次扫描每个给定的目标文件,同时连接器还维护了两个集合,一个是已定义符号集合D,另外一个是未定义符合集合U,下面是连接器进行符合决议的过程:


1,对于当前目标文件,查找其符号表,并将已定义的符号并添加到已定义符号集合D中。


2,对于当前目标文件,查找其符号表,将每个当前目标文件引用的符号与已定义符号集合D进行对比,若是该符号不在集合D中则将其添加到未定义符合集合U中。



3,当全部文件都扫描完成后,若是为定义符号集合U不为空,则说明当前输入的目标文件集合中有未定义错误,连接器报错,整个编译过程终止。


上面的过程看似复杂,其实用一句话归纳就是只要每一个目标文件所引用变量都能在其它目标文件中找到惟一的定义,整个连接过程就是正确的。


若是你以为上面的解释比较晦涩的话,你也能够将连接符号决议这个过程想象成以下的游戏:

新学期开学后,幼儿园的小朋友们都带了礼物要和其它的小朋友们分享,同时每一个小朋友也有本身的心愿单,每一个小朋友均可以依照本身的心愿单去其它的小朋友那里拿礼物,整个过程结束后,每一个小朋友都能拿到本身想要的礼物。

在这个游戏当中,小朋友就比如目标文件,每一个小朋友本身带的礼物就比如每一个目标文件的已定义符号集合,心愿单就比如每一个目标文件中未定义符号的集合。



实例说明undefined reference


假设咱们写了一个math.c的数字计算程序,其中定义了一个add函数,该函数在main.c中被引用到,那么很简单,咱们只须要在main.c中include写好的math.h头文件就可使用add函数了,如图所示:


可是因为粗枝大叶,一不当心把math.c中的add函数给注释掉了,当你在写完main.c、打算很潇洒的编译一下时,出现了很经典的undefined reference to `add(int, int)`错误,如图所示:


这个错误实际上是这样产生的:

1, 连接器发现了你写的代码math.o中引用了外部定义的add函数(不要忘了,这是经过检查目标文件math.o中的符号表获得的信息),因此连接器开始查找add函数究竟是在哪里定义的。


2,连接器转而去目标文件math.o的目标文件符号表中查找,没有找到add函数的定义。


3,连接器转而去其它目标文件符号表中查找,一样没有找到add函数的定义。


4,连接器在查找了全部目标文件的符号表后都没有找到add函数,所以连接器中止工做并报出错误undefined reference to `add(int, int)',如上图所示。


所以若是你很清楚连接器符号决议这个过程的话就会进行以下排查:

1:main.c中对add函数的函数名有没有写正确。

2:连接命令中有没有包含math.o,若是没有添加上该目标文件。

3:若是连接命令没有问题,查看math.c中定义的add函数定义是否有问题。

4:若是是C和C++混合编程时,确保相应的位置添加了extern "C"。


通常状况下通过这几个步骤的排查基本可以解决问题。

因此当你再次看到undefined reference这样的错误的是时候,你就应该能够很从容的去解决这类问题了。


接下来咱们讲解一下连接器的第二个工做过程,库与可执行文件的生成。


《完全理解连接器:三,库与可执行文件的生成》,欢迎关注微信公众号,码农的荒岛求生,获取更多内容。



本文分享自微信公众号 - 码农的荒岛求生(escape-it)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索