1. 程式人生 > >如何重建一個損壞的調用堆棧(callstack)

如何重建一個損壞的調用堆棧(callstack)

dds 時間 找不到 搜索 hand tle 簽名 每一個 原理

原文作者:Aaron Ballman
原文時間:2011年07月04日
原文地址:http://blog.aaronballman.com/2011/07/reconstructing-a-corrupted-stack-crawl/

翻譯:magictong

時間:2014年05月29日夜

後記:可惜原始的DUMP文件作者並沒有上傳



在我的日常工作中,我經常閱讀來之微軟WinQual(譯註:https://sysdev.microsoft.com/ http://en.wikipedia.org/wiki/Winqual)的報告。這些報告裏面一般包括著dump文件(譯註:崩潰轉儲文件,我們一般都是叫dump文件。是一種軟件崩潰之後產生的文件,可用於事後調試)。從這些dump文件中面我能夠分析出一些經常使用的軟件裏面究竟出了什麽問題。造成它崩潰了。總而言之,這是一個超贊的系統。我強烈建議各個獨立軟件開發商(原文:ISV)去上面註冊(尤其是這個系統對不論什麽人都是免費的,僅僅要你的可運行文件是正確簽名的)。

近期我拿到了一個堆棧已經被嚴重破壞了的dump文件,我想和大家討論一下怎麽使用Windbg工具來重建它的調用堆棧(callstack)。

在開始之前,讓我們先看看一個原始的調用堆棧是什麽樣子的,在Windbg裏面運行“k”命令就可以。
0:000> k
ChildEBP RetAddr
028b89cc 77c75350 ntdll!KiFastSystemCallRet
028b89d0 77c4b208 ntdll!ZwTerminateProcess+0xc
028b89e0 763e41ec ntdll!RtlExitUserProcess+0x7a

028b89f4 10056386 kernel32!ExitProcess+0x12
WARNING: Stack unwind information not available. Following frames may be wrong.
028b89fc 100565a0 EyeOneIO!I1_SynchronizeWhitebases+0xf0f6
028b8a0c 10054803 EyeOneIO!I1_SynchronizeWhitebases+0xf310
00000000 00000000 EyeOneIO!I1_SynchronizeWhitebases+0xd573

從上面的調用堆棧來看。有幾個特征表明這個堆棧已經被破壞了。

首先,調用堆棧的基址不可能從0x00000000開始。通常情況下,它從main函數的入口地址開始,或者從一個線程的入口地址開始。可是從上面的調用堆棧來看我們沒看看到這個特征。另外,Windbg也發出了“Stack unwind information not available. Following frames may be wrong.”的警告(譯註:這句警告的意思就是說。以下的棧幀可能是錯誤的)。



第一步,既然堆棧已經錯誤了,我們當然須要重建當前運行現成的堆棧。並找到當前現成堆棧的起始位置。

這裏有個簡單的擴展命令能夠查看,使用!teb就可以(譯註:!teb用於查看當前線程運行環境):

0:000> !teb
TEB at 7ffdb000
ExceptionList: 028b8a28
StackBase: 028c0000
StackLimit: 028b6000
SubSystemTib: 00000000
FiberData: 00001e00
ArbitraryUserPointer: 00000000
Self: 7ffdb000
EnvironmentPointer: 00000000
ClientId: 00000a4c . 00000e3c
RpcHandle: 00000000
Tls Storage: 7ffdb02c
PEB Address: 7ffdf000
LastErrorValue: 14007
LastStatusValue: c0150008
Count Owned Locks: 0
HardErrorMode: 0

看上面!teb命令顯示的結果裏面,StackBase和StackLimit告訴了我們當前線程的堆棧在內存中的範圍,因此我們如今能夠轉儲這個範圍內的地址。然後從裏面尋找一些有意義和實用的東西(譯註:就是把內存地址和相應的符號地址相應起來。然後尋找和當前的線程有關的調用堆棧)。

Windbg裏面有個專門的dds命令就是用來做這個事情的。dds命令須要你指定一個起始地址,然後它從給定的起始地址開始轉儲一定範圍內的地址,而且嘗試把每一個地址裏面的內容和符合(symbol)相應起來(譯註:假如能夠相應的話)。dds轉儲的內容包括三列數據,第一列顯示的是順序遞增的地址。第二列是顯示地址裏面的數據,第三列是符號名稱,假設地址裏面的數據能夠被成功解析為一個符號的話,否則第三列就是顯示的空白。

把真實的棧轉儲出來看看(省略了一些無關項):
(譯註:使用命令 dds 028b6000,要顯示更後面的內容能夠在028b6000的後面加上一個偏移之後再對新地址使用 dds 命令)

028b6000 00000000
...
028bf9d8 00000000
028bf9dc 00000000
028bf9e0 79035b7f
028bf9e4 028bfa1c
028bf9e8 6e760b5b i1IO!i1IO::measureOneStrip+0xbb
028bf9ec 42b840fc
...
028bfa18 00000000
028bfa1c 028bfd98
028bfa20 6e763387 i1IO!i1IO::_measureSingleRowScanThreaded+0x1467
028bfa24 42b840fc
...
028bfd94 00000006
028bfd98 028bfe2c
028bfd9c 6e761062 i1IO!i1IO::_advancedMeasureThreaded+0x222
028bfda0 013a8520
028bfda4 79035e2e
...
028bfe28 00000000
028bfe2c 028bfe38
028bfe30 763ed0e9 kernel32!BaseThreadInitThunk+0xe
028bfe34 012118e0
028bfe38 028bfe78
028bfe3c 77c516c3 ntdll!__RtlUserThreadStart+0x23
028bfe40 012118e0
...
028bfe74 00000000
028bfe78 028bfe90
028bfe7c 77c51696 ntdll!_RtlUserThreadStart+0x1b
028bfe80 6e760e40 i1IO!i1IO::_advancedMeasureThreaded
...
028c0000 ?

?

??????



實際上轉儲出來的堆棧比上面列出來的大得多,只是為了簡單起見。我僅僅保留一些相關的部分。

如今要做的第一件事情就是定位到callstack的起始位置。在這個樣例裏面,RtlUserThreadStart看起來非常像是這個起始位置,由於它是線程的起始調用函數。

在找到起始點之後。獲取起始點的前一個堆棧地址A(第一列)。然後在堆棧的內容裏面(第二列)尋找是否有等於A的堆棧B(向低地址尋找,由於堆棧是向低地址增長的)。然後再在堆棧內容裏面尋找是否有等於B的堆棧地址C……,依照這樣的方法不停的搜索內存。直到不能再找到不論什麽東西或者找到空地址。


(譯註:這個就是利用的標準函數棧幀的基本原理。對此處不理解的能夠去了解下標準函數棧幀,一般沒有經過FPO優化的調用函數鏈,能夠通過EBP的值在整個堆棧上面串聯起來,事實上Windbg自己也是這麽找的,而本文討論的恰恰是由於堆棧被破壞之後,Windbg找不到正確的callstack之後。我們怎麽手動恢復的問題)

在我們這個樣例裏面,我們從以下的堆棧開始找:

028bfe78 028bfe90
028bfe7c 77c51696 ntdll!_RtlUserThreadStart+0x1b

搜索地址028bfe78。得到以下的堆棧:

028bfe38 028bfe78
028bfe3c 77c516c3 ntdll!__RtlUserThreadStart+0x23

搜索地址028bfe38。得到以下的堆棧:

028bfe2c 028bfe38
028bfe30 763ed0e9 kernel32!BaseThreadInitThunk+0xe

搜索地址028bfe2c,得到以下的堆棧:

028bfd98 028bfe2c
028bfd9c 6e761062 i1IO!i1IO::_advancedMeasureThreaded+0x222

搜索地址028bfd98,得到以下的堆棧:

028bfa1c 028bfd98
028bfa20 6e763387 i1IO!i1IO::_measureSingleRowScanThreaded+0x1467

搜索地址028bfa1c,得到以下的堆棧:

028bf9e4 028bfa1c
028bf9e8 6e760b5b i1IO!i1IO::measureOneStrip+0xbb

如今,繼續搜索028bf9e4已經不能再在堆棧裏面找到信息了,也就是說我們可能已經找到了終於出問題的函數位置。我們能夠使用Windbg嘗試修復我們的callstack,當然我們須要給它我們上面找到的這些信息。

事實上非常easy,僅僅要上面沒找錯。我們給 k 命令指明一個確定地址,通過 L 參數傳遞進去(譯註:用上面我們最後找到的028bfa1c),那麽Windbg立即就會給我們一個更加友好的callstack信息。

0:000> k L=028bf9e4
ChildEBP RetAddr
028b89cc 77c75350 ntdll!KiFastSystemCallRet
028b89d0 77c4b208 ntdll!ZwTerminateProcess+0xc
028bf9e4 6e760b5b ntdll!RtlExitUserProcess+0x7a
028bfa1c 6e763387 i1IO!i1IO::measureOneStrip+0xbb
028bfd98 6e761062 i1IO!i1IO::_measureSingleRowScanThreaded+0x1467
028bfe2c 763ed0e9 i1IO!i1IO::_advancedMeasureThreaded+0x222
028bfe38 77c516c3 kernel32!BaseThreadInitThunk+0xe
028bfe78 77c51696 ntdll!__RtlUserThreadStart+0x23
028bfe90 00000000 ntdll!_RtlUserThreadStart+0x1b

如今我們看到的callstack是不是更加完整而且合理了?!

沒有了調用棧幀錯誤的警告,而且callstack的調用基址也正常了。

希望上面介紹的這樣的方法能給你的調試工作帶來一些幫助。


如何重建一個損壞的調用堆棧(callstack)