符号表
什么是符号表?
在编译的四个阶段中,最后一步链接的本质就是将不同的目标文件糅合到一块,生成最终可执行的二进制文件。而目标文件的互相糅合,实质上就是目标文件之间对地址的引用,就是对函数和变量的地址的引用。那怎么来完成这个过程呢?人们就想到了在每一个目标文件中存放一张记录了目标文件中所用到的所有符号以及其对应的符号值的表,链接的时候,目标文件之间就会互相寻找自己需要的符号来完成链接的过程,而这张表,就叫做符号表。
符号表长什么样
上面提到,一个程序,从源码到生成可执行文件有四个阶段:预编译,编译,汇编,链接。在汇编阶段,即生成目标文件的时候,就会产生符号表。每一个生成的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命令也用在目标文件,因为目标文件是还没链接的,过早删除符号表会导致链接不了。
Strip
为什么需要用strip去符号?strip去符号去了什么?
符号表的主要作用在于链接时按着符号寻址,但在程序运行时就没有作用了,因此这个表即使去掉后也不会影响程序的运行。但是如果是动态链接函数,需要在运行时进行动态链接.
所以ELF文件里有两张符号表,一张叫符号表(.symtab),另一张叫动态符号表(.dynsym)。当去符号的时候,只用去掉符号表,而保留动态符号表
【参考文献】