链接时符号冲突踩坑
场景
1 | // CMakeLists.txt |
1 | // main.cpp |
执行输出的结果是 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 | add_library(api SHARED api.cpp) |
注:CMake 中的指定的链接选项是通过编译器传递的。虽然 ld
只需要 -Bsymbolic
的参数,但是这里还得写完整的 -Wl,-Bsymbolic
(对于 gcc),或者用自动根据编译器选择的 LINKER:
前缀。
dlopen + DEEPBIND
修改后的文件如下:
1 | add_library(api SHARED api.cpp) |
1 | // main.cpp |
发现竟然是好的(输出是 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
中的符号(objdump
和 readelf
也有这些功能),可以看到有以下几行,大 T 表示全局可见且在 text (code) section 中(nm
的输出中,大写表示 global,小写表示 local)。
1 | output of "nm libapi.so" |
static 关键字
static
同时限定了存储位置和作用域,在编译后不会给 linker 留下任何信息,因此 nm
命令直接就看不到被 static
限定的符号。
GNU C/C++ 中的 attribute
通过在函数的定义前加上 __attribute__ ((visibility ("hidden")))
来使其的符号对外部不可见,在 nm
的输出结果中,对应的符号类型会变成小 t。
通常的做法是在编译选项中加上 -fvisibility=hidden
来使所有符号都默认不可见,然后通过设置函数的可见性为 default
来导出函数。另外可见性除了常见的 hidden
和 default
两个选项外,还有 internal
和 protected
。
导出列表
上面的两种方法都涉及到对源代码的修改,利用 GNU C/C++ 中的 --version-script
选项来指定一个文件用于指导符号可见性,好处是避免了对源文件的修改。
参考
- https://holtstrom.com/michael/blog/post/437/Shared-Library-Symbol-Conflicts-(on-Linux).html
- https://www3.risc.jku.at/education/courses/ws2003/intropar/origin-new/MproCplrDbx_TG/sgi_html/ch03.html
- https://man7.org/linux/man-pages/man1/ld.1.html
- https://man7.org/linux/man-pages/man3/dlopen.3.html
- https://developer.ibm.com/technologies/systems/articles/au-aix-symbol-visibility/
- https://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Function-Attributes.html
- https://gcc.gnu.org/wiki/Visibility
- https://stackoverflow.com/questions/45135/why-does-the-order-in-which-libraries-are-linked-sometimes-cause-errors-in-gcc
- https://docs.oracle.com/cd/E19683-01/816-1386/chapter3-13/index.html