链接共享库时限制符号的可见性

raf*_*afl 45 linker gnu shared-libraries linker-scripts

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

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

Emp*_*ian 67

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 '
Run Code Online (Sandbox Code Playgroud)

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

0000000000000718 T _fini
00000000000005b8 T _init
00000000000006b7 T bar
00000000000006c9 T baz
00000000000006ac T foo
Run Code Online (Sandbox Code Playgroud)

假设您只想导出bar()baz().创建"版本脚本" libfoo.version:

FOO {
  global: bar; baz; # explicitly list symbols to be exported
  local: *;         # hide everything else
};
Run Code Online (Sandbox Code Playgroud)

将它传递给链接器:

gcc -fPIC -shared -o libfoo.so foo.c -Wl,--version-script=libfoo.version
Run Code Online (Sandbox Code Playgroud)

观察导出的符号:

nm -D libfoo.so | grep ' T '
00000000000005f7 T bar
0000000000000609 T baz
Run Code Online (Sandbox Code Playgroud)

  • 版本脚本不允许编译器优化代码以及-fvisibility = hidden`。 (3认同)
  • 非导出符号将用小写“t”列出。 (2认同)

jpa*_*cek 38

我认为最简单的方法是添加-fvisibility=hidden到gcc选项并明确地在代码中公开一些符号公开(by __attribute__((visibility("default")))).请参阅此处的文档.

可能有一种方法可以通过ld链接器脚本实现这一点,但我对此并不了解.

  • 例如,这就是我们在Firefox中所做的事情。 (2认同)
  • 除非没有记录,否则它应该是:__ attribute __((visibility("default")))你应该考虑修改你的答案来反映这一点.此外,您的链接已损坏. (2认同)

小智 7

生成用于调用任何导出函数或使用任何导出的全局变量的代码效率低于未导出的函数.涉及额外的间接水平.这适用于可能编译时导出的任何函数.gcc仍然会为稍后由链接描述文件导出的函数产生额外的间接.因此,使用visibility属性将生成比链接器脚本更好的代码.


cod*_*fer 7

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

  • 源码注释/修饰:
    • 方法一:-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++ 库可能会对版本脚本方法造成严重破坏:unix 动态链接器允许稍后加载的动态库中的符号覆盖早期加载的动态库中的符号。现在想象一下,您有两个库使用同一仅标头库的不同版本,并且前一个库意外地暴露了一两个符号,而第二个库根本没有隐藏它们。一旦您的代码遇到来自未内联的纯标头库中的函数,您就会因在两个 .so 文件之间来回的惊人回溯而崩溃。 (2认同)