C语言中的一个重要思想就是分别编译(Separate Compilation),即若干个源程序在不同的时候单独进行编译,然后在恰当的时候整合到一起。但是,连接器一般是与C编译器分离的,它不可能了解C语言的诸多细节。那么,连接器是如何把若干个C源程序合并成一个整体呢?尽管连接器并不理解C语言,然而它却能够理解机器语言和内存布局。编译器的责任是把C源程序“翻译”成对连接器有意义的形式,这样连接器就能够“读懂”C源程序。
典型的连接器把由编译器生成的若干个目标模块,整合成一个被称为载入模块或可执行文件的实体,该实体能够被操作系统直接执行。其中,某些目标模块是直接作为输入提供连接器的;而另外一些目标模块则是根据连接过程的需要,从包括有类似printf函数的库文件中取得的。
连接器通常把目标看成是由一组外部对象(external object)组成的。每个外部对象代表着机器内存中的某个部分,并通过一个外部名称来识别。因此,程序中的每个函数和每个外部变量,如果没有被声明为static,就都是一个外部对象。某些C编译器会对静态函数和静态变量的名称做一定改变,将它们也作为外部对象。由于经过了“名称修饰”,所以它们不会与其他源程序文件的同名函数或同名变量发生命名冲突。
大多数连接器禁止同一个载入模块中的两个不同外部对象拥有相同的名称。然而,在多个目标模块整合成一个载入模块时,这些目标模块可能就包含了同名的外部对象。连接器的重要工作就是处理这类命名的冲突。
处理命名冲突的简单办法就是干脆完全禁止。对于外部对象是函数的情形,这种做法当然正确,一个程序如果包括两个同名的不同函数,编译器根本就不应该接受。而对于外部对象是变量的情形,问题就变得有些困难了。不同的连接器对这种情形有着不责骂的处理方式,我们将在后面看到这一点的重要性。
有了这些信息,我们现在可以大致想象出连接器是如何工作的情形了。连接器的输入是一组目标模块和库文件。连接器的输出是一个载入模块。连接器读入目标模块和库文件,同时生成载入模块。对每个目标模块中的每个外部对象,连接器都要检查载入模块,看是否已有同名的外部对象。如果没有,连接器就将该外部对象添加到载入模块中;如果有,连接器就要开始处理命名冲突。
除了外部对象之外,目标模块中还可能包括了对其他模块中的外部对象的引用。例如,一个调用了函数printf的C程序所生成的目标模块,就包括了一个对函数printf的引用。可以推测得出,该引用指向的是一个位于某个库文件中的外部对象。在连接器生成载入模块的过程中,它必须同时记录这些外部对象的引用。当连接器读入一个目标模块时,它必须解析出这个目标模块中定义的所有外部对象的引用,并作出标记说明这些外部对象不再是未定义的。
因为连接器对C语言“知之甚少”,所以有很多错误不能被检测出来。再次强调,如果读者的C语言实现中提供了lint程序,切记要使用!
未经允许不得转载:TacuLee » C陷阱与缺陷之什么是连接器