[原]windbg调试系列——崩溃在ComFriendlyWaitMtaThreadProc

前言

这是几年前在项目中遇到的一个崩溃问题,崩溃在了ComFriendlyWaitMtaThreadProc()里,没有源码。耗费了我很大精力,最终经过反汇编并结合原代码才最终搞清楚了事情的前因后果。本文的分析仍是基于真实项目进行的,中间略去了不少反汇编的分析工做。文末有我整理的测试代码,你们能够实际体验一把TerminateThread()的杀伤力。git

背景介绍

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

问题分析

使用windbg载入dump文件,输入.ecxrbash

ecxr

从输出结果能够看出是访问到无效的地址0x07acf914了,使用命令!address 07acf914查看该地址的信息:微信

address-07acf914

从输出结果能够看出该地址确实是不可访问的。咱们须要看看0x07acf914 是从哪里来的,该值来自edi+4指向的地址所存储的值,那么edi的值是哪里来的呢?让咱们看看前几条汇编指令是什么,输入ub 7303f614 L10函数

ub-7303f614-L10

说明:测试

7303f614这个地址是我经过7303f611+3算来的(3是指令长度),这样就能够在输出结果中看到致使崩溃的这条指令啦。固然这里输入ub 7303f611也不要紧(咱们关心的是edi的值是哪里来的),只不过咱们看不到7303f611对应的指令了。ui

咱们发现edi的值来自ebp+8对应的地址内容。研究过反汇编的小伙伴儿应该对ebp+n比较敏感,有木有?在windows下,32位进程中,ebp+8指向了调用约定为__stdcall的函数的第一个参数。ebp+8是否指向了第一个参数,咱们须要经过ComFriendlyWaitMtaThreadProc()的调用约定来判断。spa

输入k查看调用栈: 操作系统

k

从调用栈可知,ComFriendlyWaitMtaThreadProc()是在新线程中执行的,经过查看CreateThread()的原型咱们能够知道 ComFriendlyWaitMtaThreadProc() 原型应该知足typedef DWORD (__stdcall LPTHREAD_START_ROUTINE)(LPVOID lpThreadParameter);插件

综上可知ebp+8确实指向了第一个参数,这个参数指向了一个非法的地址!

我猜想有以下两种可能:

  1. 调用函数传递了一个合法地址,因为某种缘由这个地址无效了。(最后证实,咱们的代码里传递了一个栈上的局部变量,可是调用线程挂掉了,栈对应的内存无效了!)
  2. 代码中存在bug,传递参数的时候就传的有问题!(可能性过低了,对本身的代码比较有信心😂)

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

bu uiautomationcore!ComFriendlyWaitMtaThreadProc
g复制代码

断下来后,使用~*k查看全部线程的调用栈,通过排查,11号线程18号线程最值得怀疑。

thread-11

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()强制杀线程!除非你想故意埋坑!😂
  • windbg真是windows下的调试利器,再向你们安利一波。
  • 调试崩溃,死锁问题要大胆推测,当心求证。

参考资料

  • windbg帮助文档
  • 《格蠹汇编》
BianChengNan wechat
扫描左侧二维码关注公众号,扫描右侧二维码加我我的微信:)
相关文章
相关标签/搜索