链接的接口–符号
链接过程的本质就是把多个不同的目标文件相互"粘"到一起.在连接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用.比如说目标文件B用到了目标文件A中的函数"foo",那么我们就称目标文件A**定义(Define)了函数foo,称目标文件B引用(Reference)**了目标文件A中的函数foo.这两个概念同样适用于变量.
在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name).
链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(Symbol Table),这个表里面记录了目标文件中所用到的所有符号,每个定义的符号都有一个对应的值,叫做符号值(Symbol Value),对于变量和函数来说,这个符号值就是它们的地址.
EKF符号表结构:
ELF文件中的符号表往往是文件中的一个段,段名为".symtab".符号表的结构很简单,是一个Elf32_Sym结构的数组.Elf32的结构定义如下:
typedef struct {
Elf32_Word st_name; //符号名
Elf32_Addr st_value; //符号值
Elf32_Word st_size; //符号大小
unsigned char st_info; //符号类型和绑定信息
unsigned char st_other; //该成员目前为0,没用
Elf32_Half st_shndx; //符号所在的段
} Elf32_Sym;
其中,符号类型和绑定信息(st_info
)如下:
符号绑定信息:
宏定义名 | 值 | 说明 |
---|---|---|
STB_LOCAL | 0 | 局部符号,对于目标文件的外部不可见 |
STB_GLOBAL | 1 | 全局符号,外部可见 |
STB_WEAK | 2 | 弱引用 |
符号类型:
宏定义名 | 值 | 说明 |
---|---|---|
STT_NOTYPE | 0 | 未知类型符号 |
STT_OBJECT | 1 | 该符号是个数据对象,比如变量,数组等 |
STT_FUNC | 2 | 该符号是个函数或其他可执行代码 |
STT_SECTION | 3 | 该符号表示一个段,这种符号必须是STB_LOCAL的 |
STT_FILE | 4 | 该符号表示文件名,一般都是该目标文件所对应的源文件名 |
示例:使用readlf命令查看目标文件的符号表:
$ readelf -s SimpleSection.o
Symbol table '.symtab' contains 17 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS SimpleSection.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_var.1802
7: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_var2.1803
8: 0000000000000000 0 SECTION LOCAL DEFAULT 7
9: 0000000000000000 0 SECTION LOCAL DEFAULT 8
10: 0000000000000000 0 SECTION LOCAL DEFAULT 6
11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var
12: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var
13: 0000000000000000 36 FUNC GLOBAL DEFAULT 1 func1
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
16: 0000000000000024 51 FUNC GLOBAL DEFAULT 1 main
上表中,第二列Value为符号值,第三列为符号大小,第四列和第五列为符号类型和绑定信息,第六列Vis在C/C++中未使用,可以忽略,Ndx列表示该符号所属的段,最后一列为符号名称.可以看到,func1和main函数是定义在SimleSection.c
里面的,它们所在的位置为代码段,所以Ndx为1.,即在SimpleSection.o
中,.text
段下标为1.
符号修饰与函数签名:
约在20世纪70年代以前,编译器编译源代码产生目标文件时,符号名与相应的变量和函数的名字是一样的。比如一个汇编源代码里面包含了一个函数foo,那么汇编器将它编译成目标文件以后,foo在目标文件中的相对应的符号名也是foo。当后来UNIX平台和C语言发明时,已经存在了相当多的使用汇编编写的库和目标文件。这样就产生了一个问题,那就是如果一个C程序要使用这些库的话,C语言中不可以使用这些库中定义的函数和变量的名字作为符号名,否则将会跟现有的目标文件冲突。比如有个用汇编编写的库中定义了一个函数叫做main,那么我们在C语言里面就不可以再定义一个main函数或变量了。同样的道理,如果一个C语言的目标文件要用到一个使用Fortran语言编写的目标文件,我们也必须防止它们的名称冲突。为了防止类似的符号名冲突,UNIX下的C语言就规定,C语言源代码文件中的所有全局的变量和函数经过编译以后,相对应的符号名前加上下划线“_”。而Fortran语言的源代码经过编译以后,所有的符号名前加 上“_”,后面也加上“_”。比如一个C语言函数“foo”,那么它编译后的符号 名就是“_foo”;如果是Fortran语言,就是“_foo_”。
这种简单而原始的方法的确能够暂时减少多种语言目标文件之间的符号冲突的概率,但还是没有从根本上解决符号冲突的问题。比如同一种语言编写的目标文件还有可能会产生符号冲突,当程序很大时,不同的模 块由多个部门(个人)开发,它们之间的命名规范如果不严格,则有可 能导致冲突。于是像C++这样的后来设计的语言开始考虑到了这个问 题,增加了名称空间(Namespace)的方法来解决多模块的符号冲突问题.
但是随着时间的推移,很多操作系统和编译器被完全重写了好几遍,比如UNIX也分化成了很多种,整个环境发生了很大的变化,上面所提到的跟Fortran和古老的汇编库的符号冲突问题已经不是那么明显了。在现在的Linux下的GCC编译器中,默认情况下已经去掉了在C语言符号前加“_”的这种方式;但是Windows平台下的编译器还保持的这样的传统,比如Visual C++编译器就会在C语言符号前加“_”,GCC在Windows平台下的版本(cygwin、mingw)也会加“_”。
C++符号修饰:
考虑C++函数重载以及名称空间:两个相同名字的函数:func(int)
和func(double)
,尽管函数名字相同,但是参数列表不同,那么编译器和链接器在链接过程中如何区分这两个函数呢?这就是符号修饰(Name Decoration)或符号改编(Name Mangling)的作用.
如下列代码:
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);
};
}
这段代码中有6个同名函数叫func,只不过它们的返回类型和参数及所在的名称空间不同。我们引入一个术语叫做函数签名(Function Signature),函数签名包含了一个函数的信息,包括函数名、它的参数类型、它所在的类和名称空间及其他信息。函数签名用于识别不同的函数,就像签名用于识别不同的人一样,函数的名字只是函数签名的一部分。由于上面6个同名函数的参数类型及所处的类和名称空间不同,我们可以认为它们的函数签名不同。
在编译器及链接器处理符号时,它们使用某种名称修饰的方法,使得每个函数签名对应一个修饰后名称(Decorated Name)。编译器在将C++源代码编译成目标文件时,会将函数和变量的名字进行修饰,形成符号名,也就是说,C++的源代码编译后的目标文件中所使用的符号名是相应的函数和变量的修饰后名称。C++编译器和链接器都使用符号来识别和处理函数和变量,所以对于不同函数签名的函数,即使函数名相同,编译器和链接器都认为它们是不同的函数。上面的6个函数签名在GCC编译器下,相对应的修饰后名称如下表:
函数签名 | 修饰后名称(符号名) |
---|---|
int func(int) | _Z4funci |
float func(float) | _Z4funcf |
int C::func(int) | _ZN1C4funcEi |
int C::C2::func(int) | _ZN1C2C2funcEi |
int N::func(int) | _ZN1N4funcEi |
int N::C::func(int) | _ZN1N1C4funcEi |
签名和名称修饰机制不光被使用在函数上,C++中的全局变量和静态变量也有同样的机制.
有关extern “C”:
为了与C语言兼容,C++有一个用来声明或定义一个C的符号的extern "C"
关键字用法:
extern "C"{
int func(int);
int var;
}
C++编译器会将在"extern C"的大括号内部的代码当作C语言代码处理,所以这部分代码也不会被C++的名称修饰机制影响. 如果要单独声明某个函数或变量为C语言的符号,也可以使用如下格式:
extern "C" int func(int);
extern "C" int var;
小实验:
#include <stdio.h>
namespace myname{
int var=42;
}
extern "C" double i= _ZN6myname3varE;
int main()
{
printf("_ZN6myname3varE=%d\n",_ZN6myname3varE);
return 0;
}
上面的代码在myname的名称空间定义了一个全局变量var.根据gcc名称修饰规则,这个变量修饰后的名称为_ZN6myname3varE
,然后手工使用extern "C"
的方法声明外部符号_ZN6myname3varE
,并将其打印出来.本人照着书上这么做的时候,在下面g++的时候报错redeclaration,如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/ad787c824c31b7968dee0d469d714265.jpeg)
将extern "C"
语句注释后成功g++
![](https://img-blog.csdnimg.cn/510db3d8f5bc4643919cb82fbecf1d30.jpg?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASmVhbGFyIEp1bm8=,size_18,color_FFFFFF,t_70,g_se,x_16)
注释之后没有问题(g++版本的问题?)
![](https://img-blog.csdnimg.cn/5c3b0e2ad0414668866b8993167568ed.jpg)
弱符号与强符号:
我们经常碰到一种情况叫做符号重复定义.多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候就会出现符号重复定义的错误.
对于C/C++,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号.也可以通过gcc的"__attribute__((weak))
"来定义任何一个强符号为弱符号. 强符号和弱符号都是针对定义来说的,不是针对符号的引用.比如下面这段程序:
extern int ext;
int weak;
int strong=1;
__attribute__((weak)) weak2 = 2;
int main(){
return 0;
}
上面这段程序,weak
和weak2
是弱符号,strong
和main
是强符号.ext
既非强符号也非弱符号,它只是一个外部变量的引用.
强引用与弱引用
目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们需要被正确决议,如果没有找到该符号的定义,链接器就会报告符号未定义错误,这种被称为强引用(Strong Reference).与之对应还有一种弱引用(Weak Reference),在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果未定义,则链接器对该引用不报错.链接器处理强引用和弱引用的过程几乎一样,只是对于未定义的弱引用,链接器不认为它是一个错误.