【问题标题】:Limiting visibility of symbols when linking shared libraries链接共享库时限制符号的可见性
【发布时间】:2022-01-22 15:42:09
【问题描述】:

某些平台要求您向链接器提供共享库的外部符号列表。但是,在大多数 unixish 系统上,这不是必需的:默认情况下,所有非静态符号都可用。

我的理解是 GNU 工具链可以选择性地将可见性限制为显式声明的符号。使用 GNU ld 如何实现?

【问题讨论】:

    标签: linker shared-libraries gnu linker-scripts


    【解决方案1】:

    GNU ld 可以在 ELF 平台上做到这一点。

    以下是如何使用链接器版本脚本:

    /* foo.c */
    int foo() { return 42; }
    int bar() { return foo() + 1; }
    int baz() { return bar() - 1; }
    
    gcc -fPIC -shared -o libfoo.so foo.c && nm -D libfoo.so | grep ' T '
    

    默认情况下,所有符号都被导出:

    0000000000000718 T _fini
    00000000000005b8 T _init
    00000000000006b7 T bar
    00000000000006c9 T baz
    00000000000006ac T foo
    

    假设您只想导出bar()baz()。创建“版本脚本”libfoo.version

    FOO {
      global: bar; baz; # explicitly list symbols to be exported
      local: *;         # hide everything else
    };
    

    将其传递给链接器:

    gcc -fPIC -shared -o libfoo.so foo.c -Wl,--version-script=libfoo.version
    

    观察导出的符号:

    nm -D libfoo.so | grep ' T '
    00000000000005f7 T bar
    0000000000000609 T baz
    

    【讨论】:

    • 非导出符号将改为以小写 t 列出。
    • 版本脚本不允许编译器像-fvisibility=hidden一样优化代码。
    【解决方案2】:

    我认为最简单的方法是将-fvisibility=hidden 添加到 gcc 选项中,并在代码中明确公开某些符号的可见性(__attribute__((visibility("default"))))。请参阅文档here

    可能有一种方法可以通过 ld 链接器脚本来实现,但我对此了解不多。

    【讨论】:

    • 这就是我们在 Firefox 中所做的,例如。
    • 除非没有记录,否则应该是:__attribute__((visibility("default"))) 您应该考虑修改您的答案以反映这一点。另外,您的链接已损坏。
    【解决方案3】:

    为调用任何导出的函数或使用任何导出的全局变量而生成的代码效率低于未导出的代码。涉及到一个额外的间接级别。这适用于可能编译时导出的任何函数。 gcc 仍会为稍后由链接描述文件导出的函数产生额外的间接。因此使用可见性属性将产生比链接描述文件更好的代码。

    【讨论】:

      【解决方案4】:

      似乎有几种方法可以在 GNU/Linux 上管理导出的符号。根据我的阅读,这些是 3 种方法:

      • 源代码注释/装饰:
        • 方法 1:-fvisibility=hidden__attribute__((visibility("default")))
        • 方法 2(从 GCC 4 开始):#pragma GCC visibility
      • 版本脚本:
        • 方法 3:将版本脚本(又名“符号映射”)传递给链接器(例如,-Wl,--version-script=<version script file>

      我不会在这里讨论示例,因为它们大部分都被其他答案所涵盖,但这里有一些关于不同方法的注意事项、优缺点:

      • 使用带注释的方法允许编译器稍微优化代码(少一个间接)。
      • 如果使用带注释的方法,请考虑同时使用strip --strip-all --discard-all
      • 注释方法可以为内部功能级单元测试添加更多工作,因为单元测试可能无法访问符号。这可能需要构建单独的文件:一个用于内部开发和测试,另一个用于生产。 (从单元测试纯粹主义者的角度来看,这种方法通常不是最佳的。)
      • 使用版本脚本会失去优化,但允许符号版本控制,这似乎不适用于带注释的方法。
      • 使用版本脚本可以进行单元测试,前提是代码首先构建到存档 (.a) 文件中,然后链接到 DSO (.so) 中。单元测试将与 .a 链接。
      • Mac 不支持版本脚本(至少在使用 Mac 提供的链接器时不支持,即使编译器使用 GCC 也是如此),因此如果需要 Mac,请使用带注释的方法。

      我确定还有其他人。

      以下是一些我认为有帮助的参考资料(带有示例):

      【讨论】:

      • 重要的一点是,很难为 C++ 找到正确的版本脚本。您需要自己识别所有必要的编译器生成的与异常相关的符号,并且符号名称匹配发生在损坏名称的级别,这意味着您将不得不使用一组脆弱的通配符。文档根本没有给出关于正确使用 C++ 的任何提示,这一事实加剧了这种情况。在发布了一个带有版本脚本的库之后,我们的结论是“再也不会”。
      • 让我补充一点:只有标头的 C++ 库会对版本脚本方法造成彻底破坏:unix 动态链接器允许稍后加载的动态库中的符号覆盖之前加载的动态库中的符号。现在想象一下,您有两个库使用同一个仅标头库的不同版本,而前一个库意外暴露了一两个符号,而第二个库根本没有隐藏它们。一旦您的代码遇到未内联的仅标头库中的函数,您就会崩溃,并在两个 .so 文件之间来回回溯惊人的回溯。
      • @tobi_s - 好点。 (幸运的是,我的项目只公开了一个 C API,所以它不会遇到这些问题。)
      • 谢谢,我只是想让那些阅读您的精彩文章的人免于因将其应用于 C++ 而感到失望 :-)
      【解决方案5】:

      如果您使用的是 libtool,还有另一个选项,类似于 Employed Russian 的回答。

      使用他的例子,它会是这样的:

      cat export.sym
      bar
      baz
      

      然后使用以下选项运行 libtool:

      libtool -export-symbols export.sym ...
      

      请注意,当使用 -export-symbols 时,默认情况下不会导出所有符号,并且仅导出 export.sym 中的符号(因此 libfoo.version 中的“local: *”行实际上隐含在此方法中)。

      【讨论】:

      • 与 EmployedRussian 的回复中的评论相同 - 与 -fvisibility=hidden 相比,这会生成次优代码。
      猜你喜欢
      • 2013-05-22
      • 2022-01-08
      • 2021-07-27
      • 2014-12-07
      • 1970-01-01
      • 2019-08-02
      • 1970-01-01
      • 1970-01-01
      • 2013-03-10
      相关资源
      最近更新 更多