总结了GCC链接中的几个注意点,包括库文件依赖顺序、强符号和弱符号、全局符号接入和共享库版本。

库文件依赖顺序

GCC在链接时对依赖库的顺序是敏感的,被依赖的库必须放在后面,比如liba.a依赖libb.a,必须写成liba.a libb.a,否则链接将出错。在库比较多依赖关系比较复杂或者相互依赖或者自己不清楚的情况下,可以使用下面的选项来强制GCC重复查找依赖库:

g++ -o tt tt.o -Xlinker "-(" -lws2_32 -lclsocketd -Xlinker "-)"

强符号和弱符号

在链接中,如果多个目标文件中含有相同名字的全局符号的定义,链接器是怎么进行处理的?这里就涉及到强符号和弱符号的问题,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号,强符号和弱符号都是针对定义来说的,不是针对符号的引用。链接器会按照如下规则处理重复定义的全局符号:

规则1:不允许强符号被多次定义,否则链接器报符号重定义错误。

规则2:如果一个符号在某个目标文件中是强符号,在其它文件中都是弱符号,那么选择强符号。

规则3:如果弱符号在所有目标文件中都是弱符号,那么选择占用空间最大的一个。

我们可以通过GCC的__atrribute__((weak))来定义任何一个强符号为弱符号,如:

__atrribute__((weak)) weak = 1;

链接时如果未找到某个符号的定义,链接器就会报符号未定义错误,这种被称为强引用。与之对应的还有一种弱引用,如果弱引用的符号未定义,链接器对该引用不报错。我们可以使用GCC中的__attribute__((weakref))这个关键字来声明对一个外部函数的引用为弱引用,如:

__attribute__((weakref)) void foo();

int main()

{

    if (foo) foo();

}

这种弱符号和弱引用对库来说十分有用,比如库中定义的弱符号可以被用户定位的强符号所覆盖,从而使程序使用自定义版本的库函数;或者程序可以对某些扩展功能模块的引用定义为弱引用,如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能,这使得程序功能更容易裁剪和组合。

全局符号介入

在动态链接中,链接器按照各个模块之间的依赖关系,对各个共享对象进行装载并且将它们的符号并入到全局符号表时,如果两个不同的模块定义了同一个符号,会出现什么结果呢?这个问题涉及到共享对象全局对象介入,即一个共享对象里面的全局对象会被另一个共享对象的同名全局符号覆盖。Linux下的动态链接器的处理规则是这样的:当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略。

共享库版本

Linux使用共享库版本的方法来解决共享库的兼容性问题,它规定共享库的文件命名规则如下:

libname.so.x.y.z

x表示主版本号,主版本号表示库的重大升级,不同主版本号之间是不兼容的。

y表示次版本号,次版本号表示库的增量升级,即新增一些新的接口符号,且保持原来的符号不变。在主版本号相同的情况下,高的此版本号的库向下兼容低的次版本号的库。

z表示发布版本号,发布版本号表示库的一些错误修正、性能改进等,并不添加任何新的接口,也不对接口进行更改。相同主版本号、次版本号的共享库之间完全兼容。

Linux采用一种叫做SO-NAME的命令机制来记录共享库的依赖关系。每个共享库都有一个对应的SO-NAME,这个SO-NAME即共享库的文件名去掉次版本号和发布版本号,保留主版本号。比如一个共享库叫做libfoo.so.2.6.1,那么它的SO-NAME就是libfoo.so.2。系统会为每个共享库在它所在的目录创建一个跟SO-NAME相同的并且指向它的软链接,这个软链接会指向目录中主版本号相同、次版本号和发布版本号最新的共享库。

在编译输出ELF文件时,将被依赖共享库的SO-NAME保存到.dynamic中,这样当动态链接器进行共享库依赖文件查找时,就会根据系统中各种共享目录中的 SO-NAME软链接自动定向到所兼容的最新版本的共享库。