符号表
符号表是什么?
我们知道,在编译的四个阶段中,最后一步链接的本质就是将不同的目标文件糅合到一块,生成最终可执行的二进制文件。而目标文件的互相糅合,实质上就是目标文件之间对地址的引用,就是对函数和变量的地址的引用。那怎么来完成这个过程呢?人们就想到了在每一个目标文件中存放一张记录了目标文件中所用到的所有符号以及其对应的符号值的表,链接的时候,目标文件之间就会互相寻找自己需要的符号来完成链接的过程,而这张表,就叫做符号表。
符号表长什么样
上面提到,一个程序,从源码到生成可执行文件有四个阶段:预处理,编译,汇编,链接。在汇编阶段,即生成目标文件的时候,就会产生符号表。每一个生成的elf文件都有符号表。符号表中的符号和源码中的变量名和函数名是一一对应的(对应的意思不代表生成的符号和源码中的函数变量名一定是一样的,而只是一种映射关系,典型的是在c++编译时,生成的符号和源码的符号是完全不一样的)。如下,我有一段测试代码:
//test.c static int a; int b; static void test1(){ return; } void test2(){ return; }
生成目标文件之后,用read -s test.o
命令即可查看其中的符号表,如下:
haley@ubuntu:~/Desktop$ readelf -s test.o Symbol table '.symtab' contains 12 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 3: 0000000000000000 0 SECTION LOCAL DEFAULT 2 4: 0000000000000000 0 SECTION LOCAL DEFAULT 3 5: 0000000000000000 4 OBJECT LOCAL DEFAULT 3 a 6: 0000000000000000 7 FUNC LOCAL DEFAULT 1 test1 7: 0000000000000000 0 SECTION LOCAL DEFAULT 5 8: 0000000000000000 0 SECTION LOCAL DEFAULT 6 9: 0000000000000000 0 SECTION LOCAL DEFAULT 4 10: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM b 11: 0000000000000007 7 FUNC GLOBAL DEFAULT 1 test2
其中,value叫符号值,对于变量和函数而言,符号值就是他们的地址。size是一个符号值所占字节数,type是符号的类型,像变量的类型就是OBJECT,函数的类型就是FUNC。Bind列是符号的作用域,LOCAL表示是局部符号,GLOBAL表示是全局符号,WEAK表示是弱符号等等。Vis列表示符号的可见性,一般比较少用到。Ndx列表示符号所属的段的index(.text段,.data段等等)。Name列就是刚才说的与变量函数名一一对应的符号名了。在上面的例子中,b和test2是global的,而a和test1是local的。
来一个实际的例子说明一下:
//main.c void test2(); int main(){ test2(); return 0; } //test.c static int a; int b ; static void test1(){ return; } void test2(){ return; }
上面有两个c源文件,然后我分别生成对应的目标文件main.o和test.o,导出他们的符号表如下:
//main.o的符号表 Symbol table '.symtab' contains 11 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.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 6 6: 0000000000000000 0 SECTION LOCAL DEFAULT 7 7: 0000000000000000 0 SECTION LOCAL DEFAULT 5 8: 0000000000000000 21 FUNC GLOBAL DEFAULT 1 main 9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_ 10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND test2 //注意这里,Ndx是UND的,且value为0 // test.o的符号表 Symbol table '.symtab' contains 12 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 3: 0000000000000000 0 SECTION LOCAL DEFAULT 2 4: 0000000000000000 0 SECTION LOCAL DEFAULT 3 5: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 a 6: 0000000000000000 7 FUNC LOCAL DEFAULT 1 test1 7: 0000000000000000 0 SECTION LOCAL DEFAULT 5 8: 0000000000000000 0 SECTION LOCAL DEFAULT 6 9: 0000000000000000 0 SECTION LOCAL DEFAULT 4 10: 0000000000000000 4 OBJECT WEAK DEFAULT 3 b 11: 0000000000000007 7 FUNC GLOBAL DEFAULT 1 test2
上面main.c引用了test.c的test2函数,但是在链接之前,main.o并不知道test2函数的定义和地址,所以main.o的符号表里将test2标记为UND(undefine的意思),地址值也缺省为0000000000000000,等待链接的时候寻找到定义了test2函数的目标文件的符号表,提取出test2函数的地址值。
下面来验证一下,用gcc test.o main.o
将它们链接到一块,生成a.out可执行文件,导出a.out的符号表:
//a.out的符号表 Symbol table '.dynsym' contains 6 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2) 3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 5: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2) Symbol table '.symtab' contains 66 entries: Num: Value Size Type Bind Vis Ndx Name ...... 31: 00000000000005f0 0 FUNC LOCAL DEFAULT 13 frame_dummy 32: 0000000000200df0 0 OBJECT LOCAL DEFAULT 18 __frame_dummy_init_array_ 33: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c 34: 0000000000201018 4 OBJECT LOCAL DEFAULT 23 a 35: 00000000000005fa 7 FUNC LOCAL DEFAULT 13 test1 36: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c 37: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c 38: 0000000000000834 0 OBJECT LOCAL DEFAULT 17 __FRAME_END__ 49: 0000000000201010 0 NOTYPE GLOBAL DEFAULT 22 _edata 50: 0000000000000694 0 FUNC GLOBAL DEFAULT 14 _fini 51: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_ 52: 0000000000201000 0 NOTYPE GLOBAL DEFAULT 22 __data_start 53: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 54: 0000000000201008 0 OBJECT GLOBAL HIDDEN 22 __dso_handle 55: 00000000000006a0 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used 56: 0000000000000620 101 FUNC GLOBAL DEFAULT 13 __libc_csu_init 57: 0000000000201020 0 NOTYPE GLOBAL DEFAULT 23 _end 58: 00000000000004f0 43 FUNC GLOBAL DEFAULT 13 _start 59: 0000000000201010 0 NOTYPE GLOBAL DEFAULT 23 __bss_start 60: 0000000000000608 21 FUNC GLOBAL DEFAULT 13 main 61: 0000000000000601 7 FUNC GLOBAL DEFAULT 13 test2 //重点关注 62: 0000000000201010 0 OBJECT GLOBAL HIDDEN 22 __TMC_END__
这里看到有两个符号表,一个是.dynsym
,一个是.symtab
。这里简单介绍一下,.dynsym
是动态符号表,是动态链接的时候用到的,而.symtab
是静态符号表,静态链接时用到的。这里我们只看.symtab
。这里看到Num为61的行,可以发现test2已经不是UND了,而且地址值也有了是0000000000000601。说明链接是成功的!
这里插个题外话,在大型的项目中,其实链接完之后,因为已经链接完成了(已经完成了重定位操作),可执行文件中的.symtab,即静态符号表其实已经变得可有可无了。所以可以使用strip
命令将静态符号表等无用得信息删掉,减小二进制文件的大小。但是.dynsym仍需保留,因为动态链接是在装载文件的时候完成的,符号信息不能删除。还有就是千万不要自作多情把strip命令也用在目标文件,因为目标文件是还没链接的,过早删除符号表会导致链接不了。
强符号和弱符号
在c/c++编程中,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。我们可以通过gcc的__attribute__ ((weak))
来将一个强符号转换成弱符号。针对强弱符号,有以下一些规则要遵守(参考自《程序员的自我修养》):
- 不允许强符号被多次定义,若有多个强符号重复定义,链接时会报错。
- 如果一个符号在目标文件中是强符号,在其他文件中是弱符号,那么链接时取得是强符号。
- 如果一个符号在所有的目标文件中都是弱符号,那么选择其中占用空间最大的那一个。比如说变量a在目标文件中是int型,在目标文件B中是double型,那么将选择double型的那个符号进行链接。
对应于强符号和弱符号,有强引用和弱引用。强引用的意思是在链接的阶段,符号要有正确的寻址,若没找到该符号的定义,连接器就会报未定义的错误。弱引用的意思是在连接阶段,若能找到该符号的定义,那么连接器将引用该符号;若没被定义也没关系,连接器不报错,而是默认其为零。我们可以使用gcc的参数__attribute__ ((weakref))
来将一个强引用转换成弱引用。
弱符号和弱引用在实践中有什么作用呢?先说弱符号,比如在库中定义的弱符号可以被用户自定义的强符号所覆盖。再说弱引用,比如程序可以定义某些拓展模块的引用定义为弱引用,当我们将拓展模块和程序连接到一块时,功能模块可以正常使用,当我们去除这些功能模块函数时,由于是弱引用,连接器只是把符号值设为零,程序还是可以正常链接,不会报错。不过,在设计弱引用时,一定要注意做判断,比如我有一个test弱引用,我们要用if(test){} else{}来去判断是否有连接test函数的定义,否则会出现意想不到的运行错误!!
extern “C”
c和c++的符号修饰机制是不同的,比如说对应一个变量名test,c编译器输出的符号还是test
,而c++编译器输出的符号有可能是_ZN12_GLOBAL__N_14testE
。因此如果你的代码一部分使用c,一部份使用c++,那么整个工程连接的时候就会出现问题。extern “C”
生来就是用来解决这个问题的。用了extern “C”
,c++编译器会将大括号内部的代码当作C语言处理,比如extern “C”{int a;}
,那么在用c++编译器编译时,变量a的符号修饰将按C语言的标准进行。但是C编译器不支持extern “C”
,只有C++编译器才支持,所以一般我们都会用下面这种条件编译的形式:
#ifdef __cplusplus extern "C" { #endif //这里放C语言定义的函数或变量声明 #ifdef __cplusplus } #endif
符号版本机制
符号也是可以有版本的,当然,可有可无。符号版本机制的出现主要是为了解决动态链接库SO_NAME出现的次版本交会问题。实质上,GLIBC从2.1版本之后就一直使用符号版本机制。所谓符号版本控制,说白了其实就是在原本的符号后面加上版本号形成一个新的符号名称,随便生成一个a.out就能看到。下面所示,有几个符号像 __libc_start_main@GLIBC_2.2.5
就是说使用了GLIBC_2.2.5版本,如果在动态加载时使用了不同版本的glibc,会提示没有对应版本的符号,从而加载失败。
haley@ubuntu:~/Desktop/git_test$ readelf -s a.out Symbol table '.dynsym' contains 7 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2) 3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND test2 5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
在Linux下,我们可以通过版本脚本version-script来控制符号版本。只需要在编译动态库是加上-Wl,--version-script xxx
即可。xxx是自定义的脚本,格式如下:
VER_1.1{ global: test; ... local: *; //一般情况下local里面打*号,表示除了global里面写的那些符号之外全部都是local的 };