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

61

有些平台要求你向链接器提供共享库的外部符号列表。但在大多数类Unix系统中,这并不是必需的:所有非静态符号都将默认可用。

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

5个回答

87

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

2
非导出符号将以小写t列出。 - PypeBros
6
版本脚本不像-fvisibility=hidden那样让编译器优化代码。 - yugr

51
我认为最简单的方法是在gcc选项中添加-fvisibility=hidden,然后在代码中将某些符号的可见性明确设置为公共的(通过__attribute__((visibility("default"))))。请参阅此处的文档。可能也有一种方法可以通过ld链接脚本来完成,但我对此了解不多。

2
例如,在 Firefox 中,这就是我们所做的。 - Ted Mielczarek
2
除非它是未记录的,否则应为:attribute((visibility("default")))。您应该考虑修改您的答案以反映这一点。此外,您的链接已损坏。 - redteam316

8

调用任何已导出函数或使用已导出全局变量的代码效率都比那些未导出的要低。这涉及到额外的间接层。这适用于可能在编译时导出的任何函数。即使由链接器脚本取消导出的函数,gcc 仍将产生额外的间接引用。因此,使用可见性属性将比链接器脚本生成更好的代码。


你确定这在调用函数时也适用吗?根据我的经验,使用隐藏和导出的函数得到的代码是相同的。只需使用R_X86_64_PLT32重定位进行常规调用即可。但是,对于访问导出的变量,在查找之前必须先在GOT中查找它会有一些差别。 - knatten

7

在GNU / Linux上管理导出符号似乎有几种方法。从我的阅读中,这些是三种方法:

  • 源代码注释/修饰:
    • 方法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,请使用注解方法。

我相信还有其他方法。

以下是一些我发现有帮助的参考资料(附有示例):


1
一个重要的问题是,对于C++来说,版本脚本很难正确编写。您需要自己识别所有必要的编译器生成的异常相关符号,并且符号名称匹配发生在混淆名称的级别上,这意味着您将不得不使用一组脆弱的通配符。这被事实加剧了,即文档根本没有关于C++正确使用的提示。在使用版本脚本发布一个库后,我们的结论是“再也不要了”。 - tobi_s
2
让我补充一下:仅包含头文件的 C++ 库可能会对版本脚本方法造成严重破坏:Unix 动态链接器允许后加载的动态库中的符号覆盖先加载的动态库中的符号。现在想象一下,你有两个库使用同一个头文件库的不同版本,并且早期的库意外地暴露了一个或两个符号,而第二个库根本没有隐藏它们。一旦你的代码调用了来自未内联的头文件库的函数,就会出现惊人的回溯,来回在两个 .so 文件之间跳转导致崩溃。 - tobi_s
@tobi_s - 说得好。 (幸运的是,我的项目只暴露了一个C API,因此不会面临这些问题。) - codesniffer
谢谢,我只是想挽救那些读了你优秀文章的人们,避免他们将其应用到C++中带来的失望 :-) - tobi_s

1
如果您正在使用libtool,有另一种选项与Employed Russian的答案非常相似。使用他的示例,可能是这样的:
cat export.sym
bar
baz

然后使用以下选项运行libtool:
libtool -export-symbols export.sym ...

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

1
与EmployedRussian的回复相同 - 这会生成比“-fvisibility = hidden”更次优的代码。 - yugr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接