《2022年2022年链接器和加载器[参 .pdf》由会员分享,可在线阅读,更多相关《2022年2022年链接器和加载器[参 .pdf(11页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、第 9章 共享库$Revision: 2.3 $ $Date: 1999/06/15 03:30:36 $ 程序库的产生可以追溯到计算技术的最早期,因为程序员很快就意识到通过重用程序的代码片段可以节省大量的时间和精力。随着如Fortran and COBOL 等语言编译器的发展,程序库成为编程的一部分。当程序调用一个标准过程时,如sqrt(),编译过的语言显式地使用库,而且它们也隐式地使用用于I/O 、转换、排序及很多其它复杂得不能用内联代码解释的函数库。随着语言变得更为复杂,库也相应地变复杂了。当我在20年前写一个 Fortran 77编译器时,运行库就已经比编译器本身的工作要多了,而一个F
2、ortran 77库远比一个 C+库要来得简单。语言库的增加意味着:不但所有的程序包含库代码,而且大部分程序包含许多相同的库代码。例如,每个C程序都要使用系统调用库,几乎所有的C程序都使用标准 I/O 库例程,如 printf,而且很多使用了别的通用库,如 math , networking,及其它通用函数。这就意味着在一个有一千个编译过的程序的UNIX系统中,就有将近一千份printf的拷贝。如果所有那些程序能共享一份它们用到的库例程的拷贝,对磁盘空间的节省是可观的。(在一个没有共享库的 UNIX系统上,单 printf的拷贝就有 5到 10M。)更重要的是,运行中的程序如能共享单个在内存中
3、的库的拷贝,这对主存的节省是相当可观的,不但节省内存,也提高页交换。所有共享库基本上以相同的方式工作。在链接时,链接器搜索整个库以找到用于解决那些未定义的外部符号的模块。但链接器不把模块内容拷贝到输出文件中,而是标记模块来自的库名,同时在可执行文件中放一个库的列表。当程序被装载时,启动代码找到那些库,并在程序开始前把它们映射到程序的地址空间,如图 1。标准操作系统的文件映射 机制自动共享那些以 只读或 写时拷贝的映射页。 负责映射的启动代码可能是在 操作系统中, 或在可执行体,或在已经映射到 进程地址空间的 特定动态链接器中, 或是这三者的某种并集。-图 9-1:带有共享库的程序可执行程序,共
4、享库的 图例可执行程序 main , app 库,C 库不同位置来的文件箭头展示了从 main到 app, main 到 C, app到 C的引用名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 1 页,共 11 页 - - - - - - - - - -在本章,我们着 眼于静态链接库,也就是库中的程序和数据地址在链接时 绑定到可执行体中。在 下一章我们着 眼于更复杂的动 态链接库。 尽管动态链接更 灵活更“现代”,但也比静态链接要 慢很多,因为在链接时要 做的大量工作在每 次启动动
5、态链接的程序时要重 新做。同时,动 态链接的程序通 常使用额外的“胶合(glu e) ”代码来调用共享库中的例程。胶合代码通常包含若干个跳转,这 会明显地减慢调用速度。在同时 支持静态 和动态共享库的系统上,除非程序需要动态链接的 额外扩展性,不然使用静态链接库能使它们更快更 小巧。绑定时间共享库提出的 绑定时间 问题,是常规链接的程序不 会遇到的。一个用到了共享库的程序在运行时 依赖于这些库的有 效性。当所 需的库不存在时,就 会发生错误。在这 情况下,除了打印出一个 晦涩的错误信息 并退出外,不 会有更多的 事情要做。当库已经存在,但是自 从程序链接以来库已经 改变了时,一个更有 趣的问题
6、就会发生。在一个 常规链接的程序中,在链接时符号就被绑定到地址上而库代码就已经绑定到可执行 体中了,所以程序所链接的库是那个忽略了随后变更的库。对于 静态共享库,符号在链接时被绑定到地址上,而库代码要 直到运行时 才被绑定到可执行 体上。(对于动 态共享库而言,它们都推迟到运行时。) 一个静态链接共享库不能 改变太多,以 防破坏 它所绑定到的程序。因为例程的地址和库中的数 据都已经 绑定到程序中了, 任何对这些地址的 改变都将 导致灾难 。名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - -
7、第 2 页,共 11 页 - - - - - - - - - 如果不 改变程序所 依赖的静态库中的 任何地址,那 么有时一个共享库就可以在不影响程序对它调用的前提 下进行升级。这就是通 常用于小 bug修复的 小更新版。更大的 改变不可避免地要改变程序地址,这就意味着一个系统要么需要多个 版本的库,要 么迫使程序员在每次改变库时都重 新链接它们所有的程序。 实际中,永远不变的解决 办法就是多 版本,因为磁盘空间 便宜,而要找到每个 会用到共享库可执行 体几乎是不可能的。 实际的共享库本章余下的部分将 关注于 UNIX System V Rel ease 3.2 (COFF 格式 ) ,较早的
8、Lin ux 系统 (a.o ut 格式 ) ,和4. 4BSD的派生系统 (a.out 和 ELF格式 ) 这三者提供的静态共享库。这 三者以几近相同的方式工作,但有些不同点具有启发意义。 SV R3.2 的实现要求改变链接器以 支持共享库搜索,并 需要操作系统的 强力支持以满足例程在运行时的启动 需求。 Linux 的实现需要对链接器 进行一点小的调整并增加一个系统调用以辅助库映射。B SD/OS的实现不对链接器或操作系统作 任何改 变,它使用一个 脚本为链接器提 供必要的参数和一个 修改过的标准 C库启动例程以映射到库中。 地址空间 管理共享库中最 困难的就是地址空间 管理。每一个共享库
9、在使用它的程序里都占用一段 固定的地址空间。不同的库,如果能够被使用在同一个程序中,它们还必须 使用互不重叠的地址空间。 虽然机械 的检查库的地址空间是 否重叠是可能的,但是 给不同的库 赋予相应的地址空间仍然是一种“魔法” 。一方 面,你还想 在它们之间留一些余地,这 样当其中某个新版本的库增 长了一些时,它不 会延伸 到下一个库的空间而发生 冲突。另一方面,你还想 将你最常用的库 尽可能紧密的放在一 起以节省 需要的页表数量(要 知道在 x86上,进程地址空间的每一个 4MB 的块都有一个对应的 二级表)。每个系统的共享库地址空间都必然有一个主表,库 从离应用程序很远的地址空间开始。Lin
10、 ux从十六进制 的 60000000开始,B SD/O S从 A0000000开始。 商业厂家 将会为厂家提供的库、用 户和第三方库进一步细分地址空间,比如对BSD/O S,用户和第三方库开始于地址 A0800000 。通常库的代码和数 据地址都 会被明确的定义,其中数 据区域从 代码区域结束 地址后的一个或两个页对 齐的地方开始。 由于一般都不会更新数据区域 的布局,而只是增加 或者更改代码区域,所以这 样就使小更新版本成为可能。每一个共享库都 会输出符号,包 括代码和数 据,而且如果这个库 依赖于别的库,那 么通常也会引入 符号。虽然以某种偶然 的顺序将例程链接为一个共享库也能使用,但是
11、真正的库使用一些分 配地址的 原则而使得链接更容 易,或者至少 使在更 新库的时 候不必修改 输出符号的地址成为可能。对于代码地址,库中有一个可以跳转到所有例程的 跳转指令表,并将这些跳转的地址作为相应例程的地址输出,而不是输出这些例程的实际地址。所有 跳转指令的大小都是相同的,所以 跳转表的地址很容 易计算,并且 只要表中不在库更 新时加入或删除 表名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 3 页,共 11 页 - - - - - - - - - 项,那么这些地址将不 会随版
12、本而改变。每一个例程多出一 条跳转指令不会明显的降低速度 ,由于实际的例程地址是不可 见的,所以 即使新版本与旧版本的例程大 小和地址都不一 样,库的新旧版 本仍然是可兼容的。对于输出数 据,情况就要复杂一些,因为没有一种像对代码地址那 样的简单方 法来增加一个间接 层。实际中的输出数 据一般是很少变动的、 尺寸已知的表,例如 C标准 I/O 库中的 FILE结构,或者像 errno 那样的单字数值(最近一 次系统调用 返回的错误代码), 或者是 t zname( 指向当前时 区名称的两个字符串的指针)。建立共享库的程序员可以 收集到这些输出数 据并放置在数据段的开 头,使它们 位于每个例程中
13、所使用的 匿名数据的前面,这样使得这些输出地址在库更 新时不太可能会有变化。共享库的 结构共享库是一个包含所有准 备被映射的库代码和数 据的可执行 格式文件, 见图9-2。-图 9-2: 典型共享库的 结构文件头, a.out , COFF 或 ELF头(初始化例程,不 总存在)跳转表代码全局数据私有数据-一些共享库 从一个小的自举例程开始,来映射库的 剩余部分。 之后是跳转表,如果它不是库的第一个内容,那 么就把它对 齐到下一个页的 位置。库中每一个输出的 公共例程的地址就是 跳转表的表 项;跟在跳转表后面的是文本段的 剩余部分(由于跳转表是可执行代码,所以它被 认为是文本) , 然后是输出
14、数 据和私有数据。在逻辑上 bss 段应跟在数据的后面,但是就 像在任何别的可执行文件中那 样,它并不在于这个文件中。创建共享库一个 UNIX共享库 实际上包含 两个相关文件, 即共享库本身和 给链接器用的空 占位库 (stub l i brar y) 。库创建工具将一个 档案格式的普通库和一些包含 控制信息 的文件作为输 入生成了这 两个文件。空 占位库根本不包含 任何的代码和数 据( 可能会包含一个 小的自举例程 ) ,但是它包含程序链接 该库时需要使用的符号定义。创建一个共享库 需要以下几步,我们将在 后面更多的 讨论它们:确定库的代码和数 据将被定 位到什么地址。彻底扫描 输入的库寻找
15、所有输出的代码符号(如果 某些符号是用来在库内通 信的,那么就会有一个 控制文件是这些不对外输出的符号的列表) 。名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 4 页,共 11 页 - - - - - - - - - 创建一个跳转表,表中的每一 项分别对应每个输出的代码符号。如果在库的开 头有一个 初始化或加载例程,那 么就编译 或者汇编它。创建共享库。运行链接器把所有内容都链接为一个大的可执行格式文件。创建空占位库:从刚刚建立的共享库中提 取出需要的符号, 针对输入库的符号调整这
16、些符号。为每一个库例程创建一个空 占位例程。在 COFF 库中,也 会有一个 小的初始化代码放在 占位库里并被链接到每一个可执行体中。创建跳 转表最简单的 创建一个跳转表的方 法就是编写一个 全是跳转指令的汇编源代码文件,如 图 3,并汇编它。这些 跳转指令需 要使用一 种系统的方 法来标记,这 样以后空占位库就能 够把这些地址提出 取来。对于像 x86 这样具有多种长度的跳转指令的平台,可能 稍微复杂一点。对于含有 小于 64K代码的库,3 个字节的短跳转指令就足够了。对于 较大的库, 需要使用更 长的 5字节的跳转指令。将不同 长度的跳转指令混在一起是不能 让人满意的,因为它使得表地址的计
17、算更加困难,同时也更 难在以后重建库时确保兼容性。最简单的解决方 法就是都 采用最长的跳转指令;或者全部都使用 短跳转,对于那些使用 短跳转太远的例程, 则用一个 短跳转指令跳转到放在表 尾的匿名长跳转指令。 ( 通常由此带来的麻烦比它的 好处更多,因为第一 跳转表很 少会有好几百项。)-图 9-3:跳转表. 从一个页 边界起始.a l ign 8; 为了变量长度而对其于 8字节边界处JUM P_ read: j mp _read.a l ign 8JUM P_ write: j mp _write._read: . code for read()._write: . code for wri
18、te()-创建共享库一旦跳转表和加载例程( 如果需要的话) 建立好之后,创建共享库就很容 易了。只需要使用合适的参数运行链接器, 让代码和数 据从正确 的地址空间开始,并将自 引导例程、跳转表和输 入库中的所有例程都链接在一起。它同时 完成了给库中每 项分配地址和 创建共享库文件两件事。名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 5 页,共 11 页 - - - - - - - - - 库之间的引用会稍微复杂一些。如果 你正在创建,例如一个使用标准C库例程的共享数学库,那就要 确
19、保引用的正确。假定当链接器 建立新 库时需要用到的共享库中的例程已经建好,那么它只需要搜索 该共享库的空 占位库,就 像普通的可执行程序 引用共享库那 样。这将让所有的 引用都正确。只留下 一个问题,就是 需要有某种方法确保任何使用新库的程序也能够链接到 旧库上。对 新库的空 占位库的适当设计可以 确保这一点。创建空占位库创建空占位库是创建共享库过程中 诡秘的部分 之一。对于库中的每一个例程,空占位库中都要包含一个同时定义了输出和输入的全局符号的对应 项。数据全局符号会被链接器放在共享库中 任何地方, 获取它们的数 值的最合理的办法就是创建一个带有符号表的共享库,并 从符号表中提 取符号。对代
20、码 全局符号, 入口指针都在跳转表中,所以同 样很简单, 只需要从共享库中提 取符号表 或者根据跳转表的基地址和每一个符号在表中的 位置来计算符号地址。不同于 普通库模块,空 占位库模块 既不包含代码也不包含数 据,只包含符号定义。这些符号 必须定义成 绝对数而不是相对,因为共享库已经完成了所有的重定 位。库创建程序从输入库中提 取出每一个例程,并 从这些例程中得到定义和未定义的全局变量,以及每一个 全局变量的 类型( 文本或数据) 。然后它创建空占位例程,通 常都是一个很 小的汇编程序,以 跳转表中每一 项的地址的 形式定义每个文本 全局变量,以共享库中 实际地址的 形式定义每个数据或 bs
21、s 全局变量,并以 “未定义 ”的形式定义没有定义的 全局变量。当它 完成所有空 占位后,就对其 进行汇编并将它们 合并到一个 普通的库 档案文件中。COFF 空占位库使用了一 种不同的、更简单的 设计。它们是 具有两个命名段的单一 目标文件。 “. l i b”段包含了 指向共享库的所有重定 位信息 ,“.init”段包含了将 会链接到每一个客户程序去的初始化代码,一 般是来初始化库中的变量。Linux 共享库更简单,a.out 文件中包含了 带有设置向量(“set ve ctor ”) 的符号定义, 我们将在后面的链接部分 详细讨论设 置向量。.共享库的名 称一般是原先的库名加上 版本号。
22、如果 原先的库称为/l i b/ l i bc.a ,这通常是 C库的名 字,当前的库 版本是 4.0 ,空占位库可能是/l i b/ l i bc_s. 4.0.0.a,共享库就是/shl i b/ l i bc_s. 4.0.0(多出来的0 可以允许小版本的升级)。一 旦库被放 置到合适的目录下面,它们就可以被使用了。版本命名任何共享库系统都 需要有一 种办法 处理库的多个 版本。当一个库被更 新后,新版本相对于之前版本而言在地址和调用上都有可能兼容或不兼容。 UNIX 系统使用前 面提到的 版本命名序号来解决这个 问题。第一个数 字在每次发布一个不 兼容的全新的库的时 候才被改变。一个和
23、 4. x. x 的库链接的程序不能使用 3. x. x 或 5. x. x 的库。第 二个数是 小版本。在 Su n 系统上,每一个可执行程名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 6 页,共 11 页 - - - - - - - - - 序所链接的库都 至少需 要一个 尽可能大的 小版本号。例如,如果它链接的是4.2. x,那么它就可以和 4.3. x 一起运行而 4.1. x 则不行(译 者注:就是 说得使用 尽可能大的 小版本号, 确保可执行程序可以运行)。 另一些系统
24、将第 二个数字当作第一个数 字的扩展,这 样的话使用一个 4.2. x 的库链接的程序就 只能和 4.2. x 的库一 起运行。第 三个数字通常都被当作 补丁级别。虽然任何 的补丁级别都是可用的,可执行程序最好还是使用最高的有 效补丁级别。不同的系统在运行时 查找对应库的方 法会略有不同。 Sun系统有一个相当复杂的运行时加载器,在库 目录中查看所有的文件名并 挑选出最好的那个。Lin ux系统使用符号链接而 避免了搜索过程。如果库l i bc.so 的最新版本是 4.2.2,库的名字是 l i bc_s. 4.2.2,但是这个库也已经被链接到 l i bc_s. 4.2 ,那么加载器将 仅需
25、打开名字较短的文件,就 选好了正确的版本。多数系统都 允许共享库存在于多个 目录中。类似于 LD _LIBRAR Y_P ATH的环境变量可以 覆盖可执行程序中的 路径,以允许开发 者使用它们自 己的库替代原先的库进行调试或性能测试(使用 “set user ID ”特性 替代当前用 户运行的程序将 忽略 LD _LIBRAR Y_P ATH以避免使用恶意用户添加了安全漏洞 的“特洛伊木马 ”库)。使用共享库链接使用静态共享库来链接,比 创建库要简单得多,因为几乎所有的确保链接器 正确解析库中程序地址的 困难工作,都在 创建空占位库时完成了。唯一困难的部分就是在程序开始运行时将 需要的共享库映
26、射 进来。每一种格式都会提供一个小窍门让 链接器 创建一个库的列表,以 便启动代码把库映射进来。 COFF 库使用一 种残忍的强制方法;链接器中的 特殊代码在 COFF 文件中 创建了一个以库名命名的段。Lin ux链接器使用一 种不那么残忍的方法,即创建一个称为设置向量的特殊符号类型。设置向量象普通的全局符号一 样,但如果它有多个定义,这些定义会被放进一个以该符号命名的数 组中。每个共享库定义一个设置向量符号 _SH AR ED_LIBRARIES _,它是 由库名、 版本、加载地址等 构成的一个数 据结构 的地址。 链接器 创建一个指向每个这种数据结构的指针的数组,并称之为_SH ARE
27、D_LIBRARIES _,好让启动代码可以使用它。BSD/O S共享库没有使用 任何的此类链接器 窍门。它使用 shell脚本建立一个共享的可执行程序,用来搜索作为 参数或隐式传入的库列表,提 取出这些文件的名 字并根据系统文件中的列表来加载这些库的地址, 然后编写一个 小汇编源文件创建一个带有库名 字和加载地址的 结构数组,并汇编这个文件,把得到的 目标文件加 入到链接器的 参数列表中。在每一 种情况中,从程序代码到库地址的 引用都是通过空 占位库中的地址自动解 析的。使用共享库运行启动一个使用共享库的程序需要三步:加载可执行程序,映射库,进行库特定的初始化操作。在每一 种情况下 ,可执行
28、程序都被系统 按照通常的方法加载到内存中。 之后,处理名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 7 页,共 11 页 - - - - - - - - - 方法会有差别。系统 V.3 内核具有了处理链接 COFF 共享库的可执行程序的 扩展性能,其内核会查看库列表并在程序运行 之前将它们映射 进来。这 种方法的不利之处在于 “内核肿胀”,会给不可分页的内 核增加更多的代码 ;并且由于这种方法不允许在未来 版本中有 灵活性和可升级性 ,所以它是不 灵活的 ( 系统V. 4整个抛弃了
29、这种策略,转而采用了我们 下章会涉及到的 ELF动态共享库 ) 。Lin ux增加了一个单 独的 usel i b() 系统调用,以 获取一个库的文件名 字和地址,并将它映射到程序的地址空间中。绑定到可执行 体中的启动例程搜索库列表,并对每一项执行 usel i b() 。BSD/O S的方法是使用标准的 mmap() 系统调用将一个文件的多个页映射 进地址空间, 该方法还使用一个链接到每个共享库起始处的自举例程。可执行程序中的启动例程遍历共享库表,打开每个对应的文件,将文件的第一页映射到加载地址中,然后调用各自的自 举例程,该例程位于可执行文件 头之后 的起始页附近的某个固定位置。然后自举例
30、程再映射余下的文本段、数 据段,然后为 bss 段映射 新的地址空间, 然后自举例程就返回了。所有的段被映射了 之后,通常还有一些库 特定的初始化工作要 做,例如,将一个 指针指向 C标准库中 指定的系统 环境全 局变量 environ。 COFF的实现是从程序文件的 “.init”段收集初始化代码, 然后在程序启动代码中运行它。根据库的不同,它有时 会调用共享库中的例程,有时不 会。 Linux 的实现中没有进行任何的库初始化,并且 指出了在程序和库中定义相同的变量将不能很 好工作的 问题。在 BS D/OS实现中,C 库的自 举例程会接收到一个 指向共享库表的 指针,并将所有其它的库都映射
31、 进来,减小了需要链接到单 独的可执行 体中的代码量。最近 版本的 BSD使用 ELF格式的可执行 体。ELF头有一个 interp段,其中包含一个运行 该文件时 需要使用的解释器程序的名 字。BSD使用共享的 C库作为解释器,这意味着在程序启动之前内核会将共享 C库先映射进来,这就节省了一些系统调用的开销。库自 举例程进行的是相同的 初始化工作,将库的剩余部分映射 进来,并且,通过一个 指针,调用程序的 main例程。ma ll oc hack 和其它共享库 问题虽然静态 共享库 具有很好的性能,但是它们的 长期维护是困难和容易出错的,下面给出一些 轶事为例。在一个 静态库中,所有的库内调用
32、都被永久绑定了,所以不可能将 某个程序中所使用的库例程通过重 新定义替换为私有版本的例程。多数 情况下 ,由于很少有程序 会对标准库中例如 read()、 str cmp() 等例程进行重新定义,所以 永久绑定不是什么大问题;并且如果它们自己的程序使用 私有版本的 str cmp() ,但库例程仍调用库中标准 版本,那 么也没有 什么大问题。但是很多程序定义了它们自己的 ma ll oc() 和free() 版本,这是分 配堆存储的例程 ;如果在一个程序中存在这些例程的多个版本,那 么程序将不能 正常工作。例如,标准 strd up()例程, 返回一个指向用 ma ll oc 分配的字符串指针
33、 ,当程序不 再使用它时可以释放它。如果库使用 ma ll oc 的某个版本来分 配字符串的空间,但是应用程序使用另一个版本的 free 来释名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 8 页,共 11 页 - - - - - - - - - 放这个 字符串的空间,那 么就会发生混乱。为了能 够允许应用程序提 供它们自 己版本的 ma ll oc 和 free , System V.3 的共享 C库使用了一 种“丑陋”的技术,如 图 4 所示。系统的 维护者将 ma ll oc
34、和 free 重新定义为间接调用,这是通过 绑定到共享库的数 据部分的函数 指针实现 的,我们将 称它们为 ma ll oc_ptr 和 free_ptr 。extern void *( *ma ll oc_ptr)(size_t) ;extern void (*free _ptr)(void *) ;#define ma ll oc(s) (*ma ll oc_ptr)(s)#define free(s) (*free _ptr)(s)-图 9-4: ma ll oc ha ck程序,共享 C库的图例ma ll oc 指针和初始化代码从库代码中的间接调用-然后它们重 新编译了整个 C库,并将
35、 下面的几行内容(或汇编同类内容 ) 加 入到占位库的.init段,这样它们就被加 入到每个使用 该共享库的程序中了。#undef mall oc#undef freema ll oc_ptr = &ma ll oc;free _ptr = &free ;由于占位库将被 绑定到应用程序中的,而不是共享库,所以它对ma ll oc 和 free 的引用是在链接时解 析的。如果存在一个 私有版本的 ma ll oc 和 free ,它将指向私有版本函数的 指针(译者注:指 ma ll oc_ptr 和 free _ptr ),否则它将使用标准库的 版本。不管哪种方法,名师资料总结 - - -精品资
36、料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 9 页,共 11 页 - - - - - - - - - 库和应用程序使用的都是相同版本的 ma ll oc 和 free 。虽然这种实现方法让库的维护工作更加 困难了,而且 只能用于 少数手工选定的名字,但只要它可以自动 进行而不 需要手工编写 脆弱的源代码,这 种在程序运行时通过 指针进行解析进行库内例程调用的方 法就是一个很 好的主意,我们将 会在下一章看到自动 版本是如何工作的。全局数据中的名 字冲突仍然 是遗留在共享库中的一个 问题。考虑一下图 5所示的小
37、程序。如果你用任何一个我们本章 描述过的共享库编译和链接它,他将打印一个值为 0 的状态代码而不是 正确的错误代码。这是 由于int errno创建了一个没有 绑定到共享库中 去的新的 errono 实例。如果 你不将 extern 注释掉,这个程序就可以 正常运行,因为它 现在引用了一个未定义的 全局变量,这将使链接器将其 绑定到共享库中的 errno 。就 像我们将要 看到的,动 态链接很 好地解决了这个 问题,但是 会付出一些性能的代 价。-图 9-5:地址冲突示 例#in clu de / * extern */int errno;main()unl ink( /non- existe
38、nt - fi l e ) ;printf(Stat us was % d n, errno) ;-最后,即使 UNIX共享库中的 跳转表也 会引起兼 容性的问题。在共享库外的例程 看来,库中输出的每个例程的地址就是一个跳转表表 项的地址。但是在库内部的例程看来,例程的地址可能是 跳转表表项,也可能是 跳转表要 跳转到的 实际入 口点。有时为了 处理某些特殊情况,一个库例程 会比较作为参数传递给它的地址, 看它是否是某个库例程的地址。一种显而易见但是不 完全有效的解决方 案就是在 建立共享库的过程中将例程的地址绑定到跳转表表 项,因为这 样可以确保库中所有例程的符号 引用都被解 析到对应的表
39、项。但是如果两个例程在同一个 目标文件中,那 么在这个 目标文件中的 引用通常是对例程文本段地址的相对 引用。( 由于是同一个 目标文件, 该例程地址已 知;除了这种特殊情况,没有 什么别的理由需 要返回到同一 目标文件中 去引用一个符号例程)。 虽然通过扫描可重定 位的文本段引用来找到相应的输出符号的地址是可能的,但是实际中最常用的解决方 法是“别那么做” ,不要编写 依赖于需要识别库例程地址的代码。W indows 的 DLL库也存在相 似的问题,因为在每一个 EXE或者 DLL内部,输 入例程的地址被认为是可以间接 跳转到例程 实际地址的 占位例程地址(译 者注:由于这里的地址并不是实际
40、地址,所以 才会被认为是一个 问题)。同 样,对这个 问题最常采用的解决方 法是“别那名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 10 页,共 11 页 - - - - - - - - - 么做”。练习如果你在一个带有共享库的 UNIX系统上 查看/sh l i b目录,你会发现每个库都 会有 3到4个版本,诸如 l i bc_s.2.0.1、 l i bc_s.3.0.0。为 什么不使用最 新的一个 呢?在一个空 占位库中,为 什么将每一个例程中的未定义全局符号都包含 进来是非
41、常重要的,即使在一个未定义的 全局符号引用了该库中的 另一个例程 ?一个空 占位库是包含了 诸如在 COFF 或 Lin ux 中的所有库符号的单一可执行体,另一个是具有多个单 独的模块的 实际库,两者什么不同呢?项目我们要 扩展链接器以 支持静态 共享库。这包 括很多子项目,第一个就是 建立共享库,然后就是使用共享库来链接可执行体。在我们的系统中,共享库 只是一个被链接了 给定地址的 目标文件。 虽然它可以 引用其它的共享库,但不 会有重定 位和未解 析的全局符号引用。占位库是普通的目录格式或者文件格式的库,库中的每一 项包含针对对应库成员的输出( 绝对的)和输 入符号,但是没有文本段或数据
42、段。每一个 占位库必须告诉链接器对应的共享库的名 字。如果 你使用目录格式的占位库,那 么一个名为 “LIBRARY NA M E”的文件将包含一行一行的文本。第一行是对应共享库的名称,剩下的行是该共享库 依赖的其它共享库名 称(空格避免 了符号的名 字冲突 )。如果你使用文件 格式的库,那 么库的初始行要有些 额外的域:LIBRAR Y nnnn pppppp fffff ggggg hhhhh .这里 fffff是共享库的名 字,剩下的是它所 依赖的其它共享库的名 称。项目 9- 1:让链接器可以 从规则 的目录格式或文件格式中生成 静态共享库和 占位库。如果你还没有那 么做,你将需要给链
43、接器增加一个标识(译 者注:参数格式)来 设置链接器分配的段基地址。输 入是一个 规则的库,和这个库所 依赖的其它 任何共享库的 占位库。输出是一个包含所有输 入库成员的段的可执行 格式的共享库,和一个对每个包含输入库的成员都有对应占位成员的 占位库。项目 9- 2:扩展链接器以使用 静态共享库生成可执行 体。鉴于在一个执行 体中引用共享库中的符号 与在一个共享库中 引用另一个共享库的符号的方 法是相同的,所以 项目 9-1已经完成了搜索 占位库符号解 析的大多数工作。链接器 需要将必要的库名放到输出文件中,以便运行时加载器 知道需要加载 什么库。让链接器 建立一个名为.l i b 的段, 保存需要的共享库名称,这些名 称之间以 null 字节标识间 隔,以 2个 null 字节标识 结尾。建立一个名为 _S HARE D_LIBRARIES的符号,它 引用. l i b段的开始地址,可以 让库的初始化例程使用。 名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 11 页,共 11 页 - - - - - - - - -
限制150内