近日在工作中碰到了个奇怪的程序崩溃问题,当测试动态模块更新的时候,无论是不是有实际的模块更新,
只要在运行过程中重新加载模块就会crash,一番调试过后,排除了不少常见问题
-
多线程访问引起的冲突
-
对旧模块引用没有释放
-
使用了已经释放了的内存指针
最后发现崩溃的触发原因不是业务模块,而是发生在一个工具模块里,而且触发点也只是设置一下状态标志位。
工具模块中具体实现的代码大致如下
Settings * GetSettings() {
static Settings * sSettings = new Settings;
return sSettings;
}
...
GetSettings()->SetReloaded(true);
...
代码看上去没有问题,并且在前后上下文也有注意加锁防止多线程冲突。、
在检查了内存指针后,唯一特殊的就是sSettings的内存地址不在模块的地址空间,而是处于进程的地址空间。
这好像有点意思啊,我印象中静态变量应该是在模块地址空间的,那么是不是编译器的生成了特殊的代码呢。
nm的结果显示sSettings的类型是 u ,并不是 b ,那么 u 和 b 都是代表什么意思呢
"b"
The symbol is in the uninitialized data section (known as BSS ).
"u"
The symbol is a unique global symbol. This is a GNU extension to the standard set of ELF symbol bindings.
For such a symbol the dynamic linker will make sure that in the entire process there is just one symbol
with this name and type in use.
看上去 u 没啥问题啊,至少保证了无论模块怎么加载,卸载都会使用同一个地址访问静态变量。经过仔细的调试发现,为了防止内存泄露,
模块的卸载代码有清理内存的操作,大概类似
...
delete GetSettings();
...
而同时 u 保证了静态变量真的只会运行一次初始化代码,而不是每次加载模块的时候都会运行,这就造成了卸载再加载后访问crash。
原因找到了,那么解决方案是什么呢?
- 删除清理代码,反正整个进程只有一份,退出时自动清理了,不会内存泄露。但是 u 是GNU的扩展,使用其他编译器或者跨平台的时候可能有问题。
- 增加工具模块的导出表,不要导出sSettings,让它待在模块的bss区域,这样每次模块加载都会有自己的一份拷贝,每个模块都会自己初始化和释放。
对于linux平台来说,link的时候加入
LDFLAGS += -Wl,--version-script=$(srcdir)/libfoo.map
LD文档里有详细介绍,简单举例一下,global里放要导出的符号, local *的意思就是其它都hide
LIBFOO_1.0 {
global:
libfoo_init; libfoo_doit; libfoo_done;
local:
*;
};
对于windows平台来说,有多种方法,参考: Exporting from a DLL | Microsoft Docs