符号表的一些事一些情

Posted by HHP on September 15, 2019

前言

做操作系统相关的开发,肯定都会接触到目标文件和可执行文件等等的分析,今天就和大家交流一下linux下elf文件的符号和符号表的知识以及在工程实践中会有什么应用。

正文

符号表是什么?

我们知道,在编译的四个阶段中,最后一步链接的本质就是将不同的目标文件糅合到一块,生成最终可执行的二进制文件。而目标文件的互相糅合,实质上就是目标文件之间对地址的引用,就是对函数和变量的地址的引用。那怎么来完成这个过程呢?人们就想到了在每一个目标文件中存放一张记录了目标文件中所用到的所有符号以及其对应的符号值的表,链接的时候,目标文件之间就会互相寻找自己需要的符号来完成链接的过程,而这张表,就叫做符号表。

符号表长什么样

上面提到,一个程序,从源码到生成可执行文件有四个阶段:预编译,编译,汇编,链接。在汇编阶段,即生成目标文件的时候,就会产生符号表。每一个生成的elf文件都有符号表。符号表中的符号和源码中的变量名和函数名是一一对应的(对应的意思不代表生成的符号和源码中的函数变量名一定是一样的,而只是一种映射关系,典型的是在c++编译时,生成的符号和源码的符号是完全不一样的)。如下,我有一段测试代码:

//test.c

static int a;
int b;
static void test1(){
    return;
}

void test2(){
    return;
}

生成目标文件之后,用read -s test.o命令即可查看其中的符号表,如下:

[email protected]:~/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就能看到。下面所示,有几个符号像 [email protected]_2.2.5 就是说使用了GLIBC_2.2.5版本,如果在动态加载时使用了不同版本的glibc,会提示没有对应版本的符号,从而加载失败。

[email protected]:~/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 [email protected]_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 [email protected]_2.2.5 (2)

在Linux下,我们可以通过版本脚本version-script来控制符号版本。只需要在编译动态库是加上-Wl,--version-script xxx即可。xxx是自定义的脚本,格式如下:

VER_1.1{
	global:
		test;
		...
	local:
		*;   //一般情况下local里面打*号,表示除了global里面写的那些符号之外全部都是local的
};

进阶:玩转符号表

前面只是讲了符号表的理论知识,只是让大家里了解一下编程背后的原理,上面说的东西我们是不需要改动符号表的。那么这里来点硬核的东西,实际上我们是可以直接修改符号表来达成我们的目的的!下面结合我自己的实践给大家讲一下。

先说一下背景:某一天,我接到一个需求,这个需求要使用到openssl库里面的一些函数来完成的。但是问题来了,我看了一看我需要用到的其中一个函数ssl_cert_free,在libssl.so的符号表里显示是LOCAL类型的函数符号,这意味着我不能直接使用这个函数,如下图所示:

[email protected]:~/Desktop/ease_ssl_faker/QAT/openssl/openssl-1.1.0f_handshaker$ readelf -s libssl.so |grep ssl_cert_free
   357: 00000000000279a0   264 FUNC    LOCAL  DEFAULT   13 ssl_cert_free

我可以修改这个符号吗?答案是可以的。有一个方法是将静态库加上version script重新输出一个新的libssl.so。手先我修改了ssl.map,将ssl_cert_free等函数放到global的那个大括号里,然后用命令gcc -shared -Wl,-Bsymbolic -Wl,-soname=libssl.so.1.1 -o ./libssl.so.1.1 -Wl,--whole-archive,--version-script=ssl.map ./libssl.a -Wl,--no-whole-archive -L. -lcrypto -ldl -g就搞掂了(。这里的重点是--whole-archive参数,可以把在其后面出现的静态库包含的函数和变量输出到动态库。执行命令之后,再输出libssl.so的符号表,已经发现ssl_cert_free变成GLOBAL了,如下图:

[email protected]:/home/haley/Desktop/ease_ssl_faker/QAT/openssl/openssl-1.1.0f_handshaker# readelf -s libssl.so.1.1 |grep ssl_cert_free
   563: 0000000000027a00   264 FUNC    GLOBAL DEFAULT   13 [email protected]@OPENSSL_1_1_0

除了上面说的将静态库包括里面的符号表重新输出一个新的libssl.so这个方法之外,还有另外一个方法就是用ar -x libssl.a命令将libssl.a的目标文件释放,然后找到对应的目标文件ssl_lib.o,用objcopy --globalize-symbol test ssl_lib.o ssl_lib_new.o命令将test符号改成GLOBAL,最后再链接,不过这个方法用在这里有点麻烦。

附objcopy和ar命令的常见用法:

ar -x xxx.a  //释放其中的.o文件
    
objdump -t xxx.o //查看导出符号表,可以看到导出的函数和引用的函数

objcopy --localize-symbol function_1 xxx.o xxx_new.o   //把函数设置为本地

objcopy --globalize-symbol function_2 xxx.o xxx_new.o //把函数设置为全局可见

objcopy --redefine-sym old_func=new_func xxx.o xxx_new.o //重命名函数名

ar cqs xxx.a xxx_new.o //打包为静态库

以上就是我关于符号表的一些理解,由于篇幅问题,还有一些知识没能详细讲到,像符号名称的修饰等等,其实展开来说是很大的学问,这里只是简单提了一下。有兴趣的朋友可以自行深入研究~~

–END–