version-script 是 GNU ld 链接器提供的符号导出控制机制,用于在链接阶段精确指定哪些符号(如 MyInit)进入动态符号表(.dynsym),其余全部隐藏;它比 -fvisibility=hidden 更底层、不可绕过,能彻底防止同名符号冲突和 ABI 混乱。

什么是 version-script,它能解决什么问题
version-script 是 GNU ld 链接器提供的机制,用于在链接阶段精确控制哪些符号被导出(对外可见),其余全部隐藏。它比 -fvisibility=hidden 更底层、更不可绕过——哪怕你忘了加 __attribute__((visibility("default"))),只要没写进 version-script 的 global: 列表,符号就进不了动态符号表(.dynsym)。这直接堵死了“多个 .so 各自带一份同名函数,运行时加载错版本”的漏洞。
怎么写 export.lst 并传给链接器
先建一个文本文件,比如 export.lst,内容按语法写清楚要导出和隐藏的符号:
{global:MyInit;MyProcess;MyShutdown;local:*;};
注意三点:
-
global:后面是显式列出的函数名,必须和编译后符号完全一致(C++ 要用c++filt确认 mangled 名,或直接用extern "C"导出) -
local:*表示其余所有符号一律不导出,包括静态函数、内部变量、模板实例等 - 末尾的分号
;和花括号{}缺一不可,否则链接失败报parse error in version script
编译时用 -Wl,--version-script=export.lst 传给链接器:
g++ -shared -fPIC -o libmyplugin.so plugin.o -Wl,--version-script=export.lst
常见错误:为什么写了 version-script 还是出现 undefined symbol
最常踩的坑是符号名对不上。比如你写了 global:MyInit,但实际编译出来的符号是 _Z6MyInitv(C++ name mangling),那这个符号根本不会被导出。验证方式很简单:
- 先看目标文件里有没有这个符号:
nm -C plugin.o | grep MyInit(-C解析 C++ 名) - 再看生成的 so 是否导出了它:
nm -D libmyplugin.so | grep MyInit(-D只查动态符号表) - 如果第二步没输出,说明 version-script 没生效,优先检查拼写、mangling、路径是否写错
另一个典型问题是路径写错:-Wl,--version-script=export.lst 中的 export.lst 必须是相对当前工作目录的路径,不是相对于源码目录或 Makefile 目录。
和 -Bsymbolic、-fvisibility=hidden 的关系怎么理
三者定位不同,不是互斥,而是可以叠加使用:
-
-fvisibility=hidden是编译期默认策略,影响所有符号的 visibility 属性,但可被default属性覆盖 -
-Wl,-Bsymbolic是链接期行为,让 so 内部调用优先绑定本 so 内定义的符号(防“跨 so 调用跑偏”),但它不控制导出,外部仍能看到所有未隐藏的符号 -
--version-script是最终裁决者,它在链接末期强制重写动态符号表,连default属性都压不住它
所以推荐组合:源码加 -fvisibility=hidden + 关键函数加 default + 链接加 --version-script。这样既有编译期防护,又有链接期兜底。
真正容易被忽略的是:version-script 对 C++ 模板、内联函数、匿名命名空间里的符号天然无效——它们本来就不会进入动态符号表。别指望靠它去“隐藏”这些,重点盯住你明确想暴露给外部的那几个 C 风格接口函数就行。


















