怎樣重建一個損壞的調用堆棧(callstack)

原文作者: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,當然我們需要給它我們上面找到的這些信息。其實很簡單,只要上面沒找錯,我們給 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的調用基址也正常了。

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


發佈了148 篇原創文章 · 獲贊 261 · 訪問量 187萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章