弱符号与强符号概念
弱符号与强符号概念
链接过程实质上就是把不同目标文件粘在一起,对不同目标文件中定义或引用的相同名字进行决议resolve和绑定binding。
符号的分类如下:
- 定义在本目标文件中的全局符号,可以被其它文件引用。
- 在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号(External Symbol), 也就是我们前所谓符号引用。
- 段名,这种符号通常由编译器产生,它的值就是该段的起始地址。
- 局部符号,这类符号只在当前编译单元内部可见。局部符号对于链接过程没有作用,链接器往往忽略它们。
- 行号信息,即目标指令与源代码中代码行的对应关系,它是可选的。
链接关心的是各种全局符号。
readelf -s xxx.o
所有 bind 这一列为 GLOBAL 的为 全局符号。
特殊符号
- __executable_start 该符号为程序起始地址,不是入口地址,是程序最开始的地址。
- __etext, _etext, etext 该符号为代码段结束地址
- _edata edata 该符号为数据段结束地址
- _end end 该符号为程序结束地址
以上地址均指的是载入后的虚拟地址。
符号修饰和符号签名
Name Decoration Name Mangling
gcc 编译选项 "-fleading-underscore" 或 "-fno-leading-underscore" 可以打开或者关闭在编译时 C 语言符号前加上下划线。
C++符号修饰因编译器不同而区别很大。比如下面一段代码:
int func(int);
float func(float);
class C {
int func(int);
class C2 {
int func(int);
};
};
namespace N {
int func(int);
class C {
int func(int);
};
}
在gcc下编译,其得到的修饰后的符号名称为:
函数签名 修饰后符号名
int func(int) _Z4funci
float func(float) _Z4funcf
int C::func(int) _ZN1C4funcEi
int C::C2::func(int) _ZN1C2C24funcEi
int N::func(int) _ZN1N4funcEi
int N::C::func(int) _ZN1N1C4funcEi
binutils 工具集中的c++filt 可以用于解析被修饰过的名称
如: c++filt _ZN1N1C4funcEi 输出为 N::C::func(int)
如果是VC编译上面这段代码得到的名称修饰结果为
函数签名 修饰后符号名
int func(int) ?func@@YAHH@Z
float func(float) ?func@@YAMM@Z
int C::func(int) ?func@C@@AAEHH@Z
int C::C2::func(int) ?func@C2@C@@AAEHH@Z
int N::func(int) ?func@N@@YAHH@Z
int N::C::func(int) ?func@C@N@@AAEHH@Z
微软提供了一个api将修饰后的名称转换为函数签名,UnDecorateSymbolName().
在linux平台上, extern “C” 的作用就是让 gcc 编译 C++文件时,对C++函数或变量不采用C++的方式来进行名称修饰。
弱符号与强符号
对于C++来说,弱符号通常来源于未初始化的全局变量。而默认情况下,编译器将函数和初始化了的全局变量作为强符号。
可以通过gcc的 __attribute__((weak)) 来定义任何一个强符号为弱符号。
不同的目标文件中不能有同名的强符号,否则不能链接在一起。
如果一个符号在某个目标文件中是强符号,在其它文件中都是弱符号,那么该名称在链接时选择强符号。
如果一个符号在所有的目标文件中都是弱符号,则选择占用空间(字节数)最大的一个。
相应的有 弱引用与强引用的概念。
可以将一个外部函数申明为弱引用,比如下面的做法:
__attribute__((weakref)) void foo();
int main()
{
if(foo) foo();
}
多个符号定义类型不一致及其处理
不一致有三种情况:
- 两个或两个以上强符号类型不一致;
- 有一个强符号,其他都是弱符号,出现类型不一致;
- 两个或者两个以上弱符号类型不一致。
第一种情况,在编译的时候会提示多重定义错误,因为多个同名强符号定义本身就是非法的。
后面两种情况需要链接器(ld)来处理。
编译器把未初始化的全局变量作为弱符号处理。比如在某个.o中定义了一个未初始化的全局变量 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类型。这里使用的是一种成为 Common Block的机制,是一种事先声明临时使用空间的机制。
如果在另一个.o文件也定义了相同名字的 global_uninit_var 变量,且未初始化,类型为占8个字节的double,则按照common block的链接规则,在最终链接后的输出文件中,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
正是由于未初始化的全局变量(弱符号)其大小在编译某个目标文件时未可知,所以那时无法为其在 .bss 节区分配空间。在链接过程中,任何一个弱符号的最终大小都可以确定了,所以它可以在最终输出文件的bss段为其分配空间。所以,从最终的输出可执行文件来看,未初始化的全局变量是放在 .bss 节区的。
GCC 可以使用 -fno-common 使得我们可以不以COMMON 块机制处理未初始化的全局变量。这时,该符号就相当于一个强符号。
int global __attribute__((no_common));
当然,如果程序员足够小心,在声明全局变量时记住在该加 “extern” 关键字时加上它,很多的弱符号类型不一致问题可避免。
__END__