[原]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
掃描左側二維碼關注公衆號,掃描右側二維碼加我我的微信:)
相關文章
相關標籤/搜索