租房平台网站开发,高端网站模板,专业网络推广团队,企业国家信用信息公示今天讨论一个问题#xff0c;那就是在 c/c 中#xff0c;比如两个文件中的同名全局变量#xff0c;一定会造成 redefine 的问题吗#xff1f;
我们知道#xff0c;在 c 和 c 中#xff0c;编译器对符号的 mangling 是不同的#xff0c;c 中通过下划线前缀加上符号的名称…今天讨论一个问题那就是在 c/c 中比如两个文件中的同名全局变量一定会造成 redefine 的问题吗
我们知道在 c 和 c 中编译器对符号的 mangling 是不同的c 中通过下划线前缀加上符号的名称的方式而 c 中符号 mangling 的规则要复杂很多需要加上符号的类型名称和长度而函数中有函数名长度参数名长度和类型返回值类型不作为 mangling 的元素。
所以在 c 中不能进行函数重载而 c 中可以函数重载且只需要参数个数参数类型不同即可而仅仅返回值不同不能构成函数重载。
那么如果在两个不同的文件中有两个同名的全局变量编译链接的时候会发生什么呢一定会发生 redefine 的错误吗
c 中的实验
实验环境
OSUbuntu 16.04.7 LTS, xenialCompiler: gcc 9.4
// 文件 a.c
#include stdio.h
int a;void foo() {a 10;printf(%d, %p\n, a, a);
}这里使用了一个函数进行打印是因为单纯写一个变量声明 int a编译器优化时会认为是一个 unused code直接作为无用代码消除了。
// b.c
#include stdio.h
double a;void goo() {printf(%lf, %p\n, a, a);
}再添加一个 main.c
// main.c
#include stdio.h
extern void foo();
extern void goo();
int main(int argc, char *argv[])
{foo();goo();printf(HelloWorld\n);return 0;
}写一个简单的 Makefile 来编译一下
SRC$(shell ls *.c)
OBJS$(subst .c,.o,${SRC})
$(info OBJS$(OBJS))CFLAGS -g -Wall -O0 #-fcommon%.o: %.cgcc $(CFLAGS) -c $ -o $main: $(OBJS)gcc $^ -o $在 c 中这段代码是可以编译的且没有警告。编译完后我们使用 readelf 来看下这几个 elf 文件。(readelf 是一个用于查看ELF格式文件信息的命令行工具)
# readelf a.o
Symbol table .symtab contains 17 entries:Num: Value Size Type Bind Vis Ndx Name0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND1: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c
......14: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM a15: 0000000000000000 45 FUNC GLOBAL DEFAULT 1 foo16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf从上面可以发现符号 a 的索引为 COM表明这是一个 COMMON 的符号。printf 是 libc 中的 api需要链接 libc.soUND 表示 undefine。
再看下 b.o 同样如此 14: 0000000000000008 8 OBJECT GLOBAL DEFAULT COM a15: 0000000000000000 39 FUNC GLOBAL DEFAULT 1 goo16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printfgcc 编译的 elf 文件中符号是没有类型的只能知道这是一个 GLOBAL 对象OBJECT符号类型是 COMMON。
无论是在 a.c 还是 b.c 中符号 a 都是全局且未初始化的gcc 编译器编译时将其编译成了 COMMON 符号。
什么是 COMMON 符号 from 《程序员的自我修养——链接、装载与库》 chapter 4.3 COMMON 块 早期的 Fortran 没有动态分配空间的机制程序员必须事先声明它所需要的临时空间的大小。Fortran 把这种空间叫做 COMMON 块。当不同的目标文件需要的 COMMON 块空间大小不一致时以最大的那块为准。 编译器在处理未初始化的全局变量时将其作为弱符号进行处理。而现在的链接机制就是在处理弱符号的时候采用的与 COMMON 块一样的机制。
COMMON 符号针对的是一种弱符号处理。仅能包含在可重定位目标文件中而不包含在可执行目标文件中。可执行目标文件是链接器连接后生成的文件这时链接器会对符号进行处理针对 COMMON 这种弱符号
如果出现两个以上的同名强符号会直接报 redefine 的错误如果有一个同名的是强符号其他都是弱符号那链接的结果以强符号的为准。但是如果弱符号 size 大小比强符号大链接器会发出告警如果两个以上都是弱符号链接结果以size最大的为准
编译器在编译成目标文件时COMMON 所表示的弱符号因为没有确定最终所占空间的大小此时无法在 BSS 段分配空间。只有在链接阶段确定了实际所占内存大小之后才会在BSS段为其分配空间。最终 未初始化的全局变量就是保存在 BSS 段中的 。
总结
c 中 COMMON 符号其实就是一种弱符号。像上面的例子中的变量申明的情况未初始化的全局变量在单个编译单元中编译成目标文件时就属于一种弱符号编译器作为 COMMON 符号进行处理。这样最终在链接时根据多个弱符号去最大的为准的原则最终以 double 作为 a 的 size 大小保存到 BSS 段中。
c 中的实验
将上面的代码改成 cpp使用 g 进行编译。可以查看一下 a.cpp.o
# readelf -sW a.cpp.o
Symbol table .symtab contains 12 entries:Num: Value Size Type Bind Vis Ndx Name0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND1: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.cpp......9: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 a10: 0000000000000000 45 FUNC GLOBAL DEFAULT 1 _Z3foov11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf可以发现foo 函数在 c 中的mangling与 c 中是有很大区别的。而符号 a 是一个全局强符号。在 c 中认为未初始化的全局变量默认初始化为 0而不是像 c 中以弱符号的形式进行处理。所以这种情况在 C 中编译链接时会出现 redefine 的错误。
但是如果将 a.cpp 中的 a 的声明加上 extern
extern int a;这样结果就不一样了。可以看先这个时候 a.cpp.o 的符号
# readelf -sW a.cpp.o
Symbol table .symtab contains 17 entries:Num: Value Size Type Bind Vis Ndx Name0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND1: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.cpp......14: 0000000000000000 45 FUNC GLOBAL DEFAULT 1 _Z3foov15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND a16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf因为是 extern 的声明此时 a 是一个 UND 的符号链接器认为 a 的实际定义是在其他的模块中。这样就不会出现 redefine 的问题了。而 NOTYPE 表示在当前文件的符号表中没有特定类型与 a 关联。实际上链接器本身也是不支持符号的类型的变量类型对于链接器来说是透明的它只需要知道符号的名字即可。
总结
c 中对未初始化的全局变量以弱符号进行处理在单个编译单元中编译成目标文件时符号类型为 COMMON这样多个弱符号链接时链接器会选择 size 最大的那个同名符号的类型为最终改符号的类型。
而在 c 中未初始化的全局变量默认是初始化为 0 的变量保存在 BSS 段不作为弱符号进行处理。而使用 extern 声明时在编译单元中会任务是一个 UND 的符号在其他 translation unit (TU) 中进行了声明。
当出现两个及以上的同名强符号时链接器就会发出 redefine 的告警。