热更新本质是通过动态链接实现模块替换,而非裸指针操作;核心步骤为编译共享库→dlopen加载→dlsym获取函数地址→函数指针调用,需配合dlclose卸载、符号隔离与接口原子切换。

热更新本质不是靠指针,而是靠动态链接和函数指针跳转
直接用裸指针(比如 int* 或 void*)无法实现模块热更新——它不负责加载/卸载代码,也不处理符号重定位。真正起作用的是:把新模块编译成共享库(.so / .dll),运行时用 dlopen / LoadLibrary 加载,再用 dlsym / GetProcAddress 拿到函数地址,最后通过函数指针调用。指针在这里只是“跳板”,不是机制核心。
常见错误现象:Segmentation fault 在第二次加载后触发,往往是因为旧库没卸载干净、全局状态冲突,或函数指针仍指向已释放的代码段。
- 必须确保模块导出 C 风格符号(用
extern "C"),否则 C++ 名字修饰会让dlsym找不到函数 - 不要在模块里 new 全局对象或注册 atexit 回调——卸载时这些不会被自动清理
- 热更新前,必须先调用
dlclose(且确认引用计数归零),否则新版本加载可能失败或旧代码仍在执行
如何安全地切换函数指针而不崩溃
不能让正在执行的线程突然跳到新模块的同名函数里——尤其是该函数还在用旧模块里的静态变量或虚表。正确做法是:定义统一接口结构体,用原子指针(std::atomic<void></void>)或互斥锁保护其更新,并确保调用方每次都通过该指针间接调用。
示例接口定义:
立即学习“C++免费学习笔记(深入)”;
struct ModuleAPI {
int (*process)(const char*, int);
void (*cleanup)();
};
更新逻辑要点:
- 新模块加载成功后,先调用其
init()(需约定导出),验证功能正常 - 用
std::atomic_store_explicit(¤t_api, new_api_ptr, std::memory_order_release)原子更新指针 - 调用方始终写成
current_api.load()->process(...),而不是缓存局部副本 - 旧模块的
cleanup()必须在确认无任何线程正在使用它之后才调用dlclose
Windows 下 LoadLibrary 失败但没报错?检查路径和依赖
LoadLibrary 返回 NULL 时,GetLastError() 才是关键。常见却被忽略的原因:
- 路径含中文或空格但没用宽字符版
LoadLibraryW,或没加引号 - 目标 DLL 依赖的其他 DLL 不在 PATH 或当前目录(可用
dumpbin /dependents查) - 32/64 位不匹配:主程序是 x64,却尝试加载 x86 的 DLL → 错误码
193 (ERROR_BAD_EXE_FORMAT) - DLL 已被加载过(即使不同路径),Windows 默认按模块名去重,导致
GetProcAddress返回旧地址
解决办法:用完整绝对路径 + LOAD_WITH_ALTERED_SEARCH_PATH 标志,并确保所有依赖都可达。
Linux 下 dlopen 后内存泄漏?别漏掉 RTLD_LOCAL
默认 dlopen(filename, RTLD_NOW) 等价于 RTLD_GLOBAL,会把符号注入全局符号表——后续其他 dlopen 可能意外绑定到旧版本函数,造成行为错乱,且 dlclose 后部分资源(如 .init_array 中的代码)未必彻底释放。
- 一律显式用
RTLD_NOW | RTLD_LOCAL,避免符号污染 - 用
nm -D your_module.so确认只导出必要函数,隐藏内部符号(编译加-fvisibility=hidden,导出函数加__attribute__((visibility("default")))) -
dlclose不保证立即释放内存——glibc 只在引用计数为 0 时标记为可回收,实际释放时机不确定;别依赖它做资源强同步
真正的难点不在怎么换指针,而在于模块边界是否干净:有没有跨模块的 new/delete、std::string 生命周期、线程局部存储(TLS)或信号处理函数。这些一旦越界,热更新就从便利变成定时炸弹。


















