最近在新项目中遇到一个崩溃的问题,大致的过程就是在一个动态库中new了另一个动态库中导出的类对象,然后将new的指针又保存回另一个动态库的容器中,当整个程序退出时,释放这个容器中保存的指针,在释放时发生崩溃,此时观察对象的虚表,发现已经野掉。
遇到问题的第一反应是这个对象是不是已经被释放过了,由于重复释放导致的崩溃,于是在类的析构函数中放上断点,再次运行,结果断点没进,直接崩溃掉。好吧,再次调试,在new创建对象以后放上数据断点监视虚表,退出程序,这一次断点停下来了,停在了new这个对象的模块释放的地方,咦,这不很奇怪,在我已有的认识里面,对象的虚表应该在类所在的模块才对啊,怎么跑到其它模块去了呢?带着疑问又一次调试,反汇编跟踪对象的构造过程,跟进对象的构造函数,赋给对象的虚表就是类所在的模块啊,这就奇怪了,为什么在释放的时候虚表变成了其它模块呢?难道是有内存溢出把虚表覆盖了?继续跟踪,在跳出对象的构造函数以后,看到了一行奇怪的代码,在new对象的模块生成了一个local vftable再次赋给了虚表。这是什么骚操作,对象已经有虚表了,再赋一个local vftable干啥。
下面用一个简单的程序模拟上面的过程:
在module1动态库中定义一个TestA,整个类导出导入
在module1中再定义另外两个导出函数
在module2动态库中定义一个函数,在堆上创建TestA的对象,并调用module1中的saveTestA函数保存创建的指针
在LocalVftable主程序中采用LoadLibrary的方式隐式使用module1及module2,当调用clearTestA函数时崩溃
调试TestA的构造过程,会将TestA::vftable赋给对象首地址内存,并且观察内存地址范围可以发现TestA::vftable在module1中
跳出构造,继续调试会发现编译器又用一个local vftable覆盖了对象原有的虚表!并且这个local vftable在module2中
后面在释放创建的TestA对象时由于module2已经卸载,所以虚表中的虚函数地址都已经野掉
好了,经过上面的示例程序的分析,问题很清晰了,那该怎么解决呢?最佳实践当然是哪个模块new就由哪个模块释放,这样就不会存在问题,可是现实情况很多时候有这种需求。应该有其他人遇到过同样的问题吧,于是开始百度搜索local vftable,有用的资料极少,后面在谷歌上找到了一些有用信息。
大致的意思就是微软为了解决new和delete不匹配(不在同一个模块)的问题,在5.0以后的版本就搞了一个local vftable,这个local vftable解决了一些问题,当也引入了新问题,如果提前FreeLibrary了相应模块,那么local vftable就不存在了,好在有非常简单的方法避免生成local vftable。
解决方法很简单,就是整个类导出时另一个使用这个类的模块不要使用dllimport或者在单独需要导出的函数上使用dllimport。不过这又会带来一个问题,就是如果类上有静态成员变量,则无法导出,另一个使用的模块会报链接错误,好在也有解决方法,就是在静态成员变量上单独使用dllimport。最终的类变成下面这样:
类TestA导出不导入,这样在module2中编译器就不会额外生成一个local vftable
总结:local vftable的问题并不一定会暴露出来,需要满足很多条件,如果主模块是显示依赖动态库或者不满足特定的卸载顺序都不会出现问题,但是一旦出现崩溃,那就比较难调试,并且百度能查到的资料还很少。