so 符号可见性

    技术2024-07-19  74

    IBM XL C / C ++ V13编译器:可见性属性的支持

    在上一篇文章中 ,我们了解了处理符号可见性的不同方法。 最重要的包括:GNU可见性属性扩展,IBMAIX®导出文件和GNU版本脚本。 GNU可见性可以使程序员从源代码​​级别控制符号的可见性,而用于链接的脚本对于从构建和发布步骤的角度来控制符号可见性可能更为有用。

    但是,随着用于AI​​X和Linux®V13编译器的XL C / C ++的发布,XL C / C ++编译器接受了GNU可见性扩展。 此外,诸如new pragma指令和new global编译器选项之类的实体也可以用于此目的。 在这一部分中,我们将重点更多放在可见性属性设置及其最终解决方案上。 此外,我们可能会详细介绍AIX和Linux可见性设计之间的差异以及所实现的AIX可见性的兼容性。

    可见性属性说明符

    与GNU可见性属性扩展类似,对于XL C / C ++编译器,程序员可以通过属性说明符使用GNU属性语法来设置符号可见性。 清单1显示了函数和变量声明的示例。

    清单1.变量和函数的可见性说明符
    int __attribute__((visibility("hidden"))) m; // m is set as "hidden" void __attribute__((visibility("protected"))) fun(); // fun() is set as "protected"

    可见性属性的类型可以是default , hidden , protected或internal 。 这与我们在上一篇文章中介绍的GNU语法一致。 但是,在缺省值中,AIX和Linux实现之间存在细微的差异。 本文稍后将对此进行讨论。

    乍一看,可见性属性看起来很简单。 但是,在编程实践中,程序员可能会有更多问题。 例如,一个普遍提出的问题是关于具有不同可见性的同一符号的多重定义。 这对于处理不同的标头定义可能并不罕见,并且可能给程序员带来很多麻烦。 从编译器的角度来看,此问题是由不完善的现实引起的,解决方案将更多地依赖于程序员方面。 但是,编译器确实提供了规则和便利来提供帮助。 XL C / C ++编译器将采用首次遇到的可见性属性。 并且,在处理其余部分(具有不同的可见性)时,会向程序员发出警告。 最后,编译器将忽略后者定义的可见性。 清单2提供了一个示例。

    清单2.为同一符号指定的不同可见性属性
    extern int __attribute__((visibility("hidden"))) m; // "m" is "hidden". int __attribute__((visibility("protected"))) m; // Compiler warning - "protected" is ignored.

    在此示例中, m首先定义为hidden 。 这意味着在动态链接期间,变量的符号将不可见。 但是,后面的代码将再次对其进行设置,但是在protected , 全局可见 。 编译器将发出警告,然后在处理代码时忽略protected属性。

    有关可见性的其他一些问题将涉及类型 。 在C ++编码中,程序员可能能够自己创建类型 。 程序员甚至可能想知道是否可以为此类类型指定可见性属性。 这是一个有趣的问题,因为在C编程中此类问题永远不会发生。 所有内置类型都不会干扰可见性属性。 但是,在C ++编程中,这不再是正确的。 在下面的清单中对此进行了演示。

    清单3.为类指定的可见性属性
    class __attribute__((visibility("hidden"))) T // class T has "hidden" visibility { public: static int gVal; // T::gVal is "hidden" void Dosth(); // T::Dosth() is "hidden" }; T t1; // t1 is "hidden". T __attribute__((visibility("internal"))) t2; // t2 has "internal" visibility.

    在清单3中,我们定义了具有hidden可见性的类T 在C ++代码中,程序员可以为类以及函数和对象定义可见性属性。 语法相同,但效果会有所不同。 在这里,我们可以假设类型T是hidden 。 后来,代码定义了两个T类型的对象: t1和t2 。 您可能会发现定义的t1没有任何可见性属性说明符。 因此, t1的可见性设置是从其类型直观地获得的。 并且,对于t2 ,代码将其定义为internal可见性。 这样的定义将在其类型T的可见性属性之前。 因此, t2的最终可见性是internal 。 这是基本规则。 但是,与对象相关联的实体(如成员函数和静态成员)的可见性由于不同的对象可见性定义而不会更改。 这意味着,在清单3中, T::gVal和T::Dosth()将具有T类定义的hidden可见性。 此外,这还意味着相同类型的对象( t1和t2 )仍然共享相同的功能表,无论为它们指定了什么可见性属性。 并且,如果程序员打算改变此类成员的可见性,例如以成员函数为例,则程序员需要直接为成员函数指定可见性属性。 清单4显示了如何将T::Dosth()的可见性属性更改为protected 。

    清单4.更改类成员的可见性属性
    class __attribute__((visibility("hidden"))) T // class T has "hidden" visibility { public: static int gVal; // T::gVal is "hidden" void __attribute__((visibility("protected"))) Dosth(); // T::Dosth() is now "protected" }; T t1; // t1 is "hidden". T __attribute__((visibility("internal"))) t2; // t2 has "internal" visibility.

    可见性属性也可以应用于联合,枚举,名称空间和模板。 名称空间上的可见性将类似于作用域或上下文中的类。 为命名空间指定了可见性属性时,该属性仅应用于作用域中包含的任何符号。 但是,与此同时,功能仅限于封闭的范围。 清单5显示了一个示例。

    清单5.命名空间的Visibility属性
    namespace std __attribute__((visibility("hidden"))) { void foo1() {}; // foo1() has "hidden" visibility. } namespace std { void foo2() {} // The visibility of foo2() is "default"/"unspecified". }

    在此示例中, foo1()被hidden 。 但是, foo2()不会是hidden符号。 这意味着,如果您在文件中定义了名称空间std的可见性,那么编写具有相同名称空间std另一个文件的程序员将不会受到影响。 结果,这样的设计可以潜在地避免跨代码块(具有相同名称空间)的可见性设置的污染。

    模板定义也同样有趣。 程序员可能总是对规则感到好奇,并且想知道当模板和type参数都具有可见性属性设置时如何设置可见性属性。 简而言之,如果主模板定义具有可见性,则该可见性将隐式传播到模板实例化。 否则,默认情况下,模板实例化的可见性与其模板参数相同。 有关示例,请参见清单6。

    清单6.命名空间的Visibility属性
    class __attribute__((visibility("internal"))) A {}; template <typename T> void foo1() {}; template <typename T> void __attribute__((visibility("hidden"))) foo2() {}; foo1<A>(); // The visibility of "foo1<A>()" is "internal" which is propagated from its argument "A". foo2<int>(); // The visibility of "foo2<int>()" is "hidden" which is propagated from template. foo2<A>(); // The visibility of "foo2<A>()" is "hidden".

    此代码将类型A定义为internal ,一个通用模板函数foo1和具有hidden可见性的模板函数foo2 。 对于第一个实例foo1<A>,由于A具有可见性属性,因此该函数将从该模板参数传播可见性属性。 对于foo2<int> ,它从模板获取可见性属性。 而且, foo2<A> 。 实例化忽略类类型A的可见性属性。 因此,模板的可见性属性具有优先权。

    语用

    除了可见性属性外,还有另一种方法可以从源代码级别设置符号的可见性属性。 XL C / C ++编译器通过引入可见性杂注来提供此类功能。 实用程序比可见性属性具有一些优势。 它允许程序员为多个声明设置可见性,而无需逐个符号定义可见性。 可见性编译指示语法如清单7所示。

    清单7.可见性用法的语法
    #pragma GCC visibility push(hidden) #pragma GCC visibility pop

    可见性编译指示仅包括两个堆栈操作。 堆栈顶部将显示当前的可见性属性。 当前可见性可以应用于在代码点之后定义的所有变量。 例如,在Linux上,当前上下文的默认可见性是default 。 如果程序员使用pragma 推送 hidden可见性,则在push pragma之后定义的所有声明的可见性都是hidden 。 除非遇到其他可见性实用程序,否则它将不会更改。 清单8显示了一个示例。

    清单8.命名空间的Visibility属性
    #pragma GCC visibility push(hidden) int m; // "hidden" visibility #pragma GCC visibility push(protected) void foo() // "protected" visibility #pragma GCC visibility pop class A{}; // "hidden" visibility ("protected" visibility is popped) #pragma GCC visibility pop int n; // "default" visibility #pragma GCC visibility pop // An error is emitted as there is no visibility pragma to pop.

    请注意,如果可见性堆栈已为空,则弹出编译指示可能会在编译过程中引起错误,具体情况如下所示。

    编译器选项

    有时,程序员可能还想从编译器选项中设置可见性属性。 使用这些选项,它们可以更改嵌入在编译文件中的符号的可见性。 对于XL C / C ++编译器,您可以指定以下选项。

    xlc -qvisibility=default | protected | hidden | internal | unspecified

    注意, unspecified选项仅适用于AIX平台。 在Linux上, -qvisibility等效于–qvisibility=default 。 在AIX上, -qvisibility等效于–qvisibility=unspecified 。

    可见性确定规则

    由于可以通过可见性属性说明符,可见性编译指示或可见性选项设置声明可见性,因此了解有关编译器如何解决它们之间的冲突的规则非常重要。

    实际上,编译器最终将通过检查以下顺序来解决声明的可见性:

    可见性属性说明符(如果有) 如果是模板实例,则其主要模板定义的可见性 包含它的上下文(结构,类,联合,枚举,名称空间或编译指示)的可见性 否则,其类型的可见性

    规则很简单。 首先,可见性属性说明符如果是声明的一部分,则始终优先。 如果不正确,并且代码是模板实例化,则考虑主模板定义的可见性。 清单6向我们展示了这样的一个实例。

    否则,声明的可见性由其封闭上下文确定。 声明的封闭上下文可以是,例如,全局变量的名称空间范围或其成员的类范围,依此类推。 有关示例,请参见清单9。

    清单9.封闭上下文的Visibility属性
    #pragma GCC visibility push(hidden) class A // "hidden" from pragma { static int m; // "hidden" from class definition void __attribute__((visibility("protected"))) foo() // foo is "protected" };

    在清单9中, class A被hidden因为该杂注定义了具有可见性hidden的封闭上下文。 它的成员也将hidden作为其可见性,因为其封闭上下文是A的定义。但是,由于存在属性说明符,因此foo设置为protected可见性。

    作为最后一条规则,声明的可见性由其类型决定。 清单4已经显示了有关类类型的示例。

    而对于没有任何可见性说明符的函数声明,其可见性由以下四个元素确定。

    功能参数 函数返回类型 函数模板参数(类型或非类型) 从编译器传递的可见性选项

    通过比较这四个元素,不难发现可见性最小的元素。 在这里,我们认为default是最可见的, protected是次要的, hidden位置是第三,而internal是最不可见的。 该功能将从提供的四个元素中获得的可见性最小。 清单10显示了一个示例。

    清单10.函数的可见性确定
    class __attribute__((visibility("internal"))) A {}; A *foo (int a) {}; // Even with option -qvisibility=hidden, foo is "internal".

    在此示例中,用户可能会观察到foo函数正在将指针类型返回给class A , class A是使用internal可见性定义的。 在全局上下文中,有一个选项定义可见性为hidden 。 foo的最终解析结果是internal因为它的可见性最小。

    如果上面列出的规则不能应用于变量声明或函数,则会为它们设置默认的可见性。 默认可见性在以下部分中描述。

    AIX上的可见性属性和向后兼容性

    从上一篇文章中我们知道,在AIX上,默认情况下不可见符号,除非我们在链接阶段手动或借助CreateExportList导出它们。 但是,在Linux上,符号默认情况下具有导出(即default )可见性。 这在AIX可见性属性和GNU可见性属性之间造成了差距。 为了向后兼容,在AIX上,XL C / C ++不会像Linux一样设置所有要导出的符号。 它可能认为没有任何可见性设置的符号是非特定的可见性,与旧的AIX实现保持一致。 对于此类符号,AIX编译器,链接器和相关工具将像以前一样处理它。 但是,不明确的可见性并不意味着该符号是内部的或根本不可见的。 这只是为了保持兼容性而专门设计的可见性。

    因此,正如我们在本文开头提到的那样,如果程序员未明确指定符号的可见性属性,则在Linux上,符号的可见性可能是默认值 。 但是在AIX上,可见性是不确定的 。

    链接时的可见性冲突

    在AIX上链接时的可见性冲突

    从上一篇文章中,我们知道,在AIX系统上,可以通过使用导出文件来确定是否导出了库/可执行文件中的符号。 但是,由于IBM XL C / C ++ V13编译器引入了符号可见性属性,因此必须知道该功能如何与导出文件交互。

    考虑a.cpp文件中的一个典型定义,如清单11所示。

    清单11.文件a.cpp
    int sym __attribute__((visibility("default")));

    变量sym是使用源代码中的default (导出)属性定义的。 我们可以编译a.cpp文件以生成一个动态共享库liba.so。 我们希望sym是库中的导出符号。 但是,如果在链接步骤中给出了exportlist(导出文件)(如清单12所示),我们将-bE:exportlist选项传递给链接器以生成liba.so库(如清单13所示)。 ,通常会提出一个问题: sym最终是共享库中的全局可见(导出)符号。

    清单12. exportlist文件
    sym hidden
    清单13.编译命令
    xlC -G -o liba.so a.c -qpic -bE:exportlist

    这样的问题给链接器/编译器设计带来了挑战。 但是作为设计师,我们注意到:

    可见性属性将由AIX的XL C / C ++ V13引入。 它的目标是更多新代码和移植代码(即接受GNU可见性属性的代码)。 在链接阶段,exportlist目标中的可见性,通常在编译阶段之后调用。

    因此,链接器最好使导出文件中的可见性关键字优先。 实际上,链接器可能必须处理来自导出文件(定义符号的可见性关键字)和诸如对象文件(提供符号的可见性属性)之类的模块的不同可见性关键字/属性组合。 这种处理也称为可视性冲突的解决 。 表1描述了链接器的工作方式。

    表1.通过AIX链接程序解决的可见性冲突
    导出文件中的关键字 目标文件中的符号可见性属性 进口的 已出口 受保护的 隐 内部 没有 出口 经验值 经验值 经验值 经验值 错误 经验值 受保护的 保护 保护 保护 保护 错误 保护 为 hidden 错误 没有 没有 没有 错误 没有 我 nternal 错误 没有 没有 没有 没有 没有 N 个(无关键字) 没有 经验值 保护 没有 没有 没有 EXP :符号已导出 PROT :导出具有受保护可见性的符号 否 :不导出符号 错误 :未导出符号。 打印错误消息,链接失败

    在表1中,行标题显示了源模块中定义的符号可见性。 列标题是在导出文件中定义的符号关键字。 注意,在某些版本的AIX中,关键字可以导出而不是export 。 表1显示了链接器可见的符号的最终分辨率结果,包括:

    符号在目标模块中是否可见(此类模块可以是最终构建的库) 导出的符号是否可以被抢占

    如表1所示,如果导出文件声称该符号已导出或受保护 ,则该符号最终将被链接器解析为全局可见(不能抢占后者)。 同样,如果导出文件将符号描述为hidden或internal ,则该符号最终将不可见。 此外,如果未指定导出文件-bexpall/-bxpfull指定链接选项-bexpall/-bxpfull ,则对象文件中的符号可见性优先。 例如,对于某些非理性组合,该符号在目标文件中具有内部属性,而导出文件希望将其导出 ,则链接器将引发错误。 原因是导出文件可能会改变程序员的意图。

    结果,对于前面提到的a.cpp文件, sym变量的符号最终将在全局范围内不可见。

    有时,程序员可能仍然选择使用链接器选项-bexpall/-bexpfull来导出所有符号。 但是,作为这种可见性分辨率的新设计,在这种情况下,除非未为符号定义可见性(非特定性),否则目标文件中的符号可见性将优先。

    现在,我们了解到导出文件通常可以在最后的链接步骤中覆盖可见性。 但是,我们不建议在新代码中同时使用导出文件和可见性属性。 混合使用可能会给代码开发带来混乱。 但是,有时此功能对于确保共享库的发行版本之间的二进制兼容性仍然非常有用。

    在Linux上链接时的可见性冲突

    链接时的可见性冲突也发生在Linux平台上。 但是,由于平台的不同,与AIX链接器相比,GNU链接器的行为略有不同。

    清单14显示了Linux平台上的一个示例,假设我们在cc中有一个变量:

    清单14.文件cc
    int bar __attribute__((visibility("protected")));

    并且我们在exmap导出地图文件中将 bar声明为全局可见。 GNU链接器还将其命名为版本脚本 。 请注意,程序员可以在导出映射中将符号声明为全局可见符号或局部符号。

    清单15. exmap文件
    {   global:   bar;   local:   *; };

    使用清单16中所示的命令来构建动态共享库(DSO)libc.so。

    清单16.编译命令
    xlC -qmkshrobj c.c -o libc.so -Wl,--version-script=exmap

    最后,是时候检查libc.so的动态符号表(DYNSYM)部分(通常称为.dynsym)了。

    清单17.符号栏的转储
    5 0: 0001100c  4 OBJECT  GLOBAL PROTECTED  21 bar

    同样,在Linux平台上,如果导出了符号,则将其拾取到DSO的DYNSYM部分。 表2显示了导出地图和可见性属性如何确定是否应导出符号(选择为DYNSYM)。

    表2. Linux链接程序解决的可见性冲突
    导出文件中的关键字 目标文件中的符号可见性属性 默认 受保护的 隐 内部 全球 是 是 没有 没有 本地 没有 没有 没有 没有

    与表1相似,表2的第一行显示了源模块中的符号可见性。 一般来说,这与AIX相似,因为关键字优先。 但是Linux版本脚本无法导出隐藏的符号。 这是由于这两个平台的性质不同而引起的。

    AIX和Linux之间的另一个区别是DYNSYM的符号和SYMTAB的符号都维护可见性属性信息。 但是,AIX在符号表和装入程序符号表中的符号之间没有保留相同的属性。 可见性属性信息仅在符号表中有效。 加载程序符号表仅显示最终的可见性分辨率结果。 如果此处列出了符号,则将其导出。 否则,该符号不可见。 这也是AIX和Linux平台之间的区别。

    CreateExportList工具

    在上一篇文章中,我们介绍了CreateExportList工具,该工具有助于自动生成导出文件。 程序员可能仍会关心它如何与可见性属性进行交互。 CreateExportList工具的一般规则是将所有全局可见符号与匹配的可见性属性作为关键字一起添加到导出文件。 表3显示了是否可以将符号var提取到导出文件中,以及如何为该符号生成关键字。

    表3. CreateExportList的新工作规则
    .o中的可见性属性 导出文件中的表示形式 评论 (未指定) 变种 为了保持旧代码的兼容性,此规则保持不变 出口的 var已导出 关键字已导出 受保护的 var保护 关键字受保护 隐 -- 此可见性属性的符号未提取到exportlist中 内部 -- 此可见性属性的符号未提取到exportlist中

    参考表1,读者通常会发现它工作良好,因为链接器可以正确处理冲突。 并且,当我们从混合对象(例如,具有符号可见性的旧对象和新对象)构建模块(库或可执行文件)时,此工具非常有用。 此外,它可以确保在编译器版本更新时,旧的构建脚本不会中断。

    汇编语法

    编写汇编代码的程序员也许也可以分配具有不同可见性的符号。 清单14提供了.globl , .comm , .extern和.weak伪操作的示例语法。

    清单14.在程序集中指定可见性属性的语法
    .extern Name [ , Visibility ] .globl Name [ , Visibility ] .weak Name [ , Visibility ] .comm Name, Expression [, Number [, Visibility ] ] For .comm , if "Visibility" is specified, "Number" (alignment) can be omitted: .comm Name, Expression , , Visibility

    此处列出的Visibility关键字的定义与清单14相似,并且可以具有四个值: internal , hidden , protected和exported 。 如果未定义可见性,则符号条目可能没有任何可见性属性(未指定)。 这类似于C / C ++源代码中的规则。

    但是,除非汇编程序的版本是最新的,否则汇编程序将不接受此类语法。 AIX链接器需要类似的预需求。 这意味着AIX 6.1 TL8 / AIX 7.1 TL2或更高版本将满足要求。 并且用于AIX,V13.1或更高版本的XL C / C ++编译器足以支持C / C ++代码中的可见性功能。

    翻译自: https://www.ibm.com/developerworks/aix/library/au-aix-symbol-visibility-part2/index.html

    相关资源:jdk-8u281-windows-x64.exe
    Processed: 0.019, SQL: 9