编译与链接 - Bsymbolic 的含义
背景
组里的某个项目中用到了动态库,线上出了一次故障,原因我们实现的动态库中函数名与其他动态库中的函数冲突了,运行过程中调用了非预期的函数。
考虑下面的代码:
// print.c
#include <stdio.h>
int a = 1;
void print() {
printf("a = %d\n", a);
}
print.c 被构建为动态链接库 libprint.so ,当其中的 print 被调用时,我们期望它打印 a = 1。但因为 a 是一个全局变量,在动态库中使用此全局符号时,符号会在当前客户程序中查找,随后在客户程序依赖的所有动态库中按顺序查找。假如客户程序中或其他先于 libprint.so 加载的动态库中也定义了全局变量 a,那么 libprint.so 中的变量 a 就会被屏蔽掉。
按直觉 print 应该优先访问定义于同一个 .c 文件中的变量 a,但这里确实不符合直觉。
后来同事在构建动态库的时候加了 -Wl,-Bsymbolic 编译选项解决了问题,但为什么这个编译选项能解决问题呢?本文来一探究竟。
-Wl,-Bsymbolic 的含义
首先看 -Wl 的含义:
-Wl,option Pass option as an option to the linker. If option contains commas, it is split into multiple options at the commas. You can use this syntax to pass an argument to the option. For example,
-Wl,-Map,output.mappasses-Map output.mapto the linker. When using the GNU linker, you can also get the same effect with-Wl,-Map=output.map.
可见 -Wl,-Bsymbolic 是给链接器传递 -Bsymbolic 选项。-Bsymbolic 的含义如下:
-BsymbolicWhen creating a shared library, bind references to global symbols to the definition within the shared library, if any. Normally, it is possible for a program linked against a shared library to override the definition within the shared library. This option is only meaningful on ELF platforms which support shared libraries.
https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_node/ld_3.html
看到这里我大概明白了,使用动态链接库时候,因为符号都是运行时绑定的,使用此选项后,链接器默认会先在本动态库中寻找符号,如果找不到才去其他全局范围查找。
符号搜索路径
通过 -Wl,-Bsymbolic 编译选项让动态库中的函数优先调用本动态库内定义的函数,这不是所有动态库的作者预期的效果吗?加载器为什么不默认先查询本动态库中的函数呢?目前看来,符号的查找按照如下顺序执行:
1、绑定类型为 LOCAL 的符号 2、当前程序的符号表 3、遍历动态库列表,查找动态库的符号表,找绑定类型为 GLOBAL 且对外可见的符号 4、符号找不到
指定了 -Bsymbolic 链接选项后,默认会先在当前动态库中查找,这样就能保证能优先访问到动态库自己定义的变量了。
关于符号查找的细节可以看这篇文档:https://docs.oracle.com/cd/E19957-01/806-0641/6j9vuquj2/index.html#chapter3-13
严格设置动态库符号的可见性
加入 -Wl,-Bsymbolic 选项后,虽然当前动态库会优先使用本地定义的函数,但是这些函数会暴露到全局空间中,有可能被其他动态库错误地引用。因此,更加稳妥的方案是将不希望暴露出去的函数的可见性限定在动态库内部。
在 {% include link.html uid=20231611 %} 中我描述了符号可见性的细节,详细内容可以查看这篇文章。
简单来说,具体的做法是在编译动态库的时候使用 -fvisibility=hidden 选项设置所有符号的可见性为 hidden,对于需要暴露的函数,使用如下方式改变其可见性:
__attribute__((visibility("default")))
int foo(int x) {
return x*x;
}
可见性为 hidden 的函数,不会被其他动态库调用。因此,我认为这是更加稳妥的方案。