什么是符号和符号可见性
符号是谈及对象文件、链接等内容时的基本术语之一。实际上,在 C/C++ 语言中,符号是很多用户定义的变量、函数名称以 及一些名称空间、类/结构/名称等的对应实体。例如,当我们定义非静态全局变量或非静态函数时,C/C++ 编译器就会在对象文 件中生成符号,这些符号对于链接器(linker)确定不同模块(对象文件、动态共享库、可执行文件)是否会共享相同的数据或 代码很有用。
尽管变量和函数都可能会在模块之间共享,但是对象文件之间的变量共享更为常见。例如,程序员可能会在 a.c 中声明一个 变量:
extern int shared_var;
却在 b.c 中定义该变量:
int shared_var;
这样,两个 shared_var 符号会出现在已编译的对象 a.o 和 b.o 中,最后在链接器解析之后,a.o 中的符号会共享 b.o 的 地址。但是,人们很少让变量在共享库和可执行文件之间共享。对于此类模块,通常只会让函数对其他模块可见。有时,我们将 此类函数称之为 API,因为我们觉得该模块是为其他模块提供调用的接口。我们也把这种符号称为导出的 (exported),因为它对 其他模块可见。注意,此可见性只在动态链接时有效,因为共享库通常在程序运行时被加载为内存映像的一部分。因此,符号可 见性 (symbol visibility) 是所有全局符号的一个用于动态链接的属性。
为什么需要控制符号可见性
在不同的平台上,XL C/C++ 编译器可能会选择导出或者不导出模块中的所有符号。例如,在 IBM PowerLinux 平台上创建 Executable and Linking Format (ELF) 共享库时,默认情况下,所有的符号都会导出。在 POWER 平台上的 AIX 系统中创建 XCOFF 库时,当前 XL C/C++ 编译器在没有工具的帮助下可能会选择不导出任何符号。还有其他方式允许程序员逐个地决定符号 可见性(这是本系列下一部分要介绍的内容)。但是,一般不建议导出模块中的所有符号。程序员可以根据需要导出符号。这不 仅对库的安全有益,也对动态链接时间有益。
程序员选择导出所有符号时,存在很高的风险,链接时可能会出现符号冲突,尤其是当模块是由不同的开发人员开发的时。因 为符号是低级别的概念,所以它不涉及到作用域。只要有人链接一个跟您的库具有相同符号名称的库,当进行链接器解析时,该 库就可能会意外地覆盖您自己的符号(但愿会给出一些警告或错误信息)。大多数情况下,此类符号从来不会被从库设计者的角 度去使用。因此,为符号创建有限制、有含义(经过深思熟虑)的名称,对于避免此类问题有很大帮助。
对于 C++ 编程,现在越来越注重性能了。然而,由于对其他库的依赖性以及使用特定的 C++ 特性(比如模板),编译器/链 接器趋向于会使用和生成大量的符号。因此,导出所有符号会减慢程序速度,并耗用大量内存。导出有限数量的符号可以缩短动 态共享库的加载和链接时间。此外,也支持编译器角度的优化,这意味着会生成更有效的代码。
以上关于导出所有符号的缺点解释了为什么一定要定义符号可见性。在本文中,我们将提供一些解决方案来控制动态共享对象 (DSO) 中的符号。用户可以使用不同的方式解决相同的问题,我们将提议特定平台应该首选哪种解决方式。
控制符号可见性的方式
在后面的讨论中,我们将用到下面的 C++ 代码片段:
清单 1. a.C
int myintvar = 5;
int func0 () {
return ++myintvar;
}
int func1 (int i) {
return func0() * i;
}
在 a.C 中,我们定义了一个变量 myintvar,以及两个函数 func0 和 func1。默认情况下,在 AIX 平台上创建共享库时,编 译器和链接器以及 CreateExportList 工具会让所有三个符号都可见。我们可以利用 dump 二进制工具从 Loader Symbol Table Information 检查这一情况:
$ xlC -qpic a.C -qmkshrobj -o libtest.a
$ dump -Tv libtest.a
***Loader Symbol Table Information***
[Index] Value Scn IMEX Sclass Type IMPid Name
[0] 0x20000280 .data EXP RW SECdef [noIMid] myintvar
[1] 0x20000284 .data EXP DS SECdef [noIMid] func0__Fv
[2] 0x20000290 .data EXP DS SECdef [noIMid] func1__Fi
这里,“EXP”表示符号是导出的。函数名称 func0 和 func1 被 C++ 重整规则(mangling rule)进行了重整( 但是,不难猜出名称的意思)。dump 工具的 -T 选项显示 Loader Symbol Table Information,动态链接器将用到此信息。在本 例中,a.C 中的所有符号都被导出。但是从库编写者的角度,本例中我们可能只想导出 func1。全局符号 myintvar 和函数 func0 被认为只保持/改变内部状态,或者说只在局部使用。因此,对于库编写者来说,让它们不可见至关重要。
我们至少有三种方式可以达此目的。包括:使用 static 关键字,定义 GNU visibility 属性,以及使用导出列表。每种方式 都有各自不同的功用和缺点。下面就来看看这些方式。
1. 使用 static 关键字
C/C++ 中的 static 可能是一个最常用的关键字,因为它可以为变量指定作用域和存储。对于作用域,可以说成它为文件中的 符号禁用了外部链接。这意味着,带有关键字 static 的符号永远不会是可链接的,因为编译器不为链接器留下关于此符号的任 何信息。这是一种语言级别的控制,是最简单的一种隐藏符号的方式。
我们来给上面的例子添加 static 关键字吧:
清单 2. b.C
static int myintvar = 5;
static int func0 () {
return ++myintvar;
}
int func1 (int i) {
return func0() * i;
}
生成共享库并再次查看 Loader Symbol Table Information,可以看到预期的效果:
$ xlC -qpic a.C -qmkshrobj -o libtest.a
$ dump -Tv libtest.a
***Loader Symbol Table Information***
[Index] Value Scn IMEX Sclass Type IMPid Name
[0] 0x20000284 .data EXP DS SECdef [noIMid] func1__Fi