链接时符号冲突踩坑

场景

1
2
3
4
5
// CMakeLists.txt
add_library(api SHARED api.cpp)

add_executable(main main.cpp)
target_link_libraries(main PRIVATE api)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// main.cpp
#include <cstdio>

void f() {
puts("f in main!");
}

void g();

int main() {
g();
return 0;
}
// api.cpp
#include <cstdio>

void f() {
puts("f in api!");
}

void g() {
f();
}

执行输出的结果是 f in main!,一定程度上出乎意料且违背预期了。我希望自己编译出的动态链接库给别人用,但是我又不能控制别人的可执行程序(或者其它动态链接库)中有什么符号,因此我希望自己的动态链接库是自包含 (self-contained) 的。

解法

链接选项 -Bsymbolic

When 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.

根据文档,这个链接选项的效果是在创建 shared library 的时候尽量把引用到的符号绑定到 shared library 内部。另外与 -Bsymbolic 类似的还有 -Bsymbolic-functions,后者只对函数符号生效。

修改后的 CMakeLists.txt 如下,运行后的输出为 f in api!

1
2
3
4
5
6
add_library(api SHARED api.cpp)
target_link_options(api PRIVATE LINKER:-Bsymbolic) # <--

add_executable(main main.cpp)
target_link_libraries(main PRIVATE api)

注:CMake 中的指定的链接选项是通过编译器传递的。虽然 ld 只需要 -Bsymbolic 的参数,但是这里还得写完整的 -Wl,-Bsymbolic(对于 gcc),或者用自动根据编译器选择的 LINKER: 前缀。

dlopen + DEEPBIND

修改后的文件如下:

1
2
3
4
5
6
add_library(api SHARED api.cpp)

add_executable(main main.cpp)
target_link_libraries(main PRIVATE dl)
add_dependencies(main api)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// main.cpp
#include <cstdio>
#include <dlfcn.h>

void f() {
puts("f in main!");
}

int main() {
auto handler = dlopen("./libapi.so", RTLD_NOW);
if (!handler) puts(dlerror());
auto ff = dlsym(handler, "g");
if (!ff) puts(dlerror());
reinterpret_cast<void(*)()>(ff)();
return 0;
}
// api.cpp
#include <cstdio>

void f() {
puts("f in api!");
}

extern "C" void g() {
f();
}

发现竟然是好的(输出是 f in api!)。但是如果给 main 加上 -export-dynamic 的选项之后,又出现了一样的问题。

When creating a dynamically linked executable, using the -E option or the --export-dynamic option causes the linker to add all symbols to the dynamic symbol table. The dynamic symbol table is the set of symbols which are visible from dynamic objects at run time.

-export-dynamic 可以使可执行文件内部的符号对于 dlopen 打开的 dynamic objects 可见(也就是打开的 .so 中可以调自己)。如此一来,符号冲突的问题就又回来了。

解决的方法除了上面的加 -Bsymbolic 的链接选项,还可以在 dlopen 的选项中加上 RTLD_DEEPBIND,说明如下。

Place the lookup scope of the symbols in this shared object ahead of the global scope. This means that a self-contained object will use its own symbols in preference to global symbols with the same name contained in objects that have already been loaded.

作用就是改变查找符号的顺序,优先查找 .so 内部的符号。

符号可见性

为了减少符号冲突的可能性,对于 C++ 可以使用 namespace,C 可能就需要有赖于符号可见性了。借用一下上面的例子,使用 nm 命令查看 libapi.so 中的符号(objdumpreadelf 也有这些功能),可以看到有以下几行,大 T 表示全局可见且在 text (code) section 中(nm 的输出中,大写表示 global,小写表示 local)。

1
2
3
4
5
6
7
output of "nm libapi.so"
...
000000000000065a T _Z1fv
...
000000000000066d T g
...

static 关键字

static 同时限定了存储位置和作用域,在编译后不会给 linker 留下任何信息,因此 nm 命令直接就看不到被 static 限定的符号。

GNU C/C++ 中的 attribute

通过在函数的定义前加上 __attribute__ ((visibility ("hidden"))) 来使其的符号对外部不可见,在 nm 的输出结果中,对应的符号类型会变成小 t。

通常的做法是在编译选项中加上 -fvisibility=hidden 来使所有符号都默认不可见,然后通过设置函数的可见性为 default 来导出函数。另外可见性除了常见的 hiddendefault 两个选项外,还有 internalprotected

导出列表

上面的两种方法都涉及到对源代码的修改,利用 GNU C/C++ 中的 --version-script 选项来指定一个文件用于指导符号可见性,好处是避免了对源文件的修改。

参考