[原]windbg调试系列——崩溃在ComFriendlyWaitMtaThreadProc
前言
這是幾年前在項目中遇到的一個崩潰問題,崩潰在了ComFriendlyWaitMtaThreadProc()里。沒有源碼,耗費了我很大精力,最終通過反匯編并結合原代碼才最終搞清楚了事情的來龍去脈。本文的分析是基于真實項目進行的,中間略去了很多反匯編的分析工作。文末有我整理的測試代碼,大家可以實際體驗一把TerminateThread()的殺傷力。
背景介紹
大概情況是這樣的:程序啟動的時候,會通過LoadLibrary()加載插件模塊。其中的UIA模塊會開啟一個工作線程,工作線程會安裝UIA相關的鉤子來監聽UIA事件,程序在退出的時候會調用每個插件模塊的導出函數做清理工作,然后調用FreeLibrary()釋放這個插件模塊。UIA模塊的清理函數會通知工作線程退出,工作線程收到退出命令后會卸載相關鉤子。清理函數會等待工作線程一段時間,等待超時就通過TerminateThread()強制殺死工作線程。程序退出時偶爾會崩潰在ComFriendlyWaitMtaThreadProc()中。背景介紹完畢,下面開始分析dump文件。
問題分析
使用windbg載入dump文件,輸入.ecxr
.ecxr從輸出結果可以看出是訪問到無效的地址0x07acf914了,使用命令!address 07acf914查看該地址的信息:
!address 07acf914從輸出結果可以看出該地址確實是不可訪問的。我們需要看看0x07acf914 是從哪里來的,該值來自edi+4指向的地址所存儲的值,那么edi的值是哪里來的呢?讓我們看看前幾條匯編指令是什么,輸入ub 7303f614 L10
ub?7303f614 L10說明: 7303f614這個地址是我通過7303f611+3算來的(3是地址7303f611對應的指令長度),這樣就可以在輸出結果中看到導致崩潰的這條指令啦。當然這里輸入ub 7303f611也沒關系(我們關心的是edi的值是哪里來的),只不過我們看不到7303f611對應的指令了。
我們發現edi的值來自ebp+8對應的地址內容。研究過反匯編的小伙伴兒應該對ebp+n比較敏感,有木有?在windows下,32位進程中,ebp+8指向了調用約定為__stdcall的函數的第一個參數。這里的ebp+8是否指向第一個參數,我們需要通過ComFriendlyWaitMtaThreadProc()的調用約定來判斷。
輸入k查看調用棧:
k從調用棧可知,ComFriendlyWaitMtaThreadProc()是在新線程中執行的,通過查看CreateThread()的原型我們可以知道 ComFriendlyWaitMtaThreadProc() 原型應該滿足typedef DWORD (__stdcall LPTHREAD_START_ROUTINE)(LPVOID lpThreadParameter); 。
綜上可知,ebp+8確實指向了第一個參數,這個參數指向了一個非法的地址!
我猜測有如下兩種可能:
調用函數傳遞了一個合法地址,由于某種原因這個地址無效了。(最后證明,我們的代碼里傳遞了一個棧上的局部變量,但是調用線程掛掉了,棧對應的內存無效了!)
代碼中存在bug,傳遞參數的時候就傳的有問題!(可能性太低了,對自己的代碼比較有信心????)
追本溯源
單純從dump看不出更多的信息了!于是我決定在?ComFriendlyWaitMtaThreadProc()中設置斷點,看看是否能找到是誰創建了這個線程! 執行如下命令:
bu uiautomationcore!ComFriendlyWaitMtaThreadProcg斷下來后,使用~*k查看所有線程的調用棧,經過排查,11號線程和18號線程最值得懷疑。
thread-11thread-1818號線程是出問題的線程。
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幫助文檔
《格蠹匯編》
感謝你能堅持看到這里,相信你已經對解決這個問題胸有成竹了。復制測試代碼到本地,親手操練一番吧!
d
猜你喜歡:
[原]調試PInvoke導致的內存破壞
:
歡迎留言交流!
總結
以上是生活随笔為你收集整理的[原]windbg调试系列——崩溃在ComFriendlyWaitMtaThreadProc的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [原]排错实战——通过对比分析sysin
- 下一篇: 开源WPF控件库MaterialDesi