• [原]调试实战——使用windbg调试崩溃在ComFriendlyWaitMtaThreadProc


    前言

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

    背景介绍

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

    问题分析

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

    ecxr
    ecxr

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

    address-07acf914
    address-07acf914

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

    ub-7303f614-L10
    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查看调用栈:

    k
    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号线程最值得怀疑。

    thread-11
    thread-11

    thread-18
    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帮助文档
    • 《格蠹汇编》
  • 相关阅读:
    Eclipse consle 输出语句中,误输中文,假死问题
    Eclipse 快捷键 (最实用)
    mysql 4种启动方式
    mysql索引的类型和优缺点
    Windows上 使用Composer安装tp5
    php 更新配置文件
    可视化工具连接Linux上的redis
    HttpClient 4 教程 第3章 HTTP状态管理
    HttpClient 4 教程 第2章 连接管理
    HttpClient 4 教程 第1章 基础
  • 原文地址:https://www.cnblogs.com/bianchengnan/p/6226780.html
Copyright © 2020-2023  润新知