当前位置:天才代写 > tutorial > C语言/C++ 教程 > 节制共享库的标记可见性(一) 标记可见性简介

节制共享库的标记可见性(一) 标记可见性简介

2017-11-02 08:00 星期四 所属: C语言/C++ 教程 浏览:608

副标题#e#

什么是标记和标记可见性

标记是谈及工具文件、链接等内容时的根基术语之一。实际上,在 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

#p#分页标题#e#

这里,“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


#p#副标题#e#

此刻,如信息所示,只有 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. 利用导出列表

#p#分页标题#e#

上面两种办理方案可以在源代码级别发挥浸染,而且只需要编译器就可以实现目标。然而,这要求用户可以或许汇报链接器去执行 雷同的事情,因为主要是在动态链接中涉及到标记可见性。针对链接器的办理方案是导出列表。

导出列表由编译器(或相关东西,如 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

#p#副标题#e#

在本例中,您会发明所有标记如下所示:

func0__Fv
func1__Fi
myintvar

#p#分页标题#e#

按照需要,我们可以简朴地删除带有 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");
}

#p#副标题#e#

#p#分页标题#e#

在上面的描写中,我们在 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

#p#分页标题#e#

这是来自 libtest.so 的输出。我们可以发明,func_DEFAULT__Fv 和 func_PROC__Fv 都是导出标记。然而,func_PROC__Fv 不具有任何从头定位。这意味着,加载器大概找不到要领来替换 TOC 表中 func_PROC 的地点。TOC 表中 func_PROC 的地点是函 数挪用要将节制转移到的处所。因此,func_PROC 好像不会被抢占。我们然后认识到,它是受掩护的。

实际上,标记抢占利用得很少。然而,它让我们可以在运行时动态地替换标记,可是也会留下一些安详裂痕。假如不想让库中 的要害标记被抢占(可是仍然需要导出),为安详起见,需要将它配置为受掩护的。

 

    关键字:

天才代写-代写联系方式