程序员必知:dlsym到底是什么?一文看懂动态链接库符号查找
一个看似冷门却无处不在的C语言函数
你有没有想过,像Photoshop这样的大型软件,为什么能支持那么多插件?微信为什么能热更新,不用每次重新下载整个安装包?Android手机上的应用,插件化技术是怎么实现的?
这一切的背后,都离不开一个低调却强大的C语言函数——dlsym。
一、dlsym是什么?说白了就是"找地址"
dlsym的全称是Dynamic Linker Symbol,翻译过来就是"动态链接器符号查找"。
听着很绕?打个比方你就懂了。
想象你开了一个图书馆(程序),书架上有很多书(共享库)。你要找某一本书(某个函数),但书太多了不知道在哪儿。这时候你有一个超级管理员(dlsym),你告诉他书名,他就能精准地告诉你书放在哪个架子上(函数的地址)。
用专业的话说:dlsym可以在程序运行时,从已经加载的动态链接库(比如Linux的.so文件、macOS的.dylib文件)中找到某个函数或变量的内存地址。
二、它和dlopen、dlclose是"铁三角"
dlsym一般不会单独出现,它总是和另外两个函数一起用,被称为"dl三件套":
| 函数 | 作用 | 好比 |
|---|---|---|
dlopen |
打开一个动态库文件 | 把图书馆的大门打开 |
dlsym |
从库中查找某个符号(函数/变量)的地址 | 找书在哪个书架 |
dlclose |
关闭动态库 | 把图书馆门关上 |
代码大概长这样(非程序员可跳过):
// 1. 打开库
void* handle = dlopen("libplugin.so", RTLD_LAZY);
// 2. 找"create_plugin"函数的地址
void* func = dlsym(handle, "create_plugin");
// 3. 调用这个函数
// ... 执行插件逻辑 ...
// 4. 关闭库
dlclose(handle);
三、它到底有什么用?三个场景最常见
场景一:插件系统(最经典)
微信的"小程序"、Chrome的"扩展"、Photoshop的"滤镜插件",都是通过类似dlsym的技术实现的。
主程序根本不知道插件里有什么函数,运行时才通过dlsym去"问"插件:你有什么功能?把你的创建函数告诉我。
场景二:Hook/劫持(黑客和逆向工程师的爱)
有些安全工具或调试工具,可以"拦截"标准函数的调用。比如你想知道程序每次调用malloc申请了多少内存,可以通过dlsym找到malloc的地址,然后替换成自己的函数,在里面加一行日志。
这在安全分析和性能调试中非常常用。
场景三:按需加载(优化启动速度)
大型软件如果一启动就把所有库都加载完,启动会非常慢。聪明的做法是:先加载核心功能,等到用户点了某个按钮,再用dlopen+dlsym去加载对应的库。
这就叫"延迟加载"(Lazy Loading)。
四、新手最常踩的四个坑
坑1:编译时符号被"藏起来"了
如果你编译共享库时加了-fvisibility=hidden参数,所有符号默认都是"隐身"的。这时候dlsym找不到任何东西。解决办法是在你想暴露的函数前面加上:
__attribute__((visibility("default")))
void my_function() { ... }
坑2:C++的函数名被"魔改"了
C++支持函数重载(同名不同参数),编译器为了区分,会把函数名改成乱七八糟的名字。比如foo(int)可能变成_Z3fooi。
如果你直接用dlsym(handle, "foo")是找不到的。两种解决办法:
- 加上
extern "C",告诉编译器按C语言方式命名 - 用
nm -C命令查看"还原后"的真实名字
坑3:返回NULL分不清是"没找到"还是"本身就是NULL"
dlsym如果找不到符号,返回NULL。但如果符号本身的值就是NULL(虽然很少见),它也返回NULL。
怎么区分?用dlerror()函数。如果dlerror()返回了错误信息,说明是"没找到";如果返回NULL,说明符号本身确实是NULL。
坑4:错误信息被覆盖
dlerror()有个特性:你读一次,它就把错误记录清了。所以调用完dlsym后,必须立即读取dlerror(),否则错误信息就丢了。
五、高能预警:dlsym存不了ABI,为什么?
有读者问了一个非常专业的问题:"JSON能存函数清单,但存不了ABI是什么意思?"
这个问题值得单独说清楚。
JSON能存什么? 函数的名字、参数类型、返回值类型——这些是"文本信息",可以写进JSON文件。
ABI是什么? ABI(Application Binary Interface,应用程序二进制接口)是一套"规则",规定了:
- 参数是通过寄存器传递还是通过栈传递?
- 寄存器的使用顺序是什么?
- 谁负责清理栈空间?
- 结构体在内存里怎么排列、有没有填充字节?
这些都是运行时行为规则,不是"数据",所以写不进JSON。
一个例子你就懂了:
同一个函数add(int a, int b),在x86_64架构上,两个参数放在rdi和rsi寄存器;在ARM架构上,放在r0和r1寄存器。
JSON只知道"有两个int参数",但不知道调用时该往哪个寄存器放。这个"知识"是由编译器根据目标平台自动处理的。
打个比方:
- JSON = 菜谱上的"食材清单"(有什么、多少克)
- ABI = "烹饪方法"(先炒还是先蒸、火候多大)
你能把"食材清单"写在纸上,但"烹饪方法"这个动作本身是写不进去的,只能靠厨师(编译器)按既定规则执行。
六、有没有比dlsym更好的选择?
如果你觉得dlsym不够用,或者用起来太麻烦,根据不同场景有不同选择:
场景一:想跨平台(Windows/Linux/macOS都用)
用C++的dylib库,它对dlopen/dlsym(Linux/macOS)和LoadLibrary/GetProcAddress(Windows)做了统一封装,写一次代码,三个平台都能跑。
场景二:在Android上做插件或Hook
用xDL,它是Android专用的增强版dlsym,能绕过Android 7.0以后的命名空间限制,还能找到"未导出"的隐藏函数。
场景三:在苹果(iOS/macOS)上调用私有API
用libSymRez或SymbolLocator,它们通过手动解析Mach-O文件(苹果的可执行文件格式)的符号表,能找到dlsym找不到的非公开函数。
场景四:在Rust语言中追求极致性能
用dlopen-rs或Relink,它们完全用Rust重写了动态链接器,支持从内存直接加载库,甚至支持运行时热更新(不重启程序就替换代码)。
七、总结
| 你想知道的 | 一句话答案 |
|---|---|
| dlsym是什么 | 动态库符号查找函数,在运行时找函数地址 |
| 全称是什么 | Dynamic Linker Symbol |
| 和谁一起用 | dlopen打开,dlsym查找,dlclose关闭 |
| 主要用途 | 插件系统、函数Hook、延迟加载 |
| 最大的坑 | C++名字粉碎、符号可见性、NULL歧义 |
| 能否被替代 | 可以,根据平台和场景有不同替代方案 |
如果你读到了这里,说明你对底层技术有真正的兴趣。
dlsym这个函数,普通应用层开发者可能几年都用不上一次,但一旦你需要做插件化、热修复、性能监控、安全审计,它就变成了"救命稻草"。
技术就是这样——有些东西平时用不到,但用到的时候,你必须懂。
本文由"编程技术杂谈"原创,欢迎转发,转载请注明出处。