为何链接器禁止不同CPP文件的同名非static变量?附原理解析
向至少一个int foo添加static关键字后,会对目标文件产生何种影响以避免“multiple definitions of foo”错误?为何foo默认能被其他CPP文件访问?我原本认为CPP文件中的变量默认相互隔离,仅在使用extern时才会对外可见。希望基于目标文件包含的内容解释其实现机制,如有可能请附带反汇编示例。
一、为什么全局foo默认能被其他CPP访问?
这是因为C++里全局变量默认带外部链接性——你在a.cpp里写的int foo;,编译器会把这个变量对应的符号标记为全局可见,放到目标文件(a.o)的全局符号表里。链接器在合并所有目标文件时,会把所有同名的外部链接符号当成同一个实体,这也是现在代码报“multiple definitions”的原因:a.o和b.o里都有一个全局的foo符号,链接器不知道该留哪一个。
你之前的认知搞反了:CPP文件的全局变量默认不是隔离的,反而对外可见;extern要么是用来声明“这个变量在其他文件定义”(比如extern int foo;),要么是显式强调变量的外部链接性(和默认行为一致)。
二、加static后目标文件的变化与错误规避
给int foo加static后,变量就变成了内部链接性:
- 符号表层面:目标文件里的
foo符号会被标记为局部符号(比如ELF格式下,符号类型从STB_GLOBAL变成STB_LOCAL),链接器合并目标文件时会忽略这类局部符号,不会把不同文件里的同名static变量当成同一个东西。 - 内存层面:每个带static的
foo会在各自的目标文件里分配独立的内存,a.cpp的static foo和b.cpp的static foo是完全无关的两个变量,互相不影响。
这样链接器就不会遇到同名的全局符号冲突,自然就不会报错了。
三、反汇编与符号表示例(基于GCC+Linux ELF平台)
1. 未加static时的符号表
用nm命令查看a.o的符号:
nm a.o 0000000000000000 B foo 0000000000000000 T do_foo_a
这里大写的B表示全局未初始化数据符号(属于.bss段),T表示全局代码符号。同样查看b.o也会有0000000000000000 B foo,链接时两个全局foo冲突。
2. 加static后的符号表
修改a.cpp为static int foo;,再看a.o的符号:
nm a.o 0000000000000000 b foo 0000000000000000 T do_foo_a
小写的b表示局部未初始化数据符号,链接器不会将其与其他文件的同名符号合并。此时如果b.o的foo还是全局的,链接器只会保留b.o的全局foo,a.o的局部foo仅在a.cpp内部有效。
3. 反汇编代码对比
未加static的do_foo_a反汇编(用gcc -S a.cpp生成.s文件):
do_foo_a: pushq %rbp movq %rsp, %rbp movl $1, foo(%rip) # 引用全局foo符号 movl foo(%rip), %eax popq %rbp ret
添加static后的do_foo_a反汇编:
do_foo_a: pushq %rbp movq %rsp, %rbp movl $1, foo(%rip) # 引用当前文件内的局部foo,符号仅在本目标文件有效 movl foo(%rip), %eax popq %rbp ret
指令本身看起来差不多,但符号的可见范围完全不同——链接器处理时,前者的foo会被解析为全局符号,后者则被限定在当前目标文件内。
内容的提问来源于stack exchange,提问作者Mister Mystère

