理解Common Block

Last Updated: 2023-06-19 03:06:51 Monday

-- TOC --

有要个细节需要注意:非可执行的ELF文件,比如.o文件,未初始化的全局变量有可能不会进入.bss段的size中。此时的未初始化的全局变量,属于一种典型的弱符号

未初始化的全局变量,在object file的符号表中,可能属于弱符号,index为COMMON,即COMMON Block!这是什么意思?

据说这种Common机制来自早期的Fortran,早期的Fortran没有动态分配空间的机制,程序员必须事先申明它所需要的空间大小。Fortran把这种空间叫做COMMON BLOCK,当不同的目标文件需要的COMMON块空间大小不一致的时候,以最大的那块为准。

类似C语言的Union

现代链接器在处理弱符号的时候,采用的就是与COMMON BLOCK一样的机制,当多个文件出现相同名称的弱符号的时候,链接器以所占空间最大的那个符号为准!链接器只能看到符号的大小,看不到符号的类型,它只能这么干。

多个文件出现同一个全局变量的定义的时候,这基本上是一个错误!但是如果只是申明,并没有被初始化,则他们全部都是弱符号。链接时,对此名称符号的应用,以size最大的那个为准。据说出现这种情况的原因,是早期C程序员粗心大意,经常忘记在声明外部引用变量的时候,加上extern关键词,使得编译器会在多个目标文件中,产生同一个符号定义。为了解决这个问题,编译器和链接器干脆就把未初始化的变量都当做COMMON类型来处理。

GCC的-fno-common允许将未初始化的全局变量,不以COMMON BLOCK的方式来处理,或者使用如下定义:

int global_var __attribute__((nocommon));

只要未初始化的全局变量不是以COMMON BLOCK的形式存在,它就是一个强符号了,出现重复定义时,链接器会直接报错。


查看了一下gcc的manual page,发现在默认情况下,-fno-common已经开启了,要测试common block,反而需要-fcommon来打开这个古老的开关。下面做个测试:

准备两个c文件,test.c:

#include <stdio.h>

int a;
int b;

int main(){
    printf("%d %d\n", a, b);
    return 0;
}

t2.c:

long long a;

先分别编译,查看符号表:

$ gcc -c -fcommon test.c
$ gcc -c -fcommon t2.c
$ readelf -s test.o

Symbol table '.symtab' contains 8 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS test.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .rodata
     4: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM a
     5: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM b
     6: 0000000000000000    40 FUNC    GLOBAL DEFAULT    1 main
     7: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
[xinlin@likecat test]$ readelf -s t2.o

Symbol table '.symtab' contains 3 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS t2.c
     2: 0000000000000008     8 OBJECT  GLOBAL DEFAULT  COM a

两个变量a,和变量b,在符号表中,都属于COM!(Ndx表示符号所在的section)

链接之后,就只有1个变量a了,而且size是8:

$ gcc test.o t2.o -o tt2
$ readelf -s tt2 | grep ' a'
    64: 0000000000404030     8 OBJECT  GLOBAL DEFAULT   25 a

这段测试代码,如果在-fcommon下强行通过编译,是存在隐患的。最终链接后,变量a是8bytes的long long类型,而printf使用的%d来打印a。

最后,

-fcommon

In C code, this option controls the placement of global variables defined without an initializer, known as tentative definitions in the C standard. Tentative definitions are distinct from declarations of a variable with the "extern" keyword, which do not allocate storage.

The default is -fno-common, which specifies that the compiler places uninitialized global variables in the BSS section of the object file. This inhibits the merging of tentative definitions by the linker so you get a multiple-definition error if the same variable is accidentally defined in more than one compilation unit.

The -fcommon places uninitialized global variables in a common block. This allows the linker to resolve all tentative definitions of the same variable in different compilation units to the same object, or to a non-tentative definition. This behavior is inconsistent with C++, and on many targets implies a speed and code size penalty on global variable references. It is mainly useful to enable legacy code to link without errors.

本文链接:https://cs.pynote.net/sf/c/cdm/202111231/

-- EOF --

-- MORE --