链接器和加载器09
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
第9章 共享库
$Revision: 2.3 $
$Date: 1999/06/15 03:30:36 $
程序库的产生可以追溯到计算技术的最早期,因为程序员很快就意识到通过重用程序
的代码片段可以节省大量的时间和精力。随着如Fortran and COBOL等语言编译器的发展,程序库成为编程的一部分。当程序调用一个标准过程时,如sqrt(),编译过的语言显式地使用库,而且它们也隐式地使用用于I/O、转换、排序及很多其它复杂得不能用内联代码解释的函数库。随着语言变得更为复杂,库也相应地变复杂了。当我在20年前写一个Fortran 7 7编译器时,运行库就已经比编译器本身的工作要多了,而一个Fortran 77库远比一个C++库要来得简单。
语言库的增加意味着:不但所有的程序包含库代码,而且大部分程序包含许多相同的
库代码。例如,每个C程序都要使用系统调用库,几乎所有的C程序都使用标准I/O库例程,如printf,而且很多使用了别的通用库,如math,networking,及其它通用函数。这就意
味着在一个有一千个编译过的程序的UNIX系统中,就有将近一千份printf的拷贝。如果所有那些程序能共享一份它们用到的库例程的拷贝,对磁盘空间的节省是可观的。(在一个没有共享库的UNIX系统上,单printf的拷贝就有5到10M。)更重要的是,运行中的程序如
能共享单个在内存中的库的拷贝,这对主存的节省是相当可观的,不但节省内存,也提高页交换。
所有共享库基本上以相同的方式工作。在链接时,链接器搜索整个库以找到用于解决
那些未定义的外部符号的模块。但链接器不把模块内容拷贝到输出文件中,而是标记模块来自的库名,同时在可执行文件中放一个库的列表。当程序被装载时,启动代码找到那些库,并在程序开始前把它们映射到程序的地址空间,如图1。标准操作系统的文件映射机制自动共享那些以只读或写时拷贝的映射页。负责映射的启动代码可能是在操作系统中,或在可执行体,或在已经映射到进程地址空间的特定动态链接器中,或是这三者的某种并集。
---------------------------------------------------------------------------------------------图9-1:带有共享库的程序
可执行程序,共享库的图例
可执行程序main,app库,C库
不同位置来的文件
箭头展示了从main到app,main到C,app到C的引用
---------------------------------------------------------------------------------------------在本章,我们着眼于静态链接库,也就是库中的程序和数据地址在链接时绑定到可执
行体中。在下一章我们着眼于更复杂的动态链接库。尽管动态链接更灵活更“现代”,但也比静态链接要慢很多,因为在链接时要做的大量工作在每次启动动态链接的程序时要重新做。同时,动态链接的程序通常使用额外的“胶合(g lu e)”代码来调用共享库中的例程。胶合代码通常包含若干个跳转,这会明显地减慢调用速度。在同时支持静态和动态共享库的系统上,除非程序需要动态链接的额外扩展性,不然使用静态链接库能使它们更快更小巧。
绑定时间
共享库提出的绑定时间问题,是常规链接的程序不会遇到的。一个用到了共享库的程
序在运行时依赖于这些库的有效性。当所需的库不存在时,就会发生错误。在这情况下,除了打印出一个晦涩的错误信息并退出外,不会有更多的事情要做。
当库已经存在,但是自从程序链接以来库已经改变了时,一个更有趣的问题就会发生。在一个常规链接的程序中,在链接时符号就被绑定到地址上而库代码就已经绑定到可执行体中了,所以程序所链接的库是那个忽略了随后变更的库。对于静态共享库,符号在链接时被绑定到地址上,而库代码要直到运行时才被绑定到可执行体上。(对于动态共享库而言,它们都推迟到运行时。)
一个静态链接共享库不能改变太多,以防破坏它所绑定到的程序。因为例程的地址和
库中的数据都已经绑定到程序中了,任何对这些地址的改变都将导致灾难。
如果不改变程序所依赖的静态库中的任何地址,那么有时一个共享库就可以在不影响程序对它调用的前提下进行升级。这就是通常用于小bu g修复的"小更新版"。更大的改变不可避免地要改变程序地址,这就意味着一个系统要么需要多个版本的库,要么迫使程序员在每次改变库时都重新链接它们所有的程序。实际中,永远不变的解决办法就是多版本,因为磁盘空间便宜,而要找到每个会用到共享库可执行体几乎是不可能的。
实际的共享库
本章余下的部分将关注于UNIX Sy stem V Re l ease 3.2 (COFF格式),较早的Lin ux系统(a.o u t格式),和4.4B S D的派生系统(a.o u t和E LF格式)这三者提供的静态共享库。这三者以几近相同的方式工作,但有些不同点具有启发意义。SV R3.2的实现要求改变链接器以支持共享库搜索,并需要操作系统的强力支持以满足例程在运行时的启动需求。Lin ux的实现需要对链接器进行一点小的调整并增加一个系统调用以辅助库映射。B S D/O S的实现不对链接器或操作系统作任何改变,它使用一个脚本为链接器提供必要的参数和一个修改过的标准C 库启动例程以映射到库中。
地址空间管理
共享库中最困难的就是地址空间管理。每一个共享库在使用它的程序里都占用一段固定的地址空间。不同的库,如果能够被使用在同一个程序中,它们还必须使用互不重叠的地址空间。虽然机械的检查库的地址空间是否重叠是可能的,但是给不同的库赋予相应的地址空间仍然是一种“魔法”。一方面,你还想在它们之间留一些余地,这样当其中某个新版本的库增长了一些时,它不会延伸到下一个库的空间而发生冲突。另一方面,你还想将你最常用的库尽可能紧密的放在一起以节省需要的页表数量(要知道在x86上,进程地址空间的每一个4MB的块都有一个对应的二级表)。
每个系统的共享库地址空间都必然有一个主表,库从离应用程序很远的地址空间开始。Lin ux从十六进制的60000000开始,B S D/O S从A0000000开始。商业厂家将会为厂家提供的库、用户和第三方库进一步细分地址空间,比如对B S D/O S,用户和第三方库开始于地址A08 00000。
通常库的代码和数据地址都会被明确的定义,其中数据区域从代码区域结束地址后的一个或两个页对齐的地方开始。由于一般都不会更新数据区域的布局,而只是增加或者更改代码区域,所以这样就使小更新版本成为可能。
每一个共享库都会输出符号,包括代码和数据,而且如果这个库依赖于别的库,那么通常也会引入符号。虽然以某种偶然的顺序将例程链接为一个共享库也能使用,但是真正的库使用一些分配地址的原则而使得链接更容易,或者至少使在更新库的时候不必修改输出符号的地址成为可能。对于代码地址,库中有一个可以跳转到所有例程的跳转指令表,并将这些跳转的地址作为相应例程的地址输出,而不是输出这些例程的实际地址。所有跳转指令的大小都是相同的,所以跳转表的地址很容易计算,并且只要表中不在库更新时加入或删除表