程序员必知:dlsym到底是什么?一文看懂动态链接库符号查找

📄 文章 🌐 公开
📋 列表 ✏️ 编辑 🎨 画布版 📋 复制MD 🌐 复制HTML ☆ 收藏

程序员必知: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架构上,两个参数放在rdirsi寄存器;在ARM架构上,放在r0r1寄存器。

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

libSymRezSymbolLocator,它们通过手动解析Mach-O文件(苹果的可执行文件格式)的符号表,能找到dlsym找不到的非公开函数。

场景四:在Rust语言中追求极致性能

dlopen-rsRelink,它们完全用Rust重写了动态链接器,支持从内存直接加载库,甚至支持运行时热更新(不重启程序就替换代码)。

七、总结

你想知道的 一句话答案
dlsym是什么 动态库符号查找函数,在运行时找函数地址
全称是什么 Dynamic Linker Symbol
和谁一起用 dlopen打开,dlsym查找,dlclose关闭
主要用途 插件系统、函数Hook、延迟加载
最大的坑 C++名字粉碎、符号可见性、NULL歧义
能否被替代 可以,根据平台和场景有不同替代方案

如果你读到了这里,说明你对底层技术有真正的兴趣。

dlsym这个函数,普通应用层开发者可能几年都用不上一次,但一旦你需要做插件化、热修复、性能监控、安全审计,它就变成了"救命稻草"。

技术就是这样——有些东西平时用不到,但用到的时候,你必须懂。

本文由"编程技术杂谈"原创,欢迎转发,转载请注明出处。

💬 留言 ⋮⋮

加载中…
💡 不登录也可留言(IP 限制:每文/每天各 1/10 条)

加载中…

纸张白
护眼绿
羊皮卷
夜间黑
100%