应编译时加-fvisibility=hidden默认隐藏所有符号,再用API_EXPORT宏精准标记需导出的接口;类必须整体标注,否则vtable等不会导出,且须用nm -D验证是否真正隐藏。

编译时默认隐藏所有符号
GCC 默认会导出所有非 static 的全局函数和变量,这是动态库符号冲突的主因。不加干预时,libA.so 和 libB.so 里同名的 subfunc 都是 GLOBAL 符号,运行时后加载的库会被前一个“覆盖”——哪怕实现完全不同。
正确做法是编译共享库时统一加 -fvisibility=hidden:
g++ -fPIC -fvisibility=hidden -shared libA.cpp -o libA.so
这会让所有符号默认变成 LOCAL,彻底切断跨库污染路径。注意:这个选项必须加在动态库编译命令中,主程序或静态库加了无效。
- 它不影响类内联函数、模板实例化,但这些本就不生成独立符号,无需额外处理
-
static函数仍保持局部性,但-fvisibility=hidden覆盖范围更广(包括类成员、虚表等) - 一旦启用,所有对外接口必须显式标记,漏标 = 不可用
只对真正需要导出的声明加 default 可见性
不能全靠头文件里一个个加 __attribute__((visibility("default"))),容易遗漏。推荐用宏统一管理:
#define API_EXPORT __attribute__((visibility("default")))然后只在头文件中明确要暴露的接口前使用:
API_EXPORT void start_service();
API_EXPORT class ConfigLoader { /* ... */ };关键点:
- 类必须整体标注
API_EXPORT,否则 vtable、构造函数、inline 成员都不会导出 - 如果用了
extern "C"块,visibility属性得写在每个函数声明上,不能只写在块外 - 头文件里别混用
static和API_EXPORT——static会让符号完全不出现在目标文件中,跟 visibility 机制冲突
检查导出符号是否真被隐藏了
光加参数不验证等于没做。编译完立刻用 nm -D 或 objdump -T 看动态符号表:
nm -D libA.so | grep subfunc
如果输出为空,说明成功隐藏;如果还看到 T 或 D 类型的 subfunc,说明要么没加 -fvisibility=hidden,要么漏标了 static 导致它意外成了全局符号。
-
nm -gC看的是全局符号(含静态库),nm -D才对应运行时动态链接可见的符号 - 用
readelf -s libA.so | grep DEFAULT可快速过滤出被标记为DEFAULT的符号 - 若项目含 C 和 C++ 混合模块,确保 C 文件也加了
-fvisibility=hidden,C 没命名空间,更依赖编译器控制
别指望 namespace 单独解决问题
把 subfunc 包进 namespace utils 后,符号名变成 _Z5utils7subfuncv,看起来不同了——但这只是名称修饰(mangling)结果。如果两个库都用了相同 namespace,修饰后符号名依然一样,照样冲突。
namespace 的真实价值是配合 -fvisibility=hidden 封装内部实现:
namespace detail {
void subfunc() { /* 实现细节,不导出 */ }
}
API_EXPORT void public_api() {
detail::subfunc(); // 安全调用,外部看不到 detail
}这时 detail::subfunc 因为被隐藏且未显式导出,根本不会进入动态符号表。
- 嵌套 namespace(如
mylib::v2::detail)不增加安全性,链接器只认最终 mangling 名 - 第三方库若没做 visibility 控制,它的 namespace 无法帮你隔离;你只能用版本脚本或
--wrap补救 - 模板特化如果要导出,必须显式实例化并加
API_EXPORT,否则链接时找不到定义
实际中最容易被忽略的是:类的导出必须整体标注,且头文件中所有需跨库使用的类型(包括 typedef、enum class)都得确认 visibility 状态。一个没标 API_EXPORT 的基类,会导致整个继承体系在 dlopen 场景下无法正确构造对象。


















