原调试debugwindbgcrash崩溃COM

前言

这是几年前在项目中遇到的一个崩溃问题,崩溃在了​​ComFriendlyWaitMtaThreadProc()​​里,没有源码。耗费了我很大精力,最终通过反汇编并结合原代码才最终搞清楚了事情的来龙去脉。本文的分析还是基于真实项目进行的,中间略去了很多反汇编的分析工作。文末有我整理的测试代码,大家可以实际体验一把​​TerminateThread()​​的杀伤力。


背景介绍

大概情况是这样的:程序启动的时候,会通过​​LoadLibrary()​​加载插件模块。其中的​​UIA​​模块会开启一个工作线程,工作线程会安装​​UIA​​相关的钩子来监听​​UIA​​事件,程序在退出的时候会调用每个插件模块的导出函数做清理工作,然后调用​​FreeLibrary()​​释放这个插件模块。​​UIA​​模块的清理函数会通知工作线程退出,工作线程收到退出命令后会卸载相关钩子。清理函数会等待工作线程一段时间,等待超时就通过​​TerminateThread()​​强制杀死工作线程。程序退出时偶尔会崩溃在​​ComFriendlyWaitMtaThreadProc()​​中。背景介绍完毕,下面开始分析​​dump​​文件。


问题分析

使用​​windbg​​载入​​dump​​文件,输入​​.ecxr​[原]调试实战——使用windbg调试崩溃在ComFriendlyWaitMtaThreadProc_工作线程

ecxr

从输出结果可以看出是访问到无效的地址​​0x07acf914​​了,使用命令​​!address 07acf914​​查看该地址的信息:[原]调试实战——使用windbg调试崩溃在ComFriendlyWaitMtaThreadProc_局部变量_02

address-07acf914

从输出结果可以看出该地址确实是不可访问的。我们需要看看​​0x07acf914​​ 是从哪里来的,该值来自​edi+4​​指向的地址所存储的值,那么​​edi​​的值是哪里来的呢?让我们看看前几条汇编指令是什么,输入​​ub 7303f614 L10​[原]调试实战——使用windbg调试崩溃在ComFriendlyWaitMtaThreadProc_反汇编_03

ub-7303f614-L10

{% note info %}

说明:

​7303f614​​这个地址是我通过​​7303f611+3​​算来的(​​3​​是地址​​7303f611​​对应的指令长度),这样就可以在输出结果中看到导致崩溃的这条指令啦。当然这里输入​​ub 7303f611​​也没关系(我们关心的是​​edi​​的值是哪里来的),只不过我们看不到​​7303f611​​对应的指令了。

{% endnote %}

我们发现​​edi​​的值来自​ebp+8​​对应的地址内容。研究过反汇编的小伙伴儿应该对​​ebp+n​​比较敏感,有木有?在​​windows​​下,​​32​​位进程中,​​ebp+8​​指向了调用约定为​​__stdcall​​的函数的第一个参数。这里的​​ebp+8​​是否指向第一个参数,我们需要通过​​ComFriendlyWaitMtaThreadProc()​​的调用约定来判断。输入​​k​​查看调用栈:[原]调试实战——使用windbg调试崩溃在ComFriendlyWaitMtaThreadProc_反汇编_04

k

从调用栈可知,​​ComFriendlyWaitMtaThreadProc()​​是在新线程中执行的,通过查看​​CreateThread()​​的原型我们可以知道 ​​ComFriendlyWaitMtaThreadProc()​​ 原型应该满足​​typedef DWORD (__stdcall LPTHREAD_START_ROUTINE)(LPVOID lpThreadParameter);​​ 。综上可知,​​ebp+8​​确实指向了第一个参数,这个参数指向了一个非法的地址!

我猜测有如下两种可能:


  1. 调用函数传递了一个合法地址,由于某种原因这个地址无效了。(最后证明,我们的代码里传递了一个栈上的局部变量,但是调用线程挂掉了,栈对应的内存无效了!)
  2. 代码中存在bug
    ,传递参数的时候就传的有问题!(可能性太低了,对自己的代码比较有信心:joy:)


追本溯源

单纯从​​dump​​看不出更多的信息了!于是我决定给 ​​ComFriendlyWaitMtaThreadProc()​​下断点,看看是否能找到是谁创建了这个线程! 执行如下命令:

bu uiautomationcore!ComFriendlyWaitMtaThreadProc
g

断下来后,使用​​~*k​​查看所有线程的调用栈,经过排查,​​11号线程​​和​​18号线程​​最值得怀疑。[原]调试实战——使用windbg调试崩溃在ComFriendlyWaitMtaThreadProc_工作线程_05

thread-11

[原]调试实战——使用windbg调试崩溃在ComFriendlyWaitMtaThreadProc_局部变量_06

thread-18

​18号线程​​是出问题的线程。​​11号线程​​包含我们自己的代码,而且​​ComFriendlyWaitForSingleObject()​​跟​​ComFriendlyWaitMtaThreadProc()​​相似度不要太高。大胆猜测内部逻辑应该是:函数​​AddWinEvent()​​内部会创建一个工作线程,​​uiautomationcore!ComFriendlyWaitMtaThreadProc()​​是新线程的入口函数,创建完线程后通过调用​​ComFriendlyWaitForSingleObject()​​等待一个内核对象(通过反汇编确认该对象为​​Event​​)来等待工作线程结束。理所当然的,​​uiautomationcore!ComFriendlyWaitMtaThreadProc()​​结束后应该会激活这个内核对象。

经过一系列的小()心()谨()慎()的反汇编,检查代码,确认逻辑,最终得到如下结论:

当主程序退出时,主线程做清理工作,会等待​​11号线程​​一段时间,如果等待超时就会调用​​TerminateThread()​​将其强行杀死(正是这个​​TerminateThread()​​的调用导致了崩溃)! 而​​18号线程​​会用到​​11号线程​​传过来的线程参数(​​11号线程​​的一个局部变量),如果​​11号线程​​被意外杀死了,那么​​11号线程​​的局部变量对应的地址就无效了,对这块内存的操作就是未定义的!至此真相大白!(中间还有很多相关细节太琐碎了,没有一一列出,这里直接写出了结论。)


解决

知道原因了,解决起来就很简单了。去掉对​​TerminateThread()​​的调用,由操作系统来清理未结束的线程即可。由于主程序会调用​​FreeLibrary()​​释放插件模块,所以主程序还需要特殊处理下,在退出的时候不调用​​FreeLibrary()​​释放​​UIA​​模块。为了让大家更好的理解问题的本质,更直观的感受下​​TerminateThread()​​的杀伤力,我特意编写了如下测试代码来模拟我在项目里遇到的问题。


测试代码

#include "stdafx.h"
#include "windows.h"
#include "process.h"

unsigned __stdcall SubWorkProc(void* param)
{
int* data = (int*)param;
while (1)
{
*data = 1;
Sleep(1000);
}

return 0;
}

unsigned __stdcall WorkProc(void* param)
{
int data = 0;
_beginthreadex(NULL, 0, &SubWorkProc, &data, 0, NULL);
while (1)
{
Sleep(1000);
}

return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
auto hThread = (HANDLE)_beginthreadex(NULL, 0, &WorkProc, NULL, 0, NULL);
Sleep(1000);
TerminateThread(hThread, 0xdead);
Sleep(INFINITE);
return 0;
}


总结


  • 永远不要使用TerminateThread()
    强制杀线程!除非你想故意埋坑!:joy:
  • windbg
    真是windows
    下的调试利器,再向大家安利一波。
  • 调试崩溃,死锁问题要大胆推测,小心求证。


参考资料


  • windbg
    帮助文档
  • 《格蠹汇编》