4.3 COMMON块
正如前面提到过的,由于弱符号机制允许同一个符号的定义存在于多个文件中,所以可能会导致的一个问题是:如果一个弱符号定义在多个目标文件中,而它们的类型又不同,怎么办?目前的链接器本身并不支持符号的类型,即变量类型对于链接器来说是透明的,它只知道一个符号的名字,并不知道类型是否一致。那么当我们定义的多个符号定义类型不一致时,链接器该如何处理呢?让我们来分析一下多个符号定义类型不一致的几种情况,主要分三种情况:
- 两个或两个以上强符号类型不一致;
- 有一个强符号,其他都是弱符号,出现类型不一致;
- 两个或两个以上弱符号类型不一致。
对于上述三种情况,第一种情况是无须额外处理的,因为多个强符号定义本身就是非法的,链接器会报符号多重定义错误;链接器要处理的就是后两种情况。
事实上,现在的编译器和链接器都支持一种叫COMMON块(Common Block)的机制,这种机制最早来源于Fortran,早期的Fortran没有动态分配空间的机制,程序员必须事先声明它所需要的临时使用空间的大小。Fortran把这种空间叫COMMON块,当不同的目标文件需要的COMMON块空间大小不一致时,以最大的那块为准。
现代的链接机制在处理弱符号的时候,采用的就是与COMMON块一样的机制。前面我们在SimpleSection.c这个例子中已经看到,编译器将未初始化的全局变量定义作为弱符号处理。比如符号global_uninit_var,它在符号表中的各个值为(使用readelf -s):
st_name = "global_uninit_var"
st_value = 4
st_size = 4
st_info = 0x11 STB_GLOBAL STT_OBJECT
st_other = 0
st_shndx = 0xfff2 SHN_COMMON
可以看到它是一个全局的数据对象,它的类型为SHN_COMMON类型,这是一个典型的弱符号。那么如果我们在另外一个文件中也定义了global_uninit_var变量,且未初始化,它的类型为double,占8个字节,情况会怎么样呢?按照COMMON类型的链接规则,原则上讲最终链接后输出文件中,global_uninit_var的大小以输入文件中最大的那个为准。即这两个文件链接后输出文件中global_uninit_var所占的空间为8个字节。
当然COMMON类型的链接规则是针对符号都是弱符号的情况,如果其中有一个符号为强符号,那么最终输出结果中的符号所占空间与强符号相同。值得注意的是,如果链接过程中有弱符号大小大于强符号,那么ld链接器会报如下警告:
ld: warning: alignment 4 of symbol `global’ in a.o is smaller than 8 in b.o
这种使用COMMON块的方法实际上是一种类似"黑客"的取巧办法,直接导致需要COMMON机制的原因是编译器和链接器允许不同类型的弱符号存在,但最本质的原因还是链接器不支持符号类型,即链接器无法判断各个符号的类型是否一致。
现在我们再回头总结性地思考关于未初始化的全局变量的问题:在目标文件中,编译器为什么不直接把未初始化的全局变量也当作未初始化的局部静态变量一样处理,为它在BSS段分配空间,而是将其标记为一个COMMON类型的变量?
通过了解链接器处理多个弱符号的过程,我们可以想到,当编译器将一个编译单元编译成目标文件的时候,如果该编译单元包含了弱符号(未初始化的全局变量就是典型的弱符号),那么该弱符号最终所占空间的大小在此时是未知的,因为有可能其他编译单元中该符号所占的空间比本编译单元该符号所占的空间要大。所以编译器此时无法为该弱符号在BSS段分配空间,因为所须要空间的大小未知。但是链接器在链接过程中可以确定弱符号的大小,因为当链接器读取所有输入目标文件以后,任何一个弱符号的最终大小都可以确定了,所以它可以在最终输出文件的BSS段为其分配空间。所以总体来看,未初始化全局变量最终还是被放在BSS段的。
关于多个文件中出现同一个变量的多个定义的原因,还有一种说法是由于早期C语言程序员粗心大意,经常忘记在声明变量时在前面加上"extern"关键字,使得编译器会在多个目标文件中产生同一个变量的定义。为了解决这个问题,编译器和链接器干脆就把未初始化的变量都当作COMMON类型的处理。
GCC的"-fno-common"也允许我们把所有未初始化的全局变量不以COMMON块的形式处理,或者使用"__attribute__"扩展:
int global __attribute__((nocommon));
一旦一个未初始化的全局变量不是以COMMON块的形式存在,那么它就相当于一个强符号,如果其他目标文件中还有同一个变量的强符号定义,链接时就会发生符号重复定义错误。