符号决议

在这个过程当中,链接器需要做的工作就是确保所有目标文件中的符号引用都有唯一的定义。要想理解这句话我们首先来看看一个典型的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这样的错误的是时候,你就应该可以很从容的去解决这类问题了。

接下来的内容我会在以下几篇文章当中一一介绍:
彻底理解链接器:三,库与可执行文件
彻底理解链接器:四,重定位

如果你喜欢这一系列的文章,也欢迎关注我的微信公共账号,码农的荒岛求生,获取更多内容。

这个系列完整的文章目录:

彻底理解链接器:二,符号决议相关推荐

  1. 链接器相关的一些基本问题

    链接器相关的一些基本问题 学习或者了解链接器,有一些基本的问题需要关心:链接器做些什么:链接器和体系结构:程序是怎样生成的.下面做简要介绍. 链接器做些什么 链接器之所以存在或者产生,基本上是由于程序 ...

  2. 【软件开发底层知识修炼】九 链接器-可重定位文件与可执行文件

    上几篇文章学习了Binutils辅助工具里面的几个实用的工具,那些工具对于以后的学习都是非常有帮助的,尤其是C语.C++语言的学习以及调试是非常有帮助的.点击链接查看上一篇文章:点击查看 本篇文章开始 ...

  3. 链接器ld程序的脚本文件u-boot.lds(二)

    目的:了解链接器用到的脚本文件u-boot.lds. 在开始这篇博文之前,需要先了解一些GNU linker script的基本知识,可以参考博主的另外一篇分享<GNU linker scrip ...

  4. 链接器:绑定符号到地址上

    目录 为什么是编译器 LLVM和LLD LLVM 的编译​​​​​​​的几个主要过程 链接器做了什么 符号链接 动态库链接 实操演示动态库链接​​​​​​​         链接器最主要的作用,就是将 ...

  5. 链接器怎样使用静态库来解决符号引用

    链接器在根据命令行中输入的可重定位目标文件和静态库的顺序从左到右的扫描这些文件.在这个扫描中,链接器会维护一个集合E,该集合包含了将来要被合并生产可执行文件的所有可重定位目标文件:维护了一个集合U,包 ...

  6. C编译器、链接器、加载器详解

    一.概述 C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接.编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程.链接是把目标 ...

  7. 链接器 --- Linker

    链接器 1. 背景 ​ 对于经常使用 IDE 的开发者,通常点击一个按钮就万事大吉了,这虽然极大简化了过程,但是对于我们C语言这些相对底层的开发者来说非常非常不友好,屏蔽了大量细节,不了解内部细节是非 ...

  8. 链接器(linker)的作用——CSAPP第7章读书笔记

    首先说说我为什么要去读这一章.这个学期开OS的课,在Morden Operating System上读到和Process有关的内容时看到这样一句话:"Process is fundament ...

  9. 【软件开发底层知识修炼】十一 链接器-链接脚本

    上一篇文章学习了链接器之-main函数不是第一个执行的函数:main函数不是第一个执行的函数 今天继续学习链接器,学习链接是如何动作的,从而引入链接脚本的概念.本文就学习链接脚本的概念. 1.链接脚本 ...

最新文章

  1. oracle判断非空并拼接,oracle sql 判断字段非空,数据不重复,插入多跳数据
  2. 动画演示10个有趣但毫无用处的Linux命令
  3. 小模型实现大一统!FBNetV5一举包揽CV任务3个SOTA
  4. Tomcat容器、JSP和Servlet
  5. 转换预定义的字符为html实体,php把一些预定义的 HTML 实体转换为字符。
  6. VB表格控件总览与例程分析
  7. Cdn间隙性故障总结
  8. Python爬虫之selenium爬虫,模拟浏览器爬取天猫信息
  9. 施密特:乔布斯影响力还没有完全释放
  10. Windows读写硬盘
  11. Android自定ViewGroup实现流式布局
  12. 新应用从哪几个方面开展ASO优化工作,aso优化内容
  13. Logistic 回归的决策边界
  14. 微信公众号自定义菜单创建
  15. 一本通 1273:货币系统
  16. 微信小程序开发者工具的使用
  17. 自学篇之如何高效地学习网络安全
  18. 自相关和相关的物理意义
  19. 每一步详细介绍—在HM中进行CU PU TU划分以及PU最佳预测模式选择
  20. 使用nw.js将vue项目打包为可在xp系统运行的桌面程序

热门文章

  1. 常州彪马机器人_PUMA560型机器人
  2. add git 指定类型文件夹_UE4 使用git配合远程仓库
  3. stata主成分分析_主成分分析在STATA中的实现
  4. 朋友圈广告助手_腾讯社交广告代理附近推跟朋友圈广告对比优势
  5. SpringBoot启动流程分析(四):IoC容器的初始化过程
  6. DataTable的Merge\COPY\AcceptChange使用说明
  7. 服务器端Session和客户端Session
  8. django 函数装饰器 变为 类装饰器
  9. 示范对外接口参数文档
  10. angular directive自定义指令