还记得上篇文章中,AddressSanitizer(ASAN)linux下的内存分析神器的问题的文章吧,
其中记录了一个明显的代码bug,若不加这个-fno-common选项,那么程序简直就是明显的错误,但可能还未被发现。
举个栗子:
int a[5];
int b[5];
memcpy(a,"1234567890",10); //明显越界了还不报错。
多出来的拷贝到哪了?可能在 b[5]中。甚至 b[6]= 1这样超过数组的赋值也不会报错。
因此,保险起见,别忘了给你的linux应用,交叉编译工具链增加-fno-common选项。
那么Why?原因是什么呢?往下看,给出了说明,未初始化的变量是弱符号,
尤其是当这些弱符号类型和强符号不同时!表面上看起来正确的程序会导致严重的错误!
当编译器将一个编译单元编译成目标文件的时候,如果该编译单元包含了弱符号(未初始化的全局变量就是典型的弱符号),那么该弱符号最终所占空间的大小在此时是未知的,因为有可能其他编译单元中该符号所占的空间比本编译单元该符号所占的空间要大。所以编译器此时无法为该弱符号在BSS段分配空间,因为所需要空间的大小未知。
gcc中no-common的说明
对于链接器来说,所有的全局符号可分为两种:强符号(Strong symbols),弱符号(Weak symbols)。
gcc的attribute中有个__attribute__((weak)),就是用来声明这个符号是弱符号的。gcc手册中这样写道:
The weak attribute causes the declaration to be emitted as a weak symbol rather than a global. This is primarily useful in defining library functions which can be overridden in user code, though it can also be used with non-function declarations. Weak symbols are supported for ELF targets, and also for a.out targets when using the GNU assembler and linker.
对于这个gcc扩展,这里作了一个简洁的介绍。我们来看更通用的情况。;)
一般来说,函数和已初始化的变量是强符号,而未初始化的变量是弱符号。对于它们,下列三条规则适用:
1. 同名的强符号只能有一个。
2. 有一个强符号和多个同名的弱符号是可以的,但定义会选择强符号的。
3. 有多个弱符号时,链接器可以选择其中任意一个。
这三条规则看起来很好理解,其实不然,尤其是当这些弱符号类型和强符号不同时!表面上看起来正确的程序会导致严重的错误!考虑下面这个csapp中的例子:
===a.c=== int x=7; int y=5; p1() {} ===b.c=== double x; p2() {}
我们把它们一起编译,并且在p2()函数中给x赋值,你会发现,y也改变了! 虽然x被看作是double,但其定义会取a.c中的int x,也就是说,在b.c中会把a.c中的int x当double来用!这当然是错误!之所以会这样,就是因为上面的规则2。避免这种错误的一个方法是,给gcc加上-
fno-common选项。
COMMON块
由于弱符号机制允许同一个符号的定义存在于多个文件中,所以可能会导致的一个问题是:如果一个弱符号定义在多个目标文件中,而它们的类型又不同,怎么办?目前的链接器本身并不支持符号的类型,即变量类型对于链接器来说是透明的,它只知道一个符号的名字,并不知道类型是否一致。当我们定义的多个符号定义类型不一致时,链接器如何处理呢?
事实上,现在的编译器和链接器都支持一种叫COMMON块的机制,这种机制最早来源于Fortan,早起的Fortan没有动态分配空间的机制,程序员必须事先声明它所需要的临时使用空间的大小。Fortan把这种空间叫COMMON块,当不同的目标文件需要的COMMON块空间大小不一致时,以最大的那块为准。
现代的链接机制在处理弱符号的时候,采用的就是与COMMON块一样的机制。编译器将未初始化的全局变量定义作为弱符号处理。
当然COMMON类型的链接规则是针对符号都是弱符号的情况,如果其中有一个符号为强符号,那么最终输出结果中的符号所占空间与强符号相同。如果链接过程中有弱符号大小大于强符号,那么ld链接器会报警告。
这种使用COMMON块的方法实际上是一种类似“黑客”的取巧办法,直接导致需要COMMON机制的原因是编译器和链接器允许不同类型的弱符号存在,但最本质的原因还是链接器不支持符号类型,即链接器无法判断各个符号的类型是否一致。
通过了解链接器处理多个弱符号的过程,我们可以想到,当编译器将一个编译单元编译成目标文件的时候,如果该编译单元包含了弱符号(未初始化的全局变量就是典型的弱符号),那么该弱符号最终所占空间的大小在此时是未知的,因为有可能其他编译单元中该符号所占的空间比本编译单元该符号所占的空间要大。所以编译器此时无法为该弱符号在BSS段分配空间,因为所需要空间的大小未知。但是链接器在链接过程中可以确定弱符号的大小,因为当链接器读取所有输入目标文件以后,任何一个弱符号的最终大小都可以确定了,所以它可以再最终输出文件的BSS段为其分配空间。所以总体来看,未初始化全局变量最终还是被放在BSS段的。
GCC的“-fno-common”也允许我们把所有未初始化的全局变量不以COMMON块的形式处理,或者使用“__attribute__”拓展.
一旦一个未初始化的全局变量不是以COMMON块的形式存在,那么它就相当于一个强符号,如果其他目标文件中还有同一个变量的强符号定义,链接时就会发生符号重复定义错误。