緣起
最近,接連在項目中遇到了兩個界面無響應的問題。都只發生在客戶特定機器上,不方便直接調試,只能抓取 dump
進行事後分析了。
抓取 dump
遠程連上可以重現問題的機器,使用 process explorer
初步觀察卡死的進程,發現 CPU
佔用率很低,經過一段時間的觀察,基本確定是一個死鎖問題。在卡死的進程上右鍵,保存完整轉儲,壓縮,發回本地進行分析。
使用 windbg 進行分析
雙擊抓取的 dump
文件,因爲之前已經執行過 windbg.exe -IA
,所以默認會通過 windbg
打開 dump
文件。先使用 ~*kvn
粗略瀏覽一下每個線程的調用棧(因爲比較長,這裏就不截圖了)。經過觀察,很快鎖定了兩個值得進一步查看的線程:一個是主線程(界面線程),因爲是界面無響應,肯定要關注界面線程。另外一個是 7
號工作線程。分別看一下這兩個線程的調用棧。
主線程的調用棧如下圖所示:
注意上圖紅色高亮部分,主線程通過 SleepConditionVariableCS()
進入等待。
看完主線程,再看 7
號工作線程的調用棧,如下圖所示:
7
號線程對 SendMessage()
的調用非常值得懷疑。
猜測整個流程是這樣的:主線程不知由於什麼原因進入等待狀態,而工作線程由於各種各樣的原因也進入了等待狀態。其中 7
號線程最明顯,因爲它正在發消息,而主線程此時是無論如何也不會響應這個消息的。
於是,典型的死鎖再一次發生了。
加載 AssemblyDesign_Tools.dll
的符號後,查看對應的源碼,消除對 SendMessage()
的調用,問題解決!so easy!
說明:工作線程中並沒有直接調用
SendMessage()
,而是調用了操作界面的相關API
,間接調用了SendMessage()
給界面線程發消息。
死鎖的問題解決了,但是爲什麼向主線程發個消息就死鎖了呢?秉着打破砂鍋問到底的原則,我又開始折騰了。下面的內容適合喜歡調試逆向的極客閱讀。
深入調查
最開始的思路是:查看主線程在等待的條件變量,然後再調查哪個工作線程會喚醒這個條件變量。奈何 64
位下,前四個參數通過寄存器 RCX, RDX, r8, r9
進行傳遞,如果這些寄存器沒有在棧上存儲一份的話,很難查看具體的值。折騰一番後,確實沒找到有用的信息,而且就算找到了,也很難找出是哪個線程會執行喚醒操作。
這個死鎖問題不像關鍵段死鎖解決起來那麼直接。不能直接通過命令(!cs -l
),或者查看調用棧就能直接理出頭緒。看來只能硬着頭皮逆向分析相關代碼了。
0
號線程和 7
號線程最值得懷疑,其它線程基本可以排除。先看看主線程爲什麼會等待吧。
主線程邏輯
找到調用 BentleyG!Bentley::BeConditionVariable::WaitOnCondition()
的地方,也就是 5
號棧幀。
在 IDA
中打開 MobileDgn.dll
,並找到這個函數,然後按下神奇的 F5
:
sub_7FEDAC749A0
,傳遞的參數是
v5
。
v5
偏移
88
的位置保存了
BeConditonVariable
類型的變量,也就是
WaitOnCondition()
所等待的變量。 猜測:
sub_7FEDAC749A0
內部會喚醒這個
BeConditionVariable
, 如果
sub_7FEDAC749A0
被順利執行,那麼主線程的等待自然就結束了。
先看看 sub_7FEDAC749A0
的反彙編代碼,當然是直接看 F5
後的僞代碼了。
sub_7FEDAC749A0
內部調用了
Bentley::BeConditionVariable::Wake((CMFCRibbonInfo::XElementButtonUndo *)((char *)v1 + 88), 1);
。
從函數名就可以猜到是用來喚醒 BeConditionVariable
的。
如此看來,sub_7FEDAC749A0
很有可能還沒有被執行,工作線程就掛起了。一起來看看 SendToWorkThread
是怎麼把任務發送到工作線程的。
SendToWorkThread()
會先判斷是否在工作線程運行,如果是則直接執行對應的函數,否則就根據參數生成一個
RpcMessage
,然後發送這個新生成的
RpcMessage
到工作線程的任務隊列中。
再來看一下生成 RpcMessage
的函數,我把這個函數命名爲 MakeMobileDgnRPCMessage()
。
HandlePaint()
傳過來的函數地址是
0x7FEDAC749A0
,保存在了
rpcMsg
偏移
128
的位置。
MobileDgnRPCGenericMessage
類的虛表地址是
000007FEDAD19658
。
繼續追蹤 SendAsynToWorkThread()
,如下圖:
HandleRpcMessage()
(這個名字是我命名的,不是
IDA
識別的),傳給它的第一個參數是一個全局變量(姑且命名爲
g_taskQueueManager
),第二個參數是要發送的
rpcMsg
,第三個參數
v3
的值是
2
,第四個參數是
1
。
這個函數比較長,我只逆了個大概,大體思路是先檢查是否存在,不存在則插入。如果隊列已經滿了,需要等待工作線程從隊列中取走一些任務才返回。
至此,基本理清了主線程相關的邏輯。 大體是這樣的: 主線程在處理HandlePaint()
的時候,先發送一個任務給工作線程,(通過
SendToWorkThread()
發送到工作線程的任務隊列中),然後通過
BeConditionVariable::WaitOnCondition()
等待這個任務結束。
看完界面線程,再來再看 7
號工作線程的相關邏輯。
工作線程邏輯
7
號工作線程,只需要關心 9
號棧幀對應的函數。
_RunThread+0x1a3c
,這個偏移有點大。 由於缺少符號,這裏很有可能只是以
_RunThread
作爲參照得到的一個偏移,實際對應的是另外一個函數的代碼。 使用
ub
向前查看反彙編,很快定位到正確的函數首地址。
從上圖可知,
9
號棧幀對應的函數起始地址是
000007fe dac5fee0
。 怎麼在
IDA
中找到這個函數呢? 如果知道這個函數相對於其所在模塊的偏移,就可以算出在
IDA
中的地址了。 該怎麼獲取這個地址對應的模塊基址呢? 在
windbg
中執行
lma address
就可以知道一個地址對應的模塊信息了。
得到模塊基址(
0x000007fe dac10000
)後,可以很簡單的計算出偏移量爲
0x4fee0
。 有了這些信息就可以在
IDA
中找到這個函數了。
小貼士:也可以在
IDA
中調整模塊基址,使其與windbg
保持一致。這樣就不用根據偏移在IDA
中手動計算地址了。
得到要查看的函數地址後(我在 IDA
中執行了 Rebase program
,所以是 000007fe dac10000
),在 IDA
中直接按快捷鍵 g
,輸入地址後即可跳轉到輸入的地址處。
注意:如果在
IDA
中以16
進制輸入地址,請加上0x
前綴,而且不要帶重音連接符。
再次按下神奇的 F5
:
驗證猜想
好了,花費了這麼多精力終於理清了主線程和工作線程的交互邏輯。目的只有一個,就是爲了更好的驗證之前的猜想:工作線程還沒有來得及執行主線程過來的任務就掛起了。
如果猜測是正確的,那麼工作線程的任務隊列中應該還保留着這個未執行的任務。接下來的任務就是來找到這個未執行的任務。
通過上面對主線程和工作線程的分析,工作線程的任務隊列中應該有類型爲 MobileDgnRPCGenericMessage
的對象,並且該對象偏移 128
的位置的值爲 0x7FEDAC749A0
。根據這兩條關鍵信息在內存中搜尋一下符合條件的記錄。
在 windbg
中輸入命令 s -q 0 L?fffffffffffffff 000007FEDAD19658
,根據虛表地址搜尋 MobileDgnRPCGenericMessage
類型的對象。找到了兩條符合條件的記錄。
再輸入 s -q 0 L?fffffffffffffff 7FEDAC749A0
根據 sub_7FEDAC749A0
的地址搜尋包含這個地址的對象。找到四條符合條件的記錄。
我們關心的是這兩次搜尋結果相差 128
的記錄(因爲根據之前的分析,workProc
存儲在偏移爲 128
的位置)。經過肉眼觀察及在 windbg
中計算(? 00000000300b4ea0 - 00000000300b4f20
),得到了一個符合條件的對象的地址 00000000 300b4ea0
。整個過程如下圖:
g_taskQueueManager
管理,該變量是一個指針,指針所在的地址是
000007FEDADAA380
,指針的值是
00000000024d9fa0
。
根據之前的分析猜測,偏移爲 8
的位置記錄了任務隊列的開始位置,偏移 16
的位置記錄了任務隊列的結束位置,偏移 24
的位置記錄了任務隊列緩衝區結尾的位置,這個任務隊列很有可能是通過 vector
管理的。在 windbg
中查看 g_taskQueueManager
的內容。
dq 0000000037e33ce0
,然後與
s -q 0 L?fffffffffffffff 00000000300b4ea0
得到的搜索記錄對比,發現有一條是吻合的。
看來,主線程發送給工作線程的任務確實還沒有被執行,工作線程就掛起了。
總結
這個偶發的掛起 bug
終於算是解決了。整個過程多虧了強大的 IDA
的強力支持。使整個分析過程簡單了 N
倍。最後,對整個分析過程中用到的技術點做一個簡單的總結:
IDA
的F5
真香。可以在
IDA
中通過Rebase program
調整模塊基址。在
IDA
中按g
可以跳轉到輸入的地址。在
IDA
中的地址需要有0x
前綴,不要包含windbg
中64
位地址的地址連接符。可以在
windbg
中通過lm a address
得到一個地址對應的模塊信息。在
windbg
中可以通過ub
進行反向反彙編。在
windbg
中可以根據虛表地址搜尋對應類型的變量在內存中的位置。在工作線程調用界面相關的
API
時,是通過給界面線程發消息的方式實現的。
參考資料
SleepConditionVariableCS msdn[1]
References:
[1]
SleepConditionVariableCS msdn: https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleepconditionvariablecs
感謝你的分享,點贊和在看
歡迎留言交流!