用dlopen加载插件并获取函数指针的核心是:编译独立.so,运行时dlopen打开,再用dlsym按extern "C"导出的符号名获取函数地址;必须用extern "C"避免C++名字修饰导致找不到符号,函数指针需强转为对应类型(如void()()),对象内存由插件管理并通过其destroy函数释放。

如何用 dlopen 加载插件并获取函数指针
核心是把业务逻辑编译成独立的 .so,运行时用 dlopen 打开,再用 dlsym 拿到导出函数地址。必须确保插件中导出的函数用 extern "C" 声明,否则 C++ 名字修饰(name mangling)会让 dlsym 找不到符号。
插件源码示例(plugin.cpp):
extern "C" {
// 插件入口,返回一个可调用对象指针
void* create_processor() {
return new MyProcessor();
}
// 可选:显式卸载逻辑
void destroy_processor(void* p) {
delete static_cast<myprocessor>(p);
}
}</myprocessor>
主程序加载时:
-
void* handle = dlopen("./libmyplugin.so", RTLD_LAZY);失败时检查dlerror() - 用
dlsym(handle, "create_processor")获取函数指针,注意强转为对应函数类型(如void*(*)()),不是void* - 调用后得到的对象内存由插件管理,主程序不能直接
delete,必须通过插件提供的destroy_processor释放
为什么 RTLD_LOCAL 比 RTLD_GLOBAL 更安全
插件依赖的符号(比如它自己链接的 libstdc++.so 中的 std::string 构造函数)默认只在插件内部可见。若用 RTLD_GLOBAL,这些符号会注入到全局符号表,可能覆盖主程序或其他插件同名符号,引发崩溃或行为异常——尤其当多个插件用不同 STL 版本编译时。
- 除非插件明确需要被后续加载的模块调用其符号,否则一律用
RTLD_LOCAL - 主程序若需向插件传入回调函数,应通过函数指针参数传递,而非依赖全局符号
-
dlopen同一路径多次不会重复加载,但dlclose后再次dlopen是全新实例,静态变量重置
插件卸载前必须清理哪些资源
dlclose 不等于“立即卸载”,它只是减少引用计数;只有计数归零且无任何代码在插件段内执行时,系统才真正释放内存。若插件里开了线程、注册了信号处理、分配了 mmap 内存或持有文件描述符,dlclose 后这些资源仍存在,变成悬空状态。
- 插件必须提供显式的
cleanup()或类似接口,在dlclose前由主程序调用 - 避免在插件构造函数/析构函数中做跨模块操作(如调用主程序函数),因析构时机不可控
- 线程不能在
dlclose后继续访问插件代码段,否则触发Segmentation fault
常见崩溃场景和定位方法
最典型的是 double free 或 use-after-free:主程序用 new 分配对象传给插件,插件又用 delete 释放;或插件用 malloc 分配内存,主程序用 delete 释放。两者堆管理器不一致,必然崩。
- 统一约定内存归属方:谁分配谁释放,跨边界传递只传指针,附带配套释放函数
- 用
LD_DEBUG=libs,symbols查看实际加载了哪些.so和符号解析路径 - 崩溃时用
gdb看栈帧是否落在插件地址范围,再用info proc mappings对照/proc/PID/maps确认模块是否已卸载
热插拔不是“换个 so 文件就完事”,关键在生命周期对齐和 ABI 边界清晰。插件接口一旦发布,字段增减、虚函数表变动、异常抛出跨越 dlopen 边界,都会让事情迅速失控。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











