gdiplus函数调用错误导致程序Crash分析

来源:互联网 发布:爱否商城 知乎 编辑:程序博客网 时间:2024/06/05 04:50

前几天看一个Crash的问题。功能是Word上的一个插件,点击上面的一个button,cation实现了,但点击结束后,过一会Word就crash了。下面是问题的分析.

 

 

调试问题

 

问题复现

程序crash时,获得如下log.

 

0x4aea74b2,指令无效。是访问内存溢出导致EIP被修改,还是对应模块从内存中unload出去了?

 

开始调试

上调试器,看看是否有模块被unload了。

 

 

从L6和L29的内容,说明准备执行unloaded模块GdiPlus中的函数,导致访问违例。

这时我们可以继续跟踪看看这个地址偏移量是什么code,也可以使用Application Verifier看看它能不能给我们更多的信息。

 

 

Application Verifier

 

下面是启动Application Verifier后的调试结果。

上面的调试告诉我们,一个Critical section在程序unloading gdiplus模块时没有释放。函数调用指令地址为0x0c974948,说明是模块dllistmt.dll启动了这个函数。但从那里启动的,我们看不出来。

 

寻找entire stack trace

给gdiplus!GdiplusStartup下断点,看看整个函数调用堆栈。

断点如期命中,但没看到期望中的完整函数堆栈。

去official的build package中找不到dllistmt模块的pdb文件。尝试恢复stack trace第一次失败。

找map文件,没有。尝试恢复stack trace第二次失败。

手动build一个新的dllistmt,同时获得pdb文件,替换到程序中,问题不能复现了。尝试恢复stack trace第三次失败。

 

虽然上面提示Following frames may be wrong,但我们看到ebp=0就闪出一个念头,是否是stack overflow导致EIP被改写,而使程序稀里糊涂执行了这条指令?

 

stack overflow?

看dllistmt模块的code,看到dllmain第一个函数调用是DisableThreadLibraryCalls,下断点再次运行。

  

从上面的分析,我们可知dllistmt!DllUnregisterServer+0x108df这是在执行dllistmt的dllmain函数,但这个地方编译器生成了一个奇怪的指令,把ebp设为0了,这样在没有pdb的情况下stack就不能正确地恢复了(这里尝试了汇编代码的单步跟踪,最后是运行到了gdiplus!GdiplusStartup。不过,这个过程比较枯燥,这里就忽略了)。调试时发现,这个简单的动作加载了8~9 dll,还启动了几个线程,感觉里面的初始化动作很复杂。怀疑里面一个模拟COM进程内通讯的模块有问题,化时间看了部分code,做了些调试,也没有发现根本问题(dllistmt模块里,有一个地方调用GdiplusStartup,但这个button的action根本就不执行到这段code)。

 

第一次迷茫

走不下去了,困惑中想到GdiplusStartup是干什么的,为什么需要Critical section,而且一直不释放它?

上google找,打开URL http://msdn.microsoft.com/en-us/library/ms534077(v=vs.85).aspx获得如下信息。

 原来初始化gdiplus需要调用GdiplusStartup,关闭gdiplus需要调用GdiplusShutdown。这个Critical section是在初始化的时候产生的,那可能是调用GdiplusShutdown时释放它,但这里却说没释放它,奇怪。对GdiplusShutdown下断点,再次调试,结果到异常发生了,这个断点也没有命中。

 

第二次迷茫

很是困惑,再次停下来分析。

1。action开始时,dllms模块动态加载dllistmt模块,dllistmt静态加载gdiplus模块。

2。action结束时,dllms模块动态unload dllistmt模块,因为这个时候gdiplus模块的引用计数为1,gdiplus这个时候也被自动unload出系统。

3。整个过程中,dllistmt没显示调用gdiplus的code,可gdiplus模块的初始化函数被调用了,对应的析构函数却没被调用,这导致6号线程发生了异常。等等,6号线程,难道就是上面说的gdiplus的后台线程?

 

大胆的假设

dllms在加载dllistmt模块时,一个我们现在还不清楚系统调用,激活了gdiplus,调用了GdiplusStartup。这个函数又创建了gdiplus的后台线程。当程序unload dllistmt模块,gdiplus模块也被unload,但没有机会调用GdiplusShutdown,所以gdiplus的后台线程还在,这样当系统切换到这个线程,因调用无效内存(gpiplus原来再系统中的代码段)指令而产生访问违例。这个分析跟当前的现象很吻合。

 

验证假设

   

调试结果跟分析的一样,gdiplus启动了线程gdiplus!BackgroundThreadProc。这样当gdiplus模块unload出去后,这个线程在执行BackgroundThreadProc函数时就产生访问违例而crash了。

 

 

解决办法

由于不是太清楚具体的code逻辑,就建议维护code的人员在load dllistmt模块时,显式的调用GdiplusStartup,在unload时再显式的调用GdiplusShutdown来归避这个问题。

 

 

经验与教训

1。如果dllistmt模块有pdb文件,那就可以立刻定位哪里调用了GdiplusStartup,而发现问题的root cause。

2。如果一开始就熟悉gdiplus的调用规则,知道GdiplusStartup和GdiplusShutdown需要配对。同时GdiplusStartup可能会创建后台线程,那就不用等到google后,才对特殊的规则做相应的调试。丰富的背景知识是调试的基础啊。

3。其实在遇到critical section错误的时候,如果看一下所有的线程stack也可能早的想到这个假设,但当时好像注意这个东西。有时,成功看起来是离我们如此的接近。

 

 

 

 

 

 

 

 

 

 

 

 

 

原创粉丝点击