当前位置:首页 >> 编程开发 >> Visual C++ >> 内容

控制共享库的符号可见性(一) 符号可见性简介

时间:2015/5/19 21:30:47 作者:平凡之路 来源:xuhantao.com 浏览:

什么是符号和符号可见性

符号是谈及对象文件、链接等内容时的基本术语之一。实际上,在 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

现在,如信息所示,只有 func1 被导出。然而,尽管 static 关键字可以隐藏符号,但是它还定义了一条额外的规则,即变 量或函数只可以在定义它们的文件范围内使用。因此,如果我们定义:

extern int myintvar;

后面,在文件 b.C 中,您可能想要从 a.o 和 b.o 构建 libtest.a。您这样做时,链接器将显示一条错误消息,指出定义在 b.C 中的 myintvar 无法被链接,因为链接器没有在其他地方找到定义。这中断了相同模块内的数据/代码共享,而这种共享通常 是程序员所需要的。因此,这种方法更多地用作文件内变量/函数的可见性控制,而不用于低级别符号的可见性控制。实际上,大 多数变量/函数不会依赖于 static 关键字来控制符号可见性。因此,我们可以考虑第二种方法:
2. 定义 visibility 属性(仅针对 GNU)

下一个控制符号可见性的备选方法是使用 visibility 属性。ELF 应用程序二进制接口 (ABI) 定义符号的可见性。一般来说 ,它定义 4 个类,但是大多数情况下,最常用的只有其中两个:

STV_DEFAULT - 用它定义的符号将被导出。换句话说,它声明符号是到处可见的。

STV_HIDDEN - 用它定义的符号将不被导出,并且不能从其他对象进行使用。

注意,这只是 GNU C/C++ 的一个扩展。因此,目前 PowerLinux 客户可以将它用作符号的 GNU 属性。下面是针对本文示例情 况的一个例子:

int myintvar __attribute__ ((visibility ("hidden")));
int __attribute__ ((visibility ("hidden"))) func0 () {
 return ++myintvar;
}
...

要定义 GNU 属性,需要包含 __attribute__ 和用括号括住的内容。您可以将符号的可见性指定为 visibility (“hidden”)。在上面的示例中,我们可以将 myintvar 和 func0 标记为 hidden 可见性。这将不允许它们在库中被 导出,但是可以在源文件之间共享。实际上,隐藏的符号将不会出现在动态符号表中,但是还被留在符号表中用于静态链接。这 是一种良好定义的行为,完全可以达到我们的目的。它显然优于 static 关键字方法。

注意,对于用 visibility 属性指定的变量,将它声明为 static 可能会让编译器感到混淆。因此,编译器会显示一条警告消 息。

ELF ABI 也定义其他可见性模式:

STV_PROTECTED:符号在当前可执行文件或共享对象之外可见,但是不会被覆盖。换句话说,如果共享库中的一个受保护符号 被该共享库中的另一个代码引用,那么此代码将总是引用共享库中的此符号,即便可执行文件定义了相同名称的符号。

STV_INTERNAL:符号在当前可执行文件或共享库之外不可访问。

注意,此方法目前不受 XL C/C++ 编译器支持,即便在 PowerLinux 平台上亦是如此。但是,我们还有别的方法。
3. 使用导出列表

上面两种解决方案可以在源代码级别发挥作用,并且只需要编译器就可以实现目的。然而,这要求用户能够告诉链接器去执行 类似的工作,因为主要是在动态链接中涉及到符号可见性。针对链接器的解决方案是导出列表。

导出列表由编译器(或相关工具,如 CreateExportlist)在创建共享库的时候自动生成。也可以由开发人员手工编写。导出 列表由链接器选项传入并当作链接器的输入。然而,由于编译器驱动程序会完成所有琐碎的工作,所以程序员很少关注那些非常 详细的选项。

导出列表的原理是,显式地告诉链接器可以通过外部文件从对象文件导出的符号是哪些。GNU 用户将此类外部文件称作为 “导出映射”。我们可以为本文的示例编写一个导出映射:

{
global: func1;
local: *;
};

上面的描述告诉链接器,只有 func1 符号将被导出,其他符号(由 * 匹配)是局部的。程序员也可以显式地列出 func0 或 myintvar 为局部符号 (local:func0;myintvar;)。但是很明显,全部匹配 (*) 更为方便。一般来说,高度推荐使用全部匹配 (*) 来将所有符号都标记为局部并只挑出需要导出的符号,因为这样更安全。这样可以避免用户忘记保持一些符号为局部的,也 可以避免两个列表中出现重复,重复可能会导致非预期的行为。

要用这一方法生成 DSO,程序员必须利用 --version-script 链接器选项传递导出映射文件:

$ gcc -shared -o libtest.so a.C -fPIC -Wl,--version-script=exportmap

利用 readelf 二进制实用工具加上 -s 选项读取 ELF 对象文件:readelf -s mylib.so

它将显示只有 func1 对该模块是全局可见的(.dynsym 部分中的信息项),其他符号被隐藏为局部的。

对于 IBM AIX OS 链接器,提供了一个类似的导出列表。确切说,在 AIX 上,导出列表被称作导出文件。

编写导出文件很简单。程序员只需将需要导出的符号放入导出文件中即可。在本示例中,就像如下所示这么简单:

func1__Fi  // symbol name

因此,我们用一个链接器选项指定导出文件时,只有我们想要导出的符号被添加到 XCOFF 的“加载器符号表”中 ,其他符号都保持为非导出的。

对于 AIX 6.1 及以上版本,程序员可能还会附加一个 visibility 属性来描述导出文件中符号的可见性。AIX 链接器现在接 受 4 个这样的 visibility 属性类型:

export:符号用全局导出属性进行导出。

hidden:符号不导出。

protected:符号被导出,但是不能被重新绑定(被抢占),即便使用的是运行时链接。

internal:符号不导出。符号的地址不得提供给其他程序或共享对象,但是链接器不对此进行验证。

export 和 hidden 之间的区别很明显。然而,exported 和 protected 之间的区别则很微妙。下一节我们将更加详细地讨论 符号抢占。

总之,上面 4 个关键字可用于导出文件中。通过将它们附加到符号的末尾(带有一个空格),将会提供不同粒度的符号可见 性控制。在本例中,我们也可以像如下所示一样指定符号可见性(在 AIX 6.1 及更高版本上):

func1__Fi export
func0__Fv hidden
myintvar hidden

这通知链接器只有 func1__Fi(即 func1)将会导出,其他符号不会导出。

您可能注意到了,与 GNU 导出映射不同,导出文件中列出的符号都是重整后的名称。重整后的名称看起来不是那么友好,因 为程序员可能会不了解重整规则。但是这确实有助于链接器快速地进行名称解析。为了弥补这一缺陷,AIX OS 选择利用一个工具 来帮助程序员。

简而言之,如果程序员在调用 XL C/C++ 编译器时指定 -qmkshrobj 选项,那么在编译器成功生成对象文件之后,编译器驱动 程序将调用 CreateExportList 工具来自动生成导出文件,其中包含已重整符号的名称。编译器驱动程序然后将此导出文件传递 给链接器来处理符号可见性设置。考虑下面这个例子,如果我们调用:

$ xlC -qpic a.C -qmkshrobj -o libtest.a

这将生成 libtest.a 库,并且所有符号都被导出(这是默认情况)。尽管这没有达到我们的目的,但是至少整个过程对程序 员看起来是透明的。程序员也可以选择使用 CreateExportList 实用工具来生成导出文件。如果选择这种方式,您现在能够手工 修改导出文件。例如,假设您想要的导出文件名称是 exportfile,那么 qexpfile=exportfile 就是您需要传递给 XL C/C++ 编 译器驱动程序的选项。

$ xlC -qmkshrobj -o libtest.a a.o -qexpfile=exportfile

更多精彩内容:http://www.bianceng.cn/Programming/cplus/

在本例中,您会发现所有符号如下所示:

func0__Fv
func1__Fi
myintvar

根据需要,我们可以简单地删除带有 myintvar、func0 的行,或者在它们后面附加 hidden 可见性关键字,然后保存导出文 件,并使用链接器选项 -bE:exportfile 来传回修改后的导出文件。

$ xlC -qmkshrobj -o libtest.a a.o -bE:exportfile

这将完成所有的步骤。现在,生成的 DSO 将不让 func1__Fi(即 func1)导出:

$ 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

另外,程序员也可以使用 CreateExportList 实用工具来显式地生成导出文件,如下所示:

$ CreateExportList exportfile a.o

在本文示例中,效果跟上面的方法相同。

对于 AIX 6.1 及更高版本上的新格式,逐个地为符号可见性附加关键字可能需要较多的精力。然而,XL C/C++ 编译器计划进 行一些更改,以便让程序员的工作更轻松(本系列下一部分中将介绍相关的信息)。

在导出列表解决方案中,所有的信息都保留在导出列表中,程序员不需要更改源文件。这将代码开发和库开发的工作分离开来 。然而,我们可能会面临此过程的一个问题。因为我们保持源文件不修改,所以编译器生成的二进制代码可能会不是最优的。编 译器失去了优化那些由于缺少信息而不被导出的符号的机会。这会增加所生成二进制文件的大小,或者减慢符号解析的处理速度 。然而,对于大多数应用程序来说,这并不是一个主要问题。

下表比较了以上所有解决方案,并且让视图更为集中。

符号抢占

正如前面所提到的,可见性关键字 export 和 protected 之间存在微妙的区别。此微妙区别就在于符号抢占(symbol preemption)上。符号抢占出现在当链接时解析的符号地址被另一个在运行时解析的符号地址取代时(注意,尽管在 AIX 上运行 时链接是可选的)。从概念上讲,运行时链接会在程序执行开始之后解析共享模块中未定义和非延迟的符号。符号抢占是一种提 供运行时定义(这些函数定义在链接时不可用)和符号重新绑定功能的机制。在 AIX 上,当主程序利用 -brtl 标志进行链接时 或者当预加载的库利用 LDR_CNTRL 环境变量进行指定时,程序能够使用运行时链接设施。利用 -brtl 进行编译会向程序添加一 个对动态链接器的引用,当程序开始运行时,该引用会被程序的启动代码 (/lib/crt0.o) 调用。共享对象输入文件按其在命令行 中指定的相同顺序在程序加载器部分被列出为关联项。当程序开始运行时,系统加载器加载这些共享对象,以便它们的定义对动 态链接器可用。

因此,在运行时重新定义共享对象中的条目是一种叫做符号抢占的功能。 符号抢占只有在 AIX 上使用运行时链接时才能发挥 作用。在链接时绑定到一个模块的导入会在运行时重新绑定到另一个模块。一个局部定义是否可以被导入的实例抢占,取决于模 块的链接方式。然而,非导出符号永远不会在运行时被抢占。运行时加载器加载组件时,该组件中所有具有默认可见性的符号都 会被已经加载的组件中相同名称的符号抢占。注意,因为主程序映像总是最先加载的,所以其定义的任何符号都不会被抢占(重 新定义)。

受保护符号会被导出,但是不可以被抢占。相反,导出的符号可被导出并抢占(如果使用运行时链接的话)。

对于默认符号,Linux 和 AIX 之间存在差别。GNU 编译器和 ELF 文件格式定义一种默认可见性,用于可被导出和抢占的符号 。这类似于 AIX 上定义的 exported 可见性。

下面的代码以 AIX 平台为例:
清单 3. func.C

#include <stdio.h>
void func_DEFAULT(){

printf("func_DEFAULT in the shared library, Not preempted\n");
}

void func_PROC(){

printf("func_PROC in the shared library, Not preempted\n");
}

清单 4. invoke.C

extern void func_DEFAULT();
extern void func_PROC();

void invoke(){

func_DEFAULT();

func_PROC();
}

清单 5. main.C

#include <stdio.h>

extern void func_DEFAULT();
extern void func_PROC();
extern void invoke();

int main(){

invoke();

return 0;
}

void func_DEFAULT(){

printf("func_DEFAULT redefined in main program, Preempted ==> EXP\n");
}

void func_PROC(){

printf("func_PROC redefined in main program, Preempted ==> EXP\n");
}

在上面的描述中,我们在 func.C 和 main.C 中都定义了 func_DEFAULT 和 func_PROC。它们名称相同,但是行为不同。来自 invoke.C 的函数 invoke 将依次调用 func_DEFAULT 和 func_PROC。我们将使用下面的 exportlist 代码来看符号是否被导出, 以及是如何导出的。
清单 6. exportlist

func_DEFAULT__Fv export
func_PROC__Fv protected
invoke__Fv

如果使用的是 AIX 6.1 之前的链接器版本,可以使用空格代替 export,symbolic 关键字代替 protected 关键字。下面代码 中列出了构建 libtest.so 库和 main 可执行文件的命令:

/* generate position-independent code suitable for use in shared libraries. */
$ xlC -c func.C invoke.C -qpic

/* generate shared library, exportlist is used to control symbol visibility */
$ xlC -G -o libtest.so func.o invoke.o -bE:exportlist

$ xlC -c main.C

/* -brtl enable runtime linkage. */
$ xlC main.o -L. -ltest -brtl -bexpall -o main

本质上,我们是从 func.o 和 invoke.o 构建 libtest.so。我们使用 exportlist 来将 func.C 中的 func_DEFAULT 和 func.C 中的 func_PROC 设置为导出符号,但是仍然是受保护的。这样,libtest.so 就有两个导出符号和一个受保护符号。对于 主程序,我们从 main.C 导出所有符号,但是将它链接到 libtest.so。注意,我们使用 -brtl 标志来为 libtest.so 启用动态 链接。

下一步是调用主程序。

$ ./main
func_DEFAULT redefined in main program, Preempted ==> EXP
func_PROC in the shared library, Not preempted

在这里我们看到一些有趣的东西:func_DEFAULT是来自 main.C 的版本,而 func_PROC 是来自 libtest.so (func.C) 的版本 。func_DEFAULT 符号被抢占,因为来自 libtest.so 的局部版本(我们说它是局部的,是因为调用函数 invoke 来自于 invoke.C,后者本质上与来自 func.C 的 func_DEFAULT 位于同一模块)被来自另一个模块的 func_DEFAULT 符号所取代。然而 ,func_PROC 上确实出现了相同的条件,它在导出文件中被指定为 protected 可见性。

注意,可以抢占其他符号的符号应该总是导出符号。假设我们在构建可执行文件 main 时删除了 -bexpall 选项,那么输出如 下所示:

$ xlC main.o -L. -ltest -brtl -o main; //-brtl enable runtime linkage.
$ ./main
func_DEFAULT in the shared library, Not preempted
func_PROC in the shared library, Not preempted

这里没有发生抢占。所有符号都保持模块中的相同版本。

实际上,要在运行时检查符号是否是导出符号或者受保护符号,我们可以使用 dump 实用工具:

$ dump -TRv libtest.so
libtest.so:

               ***Loader Section***

               ***Loader Symbol Table Information***
[Index]      Value      Scn     IMEX Sclass   Type           IMPid Name

[0]     0x00000000    undef      IMP     DS EXTref   libc.a (shr.o) printf
[1]     0x2000040c    .data      EXP     DS SECdef        [noIMid] func_DEFAULT__Fv
[2]     0x20000418    .data      EXP     DS SECdef        [noIMid] func_PROC__Fv
[3]     0x20000424    .data      EXP     DS SECdef        [noIMid] invoke__Fv

               ***Relocation Information***

    Vaddr      Symndx      Type      Relsect    Name

0x2000040c  0x00000000   Pos_Rel      0x0002     .text

0x20000410  0x00000001   Pos_Rel      0x0002     .data

0x20000418  0x00000000   Pos_Rel      0x0002     .text

0x2000041c  0x00000001   Pos_Rel      0x0002     .data

0x20000424  0x00000000   Pos_Rel      0x0002     .text

0x20000428  0x00000001   Pos_Rel      0x0002     .data

0x20000430  0x00000000   Pos_Rel      0x0002     .text

0x20000434  0x00000003   Pos_Rel      0x0002     printf

0x20000438  0x00000004   Pos_Rel      0x0002     func_DEFAULT__Fv

0x2000043c  0x00000006   Pos_Rel      0x0002     invoke__Fv

这是来自 libtest.so 的输出。我们可以发现,func_DEFAULT__Fv 和 func_PROC__Fv 都是导出符号。然而,func_PROC__Fv 不具有任何重新定位。这意味着,加载器可能找不到方法来替换 TOC 表中 func_PROC 的地址。TOC 表中 func_PROC 的地址是函 数调用要将控制转移到的地方。因此,func_PROC 似乎不会被抢占。我们然后认识到,它是受保护的。

实际上,符号抢占使用得很少。然而,它让我们可以在运行时动态地替换符号,但是也会留下一些安全漏洞。如果不想让库中 的关键符号被抢占(但是仍然需要导出),为安全起见,需要将它设置为受保护的。

相关文章
  • 没有相关文章
  • 徐汉涛(www.xuhantao.com) © 2024 版权所有 All Rights Reserved.
  • 部分内容来自网络,如有侵权请联系站长尽快处理 站长QQ:965898558(广告及站内业务受理) 网站备案号:蒙ICP备15000590号-1