第11章 高级技术
$Revision: 2.1 $
析构方法$Date: 1999/06/04 20:30:28 $
这一章描述了一些并不是在任何地方都适用的链接器技术。
C++的技术
C++对链接器来说存在三个明显的挑战。一个是它复杂的命名规则,主要在于如果多个
函数具有不同的参数类型则可以拥有相同的名称。name mangling可以对他们进行很好的地
址分配,所有的链接器都使用这种技术的不同形式。
第二个是全局的构造和析构代码,他们需要在main例程运行前运行和main例程退出后运行。这需要链接器将构造代码和析构代码片段(或者至少是指向它们的指针)都收集起来放在一个地方,以便在启动和退出时将他们一并执行。
第三,也是目前最复杂的问题即模板和“extern inline”过程。一个C++模板定义了
一个无穷的过程的家族,每一个家族成员都是由某个类型特定的模板。例如,一个模板可能定义了一个通用的hash表,则就有整数类型的hash表家族成员,浮点数类型的hash表家族成员,字符串类型的,或指向各种数据结构的指针的类型的。由于计算机的存储器容量是无穷的,被编译好的程序需要包含程序中用到的这个家族中实际用到的所有成员,并且不能包含其它的。如果C++编译器采用传统方法单独处理每一个源代码文件,他不能确定是否所编
译的源代码文件中用到的模板是否在其它源代码文件中还存在被使用的其它家族成员。如果编译器采用保守的方法为每一个文件中使用到的每一个家族成员都产生相应的代码,那么最后将可能对某些家族成员产生了多份代码,这就浪费了空间。如果它不产生那些代码,它就有漏掉某一个需要的家族成员的可能性存在。
inline函数存在一个相似的问题。通常,inline函数被像宏那样扩展开,但是在某些
情况下编译器会产生该函数相反的out of line版本。如果若干个不同的文件使用某个包含一个inline函数的单一头文件,并且某些文件需要一个out of line的版本,就会产生代码重复的相同问题。
一些编译器采用改变源代码语言的方法以帮助产生可以被“哑”链接器(d um p lin k ers)链接的目标代码。很多最近的C++系统都把这个问题放到了首位,或者让链接器更聪明些,
或者将程序开发系统的其它部分和链接器整合在一起,以解决这个问题。下面我们概要的看看后一种途径。
试验链接
对于使用“头脑简单”的链接器构建起来的系统,C++系统使用了多种技巧来使得C++
程序得以被链接。一种方法是先用传统的C前端实现来进行通常都会失败的试验链接,然后让编译器驱动(运行各种编译器、汇编器、链接器代码片段的程序)从链接结果中提取信息,再重新编译和链接以完成任务。图1
---------------------------------------------------------------------------------------------图11-1:试验链接
输入文件传递给链接器以产生试验输出和错误信息,然后将输入文件和错误信息,可
能还有更多产生的目标文件一起再传递给链接器以产生最终的目标文件。
---------------------------------------------------------------------------------------------在UNIX系统上,如果lin k er在一次链接任务中不能够解析所有的未定义符号引用,他可以选择仍然输出一个作为后续链接任务的输入文件的输出文件。在链接过程中链接器使用普通的库查规则,使得输出文件包含所需的库,这也是再次作为输入文件所包含的信息。试验链接解决了上面所有的C++问题,虽然很慢,但却是有效的方法。
对于全局的构造和析构代码,C++编译器在每一个输入文件中建立了完成构造和析构功能的例程。这些例程在逻辑上是匿名的,但是编译器给他们分配了可识别的名称。例如,GN U C++编译器会对名为j un k的类中的变量创建名为_GLOBAL_.I.__4j un k和_GLOBAL_.D.__4j u
n k的构造例程及析构例程。在试验链接结束后,链接器驱动程序会检测输出文件的符号表
并为全局构造和析构例程建立一个链表,这是通过编写一个由数组构成的队列的源代码文件来实现的(通过C或者汇编语言)。然后在再次链接中,C++的启动和退出代码使用这个数
组中的内容去调用所有对应的例程。这和那些针对C++的链接器的功能基本相同,区别仅仅是它是在链接器之外实现的。
对于模板和extern inline来说,编译器最初不会为他们生成任何代码。试验链接会获
得程序中实际使用到的所有模板和extern inline的未定义符号,编译器驱动程序会利用这些符号重新运行编译器并为之生成代码,然后再次进行链接。
这里会有一个小问题是为模板寻对应的源代码,因为所要寻的目标可能潜伏在非
常大量的源代码文件中。C前端程序使用了一种简单而特别的技术:扫描头文件,然后猜测一个在foo.h
中声明的模板会定义在中。新近版本的G CC会使用一种在编译过程中生成,以注明模板定义代码的位置的小文件,称之为“仓库”(re p ositor y)。在试验链接后,编译器驱动程序仅需要扫描这些小文件就可以到模板对应的源代码。
消除重复代码
试验链接的方法会产生尽可能小的代码,在试验链接之后会再为第一次处理遗留下的
任何源代码继续产生代码。之所以采用这种前后颠倒的方法是为了生成所有可能的代码,然后让链接器将那些重复的丢掉。图2,编译器为每一个源文件都生成了他们各自所需的每一个扩展模板和extern line代码。每一个可能冗余的代码块都被放到他们各自的段中并用唯一的名字来标识它是什么。例如,G CC将每一个代码块放置在一个命名为.gnu.lin k on c e.d.m angle d name的ELF或C OFF段中,这里“缺损名称”(mangle d name)是指增加了类型信息
的函数名称。有一些格式可以仅仅通过名字就识别出可能的冗余段,如微软的C OFF格式使
用带有精确类型标志的C OM D AT段来表示可能的冗余代码段。如果存在同一个名字的段的多
个副本,那么链接器就会在链接时将多余的副本忽略掉。
――――――――――――――――――――――――――――――――――――――――图11-2:消除重复
传递给链接器的文件中的重复段,经链接器处理后形成单一的段。
――――――――――――――――――――――――――――――――――――――――这种方法非常好的做到了为每一个例程在可执行程序中仅仅生成一个副本,作为代价,会产生非常大的包含一个模板的多个副本的目标文件。但这种方法至少提供了可以产生比其它方法更小的最终代码的可能性。在很多情况下,当一个模板扩展为多个类型时所产生的代码是一样的。例如,鉴于C++的指针都具有相同的表示方法,因此一个实现了类型为<TYPE>可进行边界检查的数组的模板,通常对所有指针类型所扩展的代码都是一样的。所以,那个已经删除了冗余段的链接器还可以检查内容一样的段,并将多个内容一样的段消除为只剩一个。一些w in d o w s的链接器就是这么做的。
借助于数据库的方法
G CC所用的“仓库”实际上就是一个小的数据库。最终,工具开发者都会转而使用数据库来存储源代码和目标代码,就像IBM的V iaual A ge C++的M ontana开发环境一样。数据库跟踪每一个声明和定义的位置,这样就可以在源代码改变后精确的指出哪些例程会对此修改具有依赖关系,并仅仅重新编译和链接那些修改了的地方。
增量链接和重新链接
在很长一段时间里,有一些链接器都允许增量的链接和重新链接。UNIX链接器提供了
一个-r参数告诉链接器在输出文件中保存符号和重定位信息,这样输出文件就可以作为下
一次链接的输入文件了。在IBM的目标代码格式中,每个输入文件中的段(IBM称这些段为控制段或C SE C T)在输出文件中都保存着他们各自的标识符。一个人可以重新编辑一个被链接的程序,并替换或删除这些控制段。这个特性在60年代和70年代早期被非常广泛的应用,因为那时用手工方式来仅仅替换被重新编译过的C SE C T段,并重新链接程序的努力相比于非常慢的编译和链接速度而言还是很值得的。被替换的C SE C T段并不一定要和原先的大小一样,链接器会在输出文件中调整用来计算那些被移动了的C SE C T段不同位置的重定位信息。
在80年代的中后期,S tanfor d的Q uong和L inton在一个UNIX链接器上试验了增量链
接技术,以尝试加快编译-链接-调试这个循环的速度。他们的链接器第一次运行时,它链接了一个传统的静态链接的可执行程序,然后在内存中保存着程序的符号表,并作为一个激活的守护程序运行在后台。在下一次的链接中,他只处理那些被改变的输入文件,在输出文件中替换掉相应的代码,并且保持其它部分不变,而不会对已被移动的引用符号的地方进行修改。由于两次链接之间,被重新编译过的文件中的段大小变化不会太大,他们在建立输出文件的最初版本时会生成比输入文件应生成的段稍微多一点空间的段,如图3。每一次后继的链接,只要被改变的输入文件的段不会增长到超过那个多出的空间,被改变的文件的段只需要替换掉输出文件中的前一个版本即可。如果它增长到超过了那个多处来的空间时,链接器会减少这个段在输出文件中后面紧跟着的若干个段中那部分多余空间,而将那些段向后移动,以让出空间来放置这个增大的段。如果需要被移动的段超过一个很小的数量限制后,那么链接器
将放弃移动索性从头重新链接。
――――――――――――――――――――――――――――――――――――――――
F igure 11-3: I n c remental lin k ing
p i c ture of in c lin k-e d o bj e c t file w ith slo p b et w een segments, an d ne w version’s segments p ointing to re p la c e ol d ones