這是幾年前在項目中遇到的一個崩潰問題,崩潰在了ComFriendlyWaitMtaThreadProc()
裏,沒有源碼。耗費了我很大精力,最終經過反彙編並結合原代碼才最終搞清楚了事情的前因後果。本文的分析仍是基於真實項目進行的,中間略去了不少反彙編的分析工做。文末有我整理的測試代碼,你們能夠實際體驗一把TerminateThread()
的殺傷力。git
大概狀況是這樣的:程序啓動的時候,會經過LoadLibrary()
加載插件模塊。其中的UIA
模塊會開啓一個工做線程,在工做線程裏會安裝UIA
相關的鉤子來監聽UIA
事件,程序在退出的時候會調用每一個插件模塊的導出函數作清理工做,而後調用FreeLibrary()
釋放這個插件模塊。UIA
模塊的清理函數會通知工做線程退出,並等待工做線程一段時間,等待超時就經過TerminateThread()
強制殺死工做線程,工做線程收到退出命令後會卸載相關鉤子。程序退出時偶爾會崩潰在ComFriendlyWaitMtaThreadProc()
中。背景介紹完畢,下面開始分析dump
文件。windows
使用windbg
載入dump
文件,輸入.ecxr
bash
從輸出結果能夠看出是訪問到無效的地址0x07acf914
了,使用命令!address 07acf914
查看該地址的信息:微信
從輸出結果能夠看出該地址確實是不可訪問的。咱們須要看看0x07acf914
是從哪裏來的,該值來自edi+4
指向的地址所存儲的值,那麼edi
的值是哪裏來的呢?讓咱們看看前幾條彙編指令是什麼,輸入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
查看調用棧: 操作系統
從調用棧可知,ComFriendlyWaitMtaThreadProc()
是在新線程中執行的,經過查看CreateThread()
的原型咱們能夠知道 ComFriendlyWaitMtaThreadProc()
原型應該知足typedef DWORD (__stdcall LPTHREAD_START_ROUTINE)(LPVOID lpThreadParameter);
。插件
綜上可知,ebp+8
確實指向了第一個參數,這個參數指向了一個非法的地址!
我猜想有以下兩種可能:
bug
,傳遞參數的時候就傳的有問題!(可能性過低了,對本身的代碼比較有信心😂)單純從dump
看不出更多的信息了!因而我決定給 ComFriendlyWaitMtaThreadProc()
下斷點,看看是否能找到是誰建立了這個線程! 執行以下命令:
bu uiautomationcore!ComFriendlyWaitMtaThreadProc
g複製代碼 |
斷下來後,使用~*k
查看全部線程的調用棧,通過排查,11號線程
和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
幫助文檔