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

  学习或者了解链接器,有一些基本的问题需要关心:链接器做些什么;链接器和体系结构;程序是怎样生成的。下面做简要介绍。

链接器做些什么

  链接器之所以存在或者产生,基本上是由于程序开发的模块化。这里讲的模块,主要是编译概念上的模块,通常他们按照功能划分,比如一个.c或者.cpp文件就是一个编译单元,就是一个模块,编译后就产生一个.o目标文件。为了最终生成一个可执行文件、静态库或者动态库,就需要把各个编译单元按照特定的约定组合到一起。这里特定的约定指的就是“目标文件格式”,它定义了目标文件、库文件和可执行文件的格式,这里组合这一过程就叫做链接。
  一个编译模块中,通常是函数的定义和全局数据的定义,数据类型的定义通常在头文件中,编译时会被包含在编译模块中。函数和数据由符号来标识,一般符号有全局和静态之分,全局符号可以被其他模块引用,而静态符号只能在本模块中引用。编译各个模块时,编译器会解析该模块。重要的一项工作就是建立符号表,符号表中包含了本模块有哪些符号可以被其他模块引用(导出符号),还包括本模块引用(导入符号,即未定义符号)、但在其他模块中定义的符号。每一个符号都关联一个地址,这个地址指明了该符号在本模块中的偏移地址(通常是一个从0开始的地址)。
  链接器在链接过程中,会扫描各个模块的符号表,得到一个“全局符号表”,链接器由此决定一个符号在哪里被定义,在哪里被引用。并且,将符号引用处替换为定义处的地址,这一过程就叫做符号解析。
  链接器的一项终极目标就是生成可执行文件。通常,可执行文件和普通目标文件的重要区别就是地址空间的使用。主流操作系统中,可执行文件都是基于虚拟地址空间的,即每个可执行文件都有相同且独立的地址空间,并且文件中各个段(代码段,数据段,以及进程空间中的堆栈段)都有相似的布局。而普通目标文件却使用从零开始的地址空间,这样一来,模块M中的符号m就可能和模块N中的符号n拥有“相同”的地址。在链接器链接各个模块时,会从各个模块中“提取”类型相同的段进行合并,并将合并后的段写入可执行文件中。这一过程被称为存储空间的分配。值得一提的是,栈、堆以及未初始化的数据这些“运行时”需要的空间不会在可执行文件中占据磁盘空间,但它们占用相应的地址空间。
  由于存在上述“合并”过程,前面提到的符号解析就涉及到另外一个过程:重定位。由于各个模块中的函数/数据地址会被重新排放,那么对这些符号的引用也必须被相应地调整。这一调整过程被称作重定位。
  符号解析,存储空间分配,还有重定位,这三个过程是一个有机的整体,是“同时”进行的,且这三个过程也是模块化所带来的必须要解决的问题。

链接器和体系结构

  在我们编写普通应用程序时,不需要过多的关系体系结构的问题,但对于链接器学习者/编写者来说,相当大的工作都必须围绕体系结构展开,比如目标平台的ABI,内存地址,指令格式,寻址方式等等,下面做大致介绍。
  一个体系结构的ABI,即二进制程序接口,主要包括硬件和操作系统两个层面。内存字长,对齐方式,子程序调用时的约定(参数传递方式,返回值传递方式),系统调用如何进行,目标文件格式等等,都属于ABI的范畴,都是链接器工作时要密切关注的问题。
  另外,多字节数据的字节序也是链接器需要考虑的。字节序是关于如何使用线性的连续字节来表示多字节数据的问题,有Little-endian和Big-endian两种字节序。小端序是指将多字节数据的低权重字节放在内存的低地址处,大端序则正好相反。直观讲,从内存的低地址向高地址方向看,先看到多字节数据的低权重字节的就是小端序,否则就是大端序。至于为什么会存在两种字节序,两者有何优劣,我觉得这只是个“个人喜好”问题,就好象剥鸡蛋先磕破哪一头,蹲厕所时脸里还是脸朝外一样,事实上,直到前些天,我才亲眼看到,真的是有“茅房拉屎脸朝内”的人的。
  指令格式和寻址方式也会影响链接器的工作,因为在符号重定位的时候,链接器需要修改指令中操作数部分,所以需要知道每种指令的指令格式及寻址方式,以便对指令做出适当的调整。

程序是怎样生成的

  事情渐渐明了了,编译器前端对语言进行词法、语法分析,建立语法树,编译器后端在语法树的基础上针对特定的平台生成指令,并按照特定的格式输出到磁盘中的目标文件。链接器按照前面所说的过程生成最终的可执行文件或程序库。(这里的程序库特指动态库,因为静态库只是目标文件的简单集合,理论上不需要链接器的参与)本文只针对链接器进行简单的讨论,关于编译器的功能和相关原理,有大量的资料可以参考。

目标文件格式

  目标文件格式是指令、数据在磁盘中存储形式的一种约定。它描述了指令、数据的存储格式和布局,并且针对不同类型的文件(普通目标文件,可执行文件,动态库)有不同描述侧重点。另外它还描述了一些供外部程序使用的元信息,例如普通目标文件中的符号表的内容和组织形式,待重定位符号的信息;可执行文件中各个段的信息,程序的入口点(程序从何处开始执行),哪些符号需要在运行时解析,这些符号包含在哪些动态库中,以及解析这些库需要哪种动态链接器等等;动态库为了实现同时被多个进程链接需要什么样的组织形式,本身又引用了其他动态库的哪些符号等等。通常,不同的平台都会有自己的目标文件的格式标准:

  • COM:DOS最初采用的一种非常简单的格式,除了指令、数据之外,基本不包含其他信息。
  • PE:Windows当前采用的目标格式,继承自COFF,是一种主流的现代目标格式,相比COM有更强大的功能支持。
  • a.out:最初UNIX平台采用的目标格式,简单且功能强大,但对于C++这样的高级语言支持不足。
  • ELF:当前Linux/Unix平台采用的主流格式,继承自a.out,且对高级语言支持很友好。

  除了ABI,目标格式的不同,也是在一种操作系统下编译的程序无法在另一种操作系统中执行的原因。

程序库

  终于到库了,研究库很有趣,也相当实用。概念上,库可以分为静态库,动态库,且这两种库都可以实现为“共享库”,但在实现上,静态共享库由于需要考虑态度的问题、实现太过复杂且得不偿失,现实中很少有这种类型的库。所以,应用中只存在两种库:静态库和动态共享库,下面分别做简要介绍,关于在Linux中如何创建这两种库,可以参考我之前写的一篇博客,或者其他更详尽更优秀的资料。

静态库

  在功能特性上,静态库是指这样一种库,在链接时,其中被引用的代码、数据被“复制”到引用该库的程序中。在格式上,静态库十分简单,他是普通目标文件的集合,是一种简单的拼接。事实上,Linux平台下静态库.a文件使用独立的归档工具ar建立,为了使链接器能够有效地查找库中包含的各个目标文件以及符号,经常还需要一个叫做ranlib的工具在.a文件中建立索引。
  在链接时,链接器想普通目标文件一样使用静态库,仅仅多了在库中查找符号及对应目标文件的过程。

动态链接库

  动态链接库和静态库差异较大,Linux平台,它由ELF格式直接支持。但由于共享库的特殊性,它需要一些特殊特性的支持:
  PIC(Position Independent Code),位置无关代码。动态共享库需要在运行时动态地加载到内存,为了在各个进程中调用共享库中的代码和数据,就需要将该库映射到不同进程的进程空间。由于各个进程的地址空间使用情况不尽相同,很难将共享库映射到各进程相同的位置。这样一来,就对共享库的代码提出了挑战,它需要能够在不同的地址区间上都正确的执行。位置无关代码就是因此而提出的。
  位置无关代码的基本思想是这样的:将共享中对绝对地址的引用转化为相对地址的形式。对于函数调用,实现起来很简单,因为代码是只读的,指令间的相对地址也是固定的,只需要将函数调用转化会相对地址即可。但对于数据的引用就复杂很多了,由于各个进程都需要访问共享库中的数据,而这些进程通常是毫无关联的,一个进程对共享库数据的修改不应该被其他进程看到。一种好的方法就是让各个进程都拥有自己的一份数据拷贝。但这又引出一个问题,共享库是被动态映射的,数据空间也只能在映射时才需要分配,那么在共享库代码中如何引用这些数据,以达到不同进程使用相同的代码访问不同的数据呢?
  于是另外一种结构被引入了,即GOT(Global Offset Table),全局偏移量表。它的基本思想也是相对地址引用。在共享库的数据段加入GOT,GOT的表项保存各个数据的地址。由于共享库中指令和数据段的相对地址在链接后是固定的,这样在指令中就可以使用相对地址来找到GOT的起始地址,然后根据各个数据在GOT的偏移量来找到其对应的地址。而该地址是在共享库被映射到进程空间时,由动态链接器在相应的进程空间中分配并设置的。
  接下来的问题就是,进程中如何调用共享库中的代码呢?ELF使用一种延迟加载的方法,即当进程调用共享库中一个函数时,才解析该函数的地址,且只有第一次才解析,第一次解析之后的调用就不会被再次解析,而是将之前解析到的地址保存下来。这里又引入一种机制,叫做PLT(Procedure Linkage Table),它和GOT一起(使用共享库的进程也有一个GOT)引入了一个函数调用的间接层。
  类似共享库的GOT,进程的GOT表项保存了本进程引用的共享库中的函数地址,但在第一次对该函数的调用之前,该表项保存的并不是函数的地址,而是指向PLT中一个指令的地址。为了方便说明问题,假设进程中main函数引用了libmath.so中的函数add,那么PLT大致是这个样子的,

1
2
3
4
5
6
7
8
9
10
11
12
13
.plt0:0x080483d0: pushl  0x8049ff80x080483d6: jmp    *0x8049ffc
...
add@plt:0x080483e0 <+0>:   jmp    *0x804a0000x080483e6 <+6>:   push   $0x00x080483eb <+11>:    jmp    0x80483d0
...
main:
...0x080484ec <+24>: call   0x80483e0 <add@plt> # 对add函数的调用
...

  第十二行对add函数的调用跳转到第五行,这是一条jmp指令,0x804a000是它的地址操作数,该地址即是add在GOT中的地址项,最初该地址处保存的地址是0x080483e6,即jmp指令的下一条push指令。于是最初jmp指令执行后没有任何效果,直接执行下一条指令。push指令将add在重定位项的索引入栈,通过该重定位项可以得到add符号本身(即字符串add)。然后又是一条jmp指令它跳转到第二行,又是一个push指令,接下来又是jmp指令,这个jmp指令也使用了GOT中的一个表项,该表项存储的是动态链接器(ld.so)的加载/解析函数。在解析函数中,查找add符号在共享库中的地址,将该地址填入add对应的GOT表项,然后跳转至add函数开始执行。到下一次调用add函数,第五行的jmp指令就直接跳转到add函数了。
  动态链接基本就是这个过程了。在这个过程中有许多有意思的指令,将栈玩弄于鼓掌,像变魔术一样,有兴趣的话可以使用gdb等相关工具调试一下。

一些工具

  玩弄二进制,很多实用工具是离不了的,最重要的就是GNU Binutils二进制工具链了。包括查看ELF文件信息的readelf,对目标文件、可执行文件、共享库、core内存转储文件等反汇编的objdump,重量级的调试器gdb,查看共享库使用情况的ldd等等。

一些参考资料

  了解链接器工作原理,现成的资料并不多,《Linkers and Loaders》算是经典了,中文版也可以买得到,翻译得还算中规中矩。另外,《程序员的自我修养:链接、装载与库》写的很浅显,不错的一本书。
  为了更好的理解链接器,必须对ELF的细节有所了解,Executable and Linkable Format这份文档可以参考。
  研究二进制,汇编知识是必须的,如果是Linux平台,了解些GNU 汇编(AT&T汇编)是再好不过了,不过讲解GNU汇编的资料更是少上加少,布鲁姆的《汇编语言程序设计》虽然内容不多,但对非专业汇编程序员也足够用了,这本书是有英文电子版的。
  有了相关工具和入门资料,剩下的就是折腾了。
  最后,附上之前做的幻灯片,就像这篇文章一样,臭长,枯燥

链接器相关的一些基本问题相关推荐

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

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

  2. 深入探究VC —— 链接器link.exe(4)【转】http://blog.csdn.net/wangningyu/article/details/4849452...

    在程序编译完成后,生成的文件是以.obj为扩展名的对象文件,link.exe是将这些对象文件与库链接起来以创建可执行文件或动态链接库文件的工具. link.exe的输入文件包括obj文件.lib文件. ...

  3. 【软件开发底层知识修炼】十三 链接器-如何写出不依赖C库函数的代码

    本文将综合以下4篇文章,学习如何写出不依赖libc库的程序: [软件开发底层知识修炼]九 链接器-可重定位文件与可执行文件 [软件开发底层知识修炼]十 链接器-main函数不是第一个被执行的函数 [软 ...

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

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

  5. 【软件开发底层知识修炼】十 链接器-main函数不是第一个被执行的函数

    上一篇文章,大概了解了链接器的工作内容就是:符号解析和重定位.点击上一篇文章查看:点击查看. 本片文章其实还是围绕链接器来学习.只不过不是很明显,当你学到下一篇文章时,就明白了. 本篇文章来弄明白一个 ...

  6. C++编译器与链接器工作原理

    原文地址 这里并没不是讨论大学课程中所学的<编译原理>,只是写一些我自己对C++编译器及链接器的工作原理的理解和看法吧,以我的水平,还达不到讲解编译原理(这个很复杂,大学时几乎没学明白). ...

  7. u-boot器件驱动模型(DeviceDrivers)之链接器的秘密

    写在前面 哈哈,写东西前总喜欢先扯蛋,赶时间的直接无视这段吧.前段时间照着x_project成功的将手上的一块基于nuc972的板子成功移植上了最新的u-boot,相关细节可以参考我的这篇博客. 那篇 ...

  8. C++链接器工具错误:LNK2001, LNK2019

    这是归属于链接器工具错误 这一类. 无法解析的外部符号"symbol" 代码引用了链接器无法在库和对象文件中找到的内容(如函数.变量或标签). 可能的原因 代码请求的内容不存在(例 ...

  9. 链接器、链接过程及相关概念解析

    文章目录 1. 编译器驱动程序 2. 目标文件 2.1 可重定位目标文件(.o) 2.2 可执行目标文件(无后缀) 2.3 共享目标文件(.dll和.so) 3. 链接器的任务 3.1 符号解析(sy ...

最新文章

  1. Python基础——数据分析考核(图形化版本)
  2. 阮一峰:Flex 布局教程
  3. stm32串口传输数据第一个数据被吞_STM32串口第一个字节丢失问题的分析过程
  4. axure form列表_常见的列表页如何用Axure画出来
  5. 梆梆加固还原DEX文件
  6. Hook入门之鼠标钩子
  7. moment.js计算时间差
  8. 更换主板后 Win10 账户密钥集不存在 0x80090016问题解决
  9. 检查日期是否为节假日api
  10. 【解决方案】网络异常流量突发分析
  11. bmp格式如何转换成png格式
  12. 【报告分享】 2020年中国汽车用户消费洞察白皮-懂车帝巨量算数(附下载)
  13. HTML中的 DOM 是什么?有什么作用?
  14. 智能网联先导区道路交叉口车路协同系统设计
  15. java勇者大冒险_文字冒险页面游戏-java用到了类,循环等
  16. 蛋糕店会员卡充值方案有哪些?
  17. WordPress集成ckplayer播放器教程
  18. 百度收录网站,主动推送(Python)
  19. JAVA在IDEA中用JNA调用C++的dll动态链接库案例
  20. 触摸屏驱动:坐标转换

热门文章

  1. 风变编程python助教_花30天时间,学完了风变编程Python基础语法课
  2. 用友t3服务器文件丢失,用友T3软件在T3用友通标准版恢复账套时在备份的文件中找不到要恢复的文件,如何解决?-用友T3...
  3. java 12错误_Java异常处理的12条军规总结
  4. hadoop 环境搭建
  5. Doctype的作用
  6. Base64 的那些事儿
  7. Strom小实例,大小写转换
  8. HDU_5249(百度之星D题)
  9. 重新想象 Windows 8 Store Apps (4) - 控件之提示控件: ProgressRing; 范围控件: ProgressBar, Slider...
  10. 同事就是同事,职场没有兄弟姐妹