问题描述

在设计和开发对外提供的接口库文件(静态链接库or动态链接库)时,对自有内部实现的细节往往有一定的保护性需求。

基本概念

而我们知道,在常规的编译中,生成的链接库文件在头部会有明确的关于 relocation table 和 symbol table 的信息,通俗的解释一下,relocation table 记录了这个库所需要使用的外部函数接口,symbol table 则记录了这个文件内可以提供的接口符号。在实际运行或者执行ld链接的时候,链接器会在各个文件的两个table中查询,以期使得一些文件中的 symbol table 记录的符号满足另一些文件 relocation table 中的需求。并按照要求修改 relocation table 中记录的位置,修改相应的函数调用指令,使得最终调用指令能够正确的将程序流导向需要的函数。

这里面的符号,就是编译器根据一定规则生成的一个名称,能够唯一确定的描述一个函数。这里的符号生成规则与编译器有关,这也是为什么不同编译器出来的链接库文件往往不可以混用。具体的符号细节不作太多的赘述,不过其中一些特点还是要明确的:

  1. 符号可以唯一确定一个函数接口,无论在语言中是否存在重载,是否有继承关系,在编译器映射的时候会映射成不通的symbol,所以不会有歧义产生。C语言编译器通常考虑函数名称,返回类型和参数类型来构造,C++语言编译器则在必要的时候还会考虑namespace,类等信息。
  2. 在符号这个层面上,是没有上下文的,此时不再存在对namespace等的限制,如果单独手写一些汇编指令的话,在这里是可以违背语言设计调用到很多奇奇怪怪的函数的,(比如将一个类的函数应用到与他毫无关系的别的类对象上,当然这会发生什么就不一定了)
  3. 在没有明确说明的情况下,所有函数都会生成符号,这些符号都在 symbol table 中。也即均可被链接使用。 -->> 这也导致了前面的问题,即封装后大量的内部接口依旧可用。

查询符号相关的指令

nm 是GNU提供的一个分析文件符号表的工具。他的使用很简单,nm <filename>即可打印出一个文件所包含的符号表信息,针对动态链接库,可以使用nm -D <filename>。其中包含的信息非常丰富,可以通过man nm查看具体的解释。

假设有如下C代码,我们把它编译成链接库文件,并尝试查看符号表:

void foo(double a, int b) {}
void foo1(int a) {}
int foo2(double c) {}
➜  test nm libtest.so 
0000000000003e60 d _DYNAMIC
0000000000004000 d _GLOBAL_OFFSET_TABLE_
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
00000000000020a0 r __FRAME_END__
0000000000002000 r __GNU_EH_FRAME_HDR
0000000000004020 d __TMC_END__
                 w __cxa_finalize@@GLIBC_2.2.5
0000000000001090 t __do_global_dtors_aux
0000000000003e58 t __do_global_dtors_aux_fini_array_entry
0000000000004018 d __dso_handle
0000000000003e50 t __frame_dummy_init_array_entry
                 w __gmon_start__
0000000000001110 t _fini
0000000000001000 t _init
0000000000004020 b completed.7286
0000000000001020 t deregister_tm_clones
00000000000010e9 T foo
00000000000010f8 T foo1
0000000000001102 T foo2
00000000000010e0 t frame_dummy
0000000000001050 t register_tm_clones

可以看到三个函数都被记录在符号表中了,(动态库相比静态库会多很多结构性的东西,不在本文讨论范围)

按需暴露接口

针对前面所描述的问题,我们这里需要限制暴露的接口,假设我们对外只暴露foo这一个函数,而其他两个函数为具体的内部实现,我们希望将其隐藏。这里就用到一个编译机制,即 Version Script。

Version Script 当然不仅仅是一个控制符号的工具,我们这里只介绍最简单的使用。

在源代码同级,放置一个 export_symbols 的文件,内容如下:

VERS_0.1 {
	global:
    	foo;
	local:
		*;
};

然后我们稍微修改一下编译指令:

gcc -fPIC --shared test.c -o libtest.so -Wl,--version-script=./export_symbols

对比一下nm输出:

➜  test nm libtest.so 
0000000000000000 A VERS_1.0
0000000000003e00 d _DYNAMIC
0000000000004000 d _GLOBAL_OFFSET_TABLE_
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
00000000000020d8 r __FRAME_END__
0000000000002008 r __GNU_EH_FRAME_HDR
0000000000004028 d __TMC_END__
                 w __cxa_finalize@@GLIBC_2.2.5
00000000000010b0 t __do_global_dtors_aux
0000000000003df8 t __do_global_dtors_aux_fini_array_entry
0000000000004020 d __dso_handle
0000000000003df0 t __frame_dummy_init_array_entry
                 w __gmon_start__
000000000000114c t _fini
0000000000001000 t _init
0000000000004028 b completed.7286
0000000000001040 t deregister_tm_clones
000000000000112f T foo
0000000000001109 t foo1
0000000000001123 t foo2
0000000000001100 t frame_dummy
                 U puts@@GLIBC_2.2.5
0000000000001070 t register_tm_clones

可以发现除了暴露接口以外,其余的函数从T标记变为了t标记。文档中介绍,大写字母与小写字母的区别是,大写字母可被外部链接,小写字母标示的符号只能被此文件内部使用。

通过nm -D可以更直观的表现:

➜  test nm -D libtest.so.old                    
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 w __cxa_finalize
                 w __gmon_start__
000000000000113f T foo
0000000000001119 T foo1
0000000000001133 T foo2
                 U puts
➜  test nm -D libtest.so
0000000000000000 A VERS_1.0
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 w __cxa_finalize
                 w __gmon_start__
000000000000112f T foo
                 U puts

很显然,只有我们指定的global符号依旧存在,其余的符号都不允许链接使用了。

Version Script 写法

前面使用了一个简单地例子,这里继续解释其中的含义,详细的语法可以在这里查到。

这里主要定义了global和local两个部分,简单地理解就是global可以供外部调用,local则只能由这个文件自己使用,而不能被外部使用。

这里支持多重列举,也支持通配符等写法。比如

VERS_0.1 {
	global:
    	foo;
    	init*;
	local:
    	foo1;
    	foo2;
    	foo3;
		*;
};

当然,对于C++语言,因为生成符号不再仅仅根据函数名了,所以要使用扩展写法,比如这样子:

VERS_1.0 {
	global:
		extern "C++" {
			YourNameSpace::SomeClass::*;
		};
	local:
		*;
};

隐藏所有符号

前面大家或许注意到了,在使用了 Version Script 后,虽然local的符号不再可以被外部使用了, 但是他们依旧可以通过nm指令看到。毕竟local的符号也是符号嘛。

当然,这里也有解决办法。使用gcc的-s指令,这个指令指示结果清除 relocation table 和 symbol table 信息,联合前面的 Version Script 一起使用,就可以彻底隐藏内部的实现细节了。

➜  test gcc -fPIC --shared test.c -o libtest.so -Wl,--version-script=./export_symbols -s
➜  test nm libtest.so 
nm: libtest.so: no symbols
➜  test nm -D libtest.so
0000000000000000 A VERS_1.0
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 w __cxa_finalize
                 w __gmon_start__
000000000000112f T foo
                 U puts