轉:http://www.codeproject.com/Articles/11132/Walking-the-callstack
轉:http://bbs.pediy.com/archive/index.php?t-157116.html
很早以前翻譯的一篇文章,今天用到,又溫習了一下,從博客轉過來和大家分享。
作者:Jochen Kalmbach
翻譯:Hefe
原文出處:www.codeproject.com
關鍵字:callstack, StackWalker
一,簡介
有些情況下,我們需要顯示當前線程的callstack,或是顯示其他我們感興趣的進程或線程的callstack,為此,我專門寫了這篇文章闡述如何獲得callstack。
我寫這篇文章的主要目的如下:
1, 提供一些簡單的接口來生成callstack
2, 基於CPP的特性提供一些方法來用於重載
3, 隱藏具體API的實現
4, Callstack信息默認輸出在debug模式窗口(可以自己定制輸出方式)
5, 支持用戶提供的內存只讀函數
6, 編譯器支持VC5-VC8
7, 提供最便利的callstack生成方案
二,背景
目前MS已經提供API(StackWalker64)用來遍歷callstack。從win9x/w2k開始,這個接口就被包含在dbghelp.dll的庫中(在NT上,取而代之的是imagehelp.dll),只是這個接口(StackWalk64)從w2k之后被改名字了,在w2k之前叫StackWalk,沒有尾巴的64。這個工程只支持最新的Xxx64接口,如果你想在比較舊的平台上運行,你可以去下載支持相關的平台dll。
最新版本的dbghelp.dll可以和windbg一起下載(譯者注:windbg是MS發布的一款調試工具,當你下載並安裝的時候,相應的安裝目錄下會有dbghelp.dll文件)。同時也包含了symsrv.dll文件,這個文件主要用來激活MS的公共符號服務(這個服務主要用來獲取系統文件的調試信息)。
三,如何使用代碼
StackWalker這個類的使用非常簡單。比如:如果你想獲得當前線程的callstack,你只需要初始化一個StackWalk的實例,然后調用ShowCallStack即可。(譯者注:一般我們需要繼承StackWalker這個類,然后聲明並初始化這個子類的實例)。
代碼演示1
#include <windows.h>
#include "StackWalker.h"
void Func5() { StackWalker sw; sw.ShowCallstack(); }
void Func4() { Func5(); }
void Func3() { Func4(); }
void Func2() { Func3(); }
void Func1() { Func2(); }
int main()
{
Func1();
return 0;
}
在debug-output窗口生成相應的輸出如下:
[...] (output stripped)
d:\privat\Articles\stackwalker\stackwalker.cpp (736): StackWalker::ShowCallstack
d:\privat\Articles\stackwalker\main.cpp (4): Func5
d:\privat\Articles\stackwalker\main.cpp (5): Func4
d:\privat\Articles\stackwalker\main.cpp (6): Func3
d:\privat\Articles\stackwalker\main.cpp (7): Func2
d:\privat\Articles\stackwalker\main.cpp (8): Func1
d:\privat\Articles\stackwalker\main.cpp (13): main
f:\vs70builds\3077\vc\crtbld\crt\src\crt0.c (259): mainCRTStartup
77E614C7 (kernel32): (filename not available): _BaseProcessStart@4
你現在可以雙擊任意一行,VS會自動的跳轉到你想到的文件並定位到具體行。
定制你自己的輸出結構
如果你想直接把callstack輸出到文件或是使用其他的輸出結構,你只需要繼承StackWalker類即可。你有兩種選擇來實現自己的輸出結構:1,重寫OnOutput方法。2,重寫所有的OnXXX函數。當然從OO的思想來說,第一種方法是推薦的,符合KISS的原則。
演示代碼2
class MyStackWalker : public StackWalker
{
public:
MyStackWalker() : StackWalker() {}
protected:
virtual void OnOutput(LPCSTR szText)
{ printf(szText); StackWalker::OnOutput(szText); }
};
獲得callstack的具體信息
如果你想獲得關於callstack的具體信息(比如已加載的模塊,地址信息,以及錯誤信息),你可以重載下面提供的相應的方法。
演示代碼3
class StackWalker
{
protected:
virtual void OnSymInit(LPCSTR szSearchPath, DWORD symOptions, LPCSTR szUserName);
virtual void OnLoadModule(LPCSTR img, LPCSTR mod, DWORD64 baseAddr, DWORD size,
DWORD result, LPCSTR symType, LPCSTR pdbName, ULONGLONG fileVersion);
virtual void OnCallstackEntry(CallstackEntryType eType, CallstackEntry &entry);
virtual void OnDbgHelpErr(LPCSTR szFuncName, DWORD gle, DWORD64 addr);
};
上述的方法會在callstack的生成過程中被調用。
callstack的各種類別
在StackWalker的構造函數中,如果你想針對具體的進程生成callstack,那你需要傳入具體的進程信息作為參數,比如進程ID和進程句柄,請看下面的兩個構造函數。
演示代碼4
class StackWalker
{
public:
StackWalker(
int options = OptionsAll,
LPCSTR szSymPath = NULL,
DWORD dwProcessId = GetCurrentProcessId(),
HANDLE hProcess = GetCurrentProcess()
);
// Just for other processes with
// default-values for options and symPath
StackWalker(
DWORD dwProcessId,
HANDLE hProcess
);
};
真正遍歷callstack的方法也就是下面的ShowCallstack()
演示代碼5
class StackWalker
{
public:
BOOL ShowCallstack(
HANDLE hThread = GetCurrentThread(),
CONTEXT *context = NULL,
PReadProcessMemoryRoutine readMemoryFunction = NULL,
LPVOID pUserData = NULL
);
};
顯示一個異常的callstack
利用這個StackWalker你同樣可以獲得一個異常句柄的callstack。你只需要寫一個異常過濾器即可。
演示代碼6
// The exception filter function:
LONG WINAPI ExpFilter(EXCEPTION_POINTERS* pExp, DWORD dwExpCode)
{
StackWalker sw;
sw.ShowCallstack(GetCurrentThread(), pExp->ContextRecord);
return EXCEPTION_EXECUTE_HANDLER;
}
// This is how to catch an exception:
__try
{
// do some ugly stuff...
}
__except (ExpFilter(GetExceptionInformation(), GetExceptionCode()))
{
}
四,本文要點
上下文與callstack
遍歷一個線程的callstack,你至少要知道以下兩點:
1, 當前線程的上下文context
線程的上下文主要是用來獲取當前IP指針(Instruction Pointer指令指針)和SP(Stack Pointer)指針的值,有時候也用來獲取FP(Frame Pointer)指針的值。簡而言之,SP和FP指針的區別在於:SP指針指向最近一次的堆棧地址,FP主要用來指向當前函數的地址,你可以參考以下的文檔來了解更多(Difference Between Stack Pointer and Frame Pointer.)。但是對於CPU來說,只有SP是必不可少的,FP是提供給編譯器用的,你可以取消FP的使用開關。
2, Callstack
Callstack其實就是一塊內存區域,它包含了調用者的所有的數據內容和地址信息。這些數據內容必須用來獲取callstack。最重要的是:在完成stack-walking之前,這些數據內容必須保持不變。這也就是為什么在獲取有效callstack的時候,當前線程必須要被掛起的原因。如果你想遍歷當前線程的stack,那么你也就不能改變callstack的指針內容,也就是在上下文中聲明的寄存器指針內容。
初始化STACKFRAME64結構
為了能利用StackWalk64來成功的遍歷callstack,我們必須用有意義的值來初始化STACKFRAME64。在STACKFRAME64的文檔中,有一小段要點描述如下:
如果STACKFRAME64的兩個成員AddrPC和AddrFrame沒有被初始化就作為參數傳給StackWalk64的話,那么這個函數在第一次被調用的時候就會失敗。
根據這篇文檔所述,大多數的程序只需要初始化AddrPC和AddrFrame這兩個參數,而且這種方式在dbghelp.dll最新版本v5.6.3.7發布之前一直都是正確的。但是,現在你除了要初始化這兩個參數之外,還要初始化AddrStack這個參數。在發現一些麻煩和問題后,我和dbghelp開發小組討論了一下,並得到了如下的答案(2005-08-02,我的觀點是斜體文字)
1, 在所有的平台下,AddrStack都要被設置成指向stack pointer(也就是ESP)。你當然可以公布AddsStack應該被設置,甚至你可以說最新版本的dbghelp必須要求這么做。
2, 現在的dbghelp版本,你應該遵循下面的做法:
a). 請使用StackWalk64
b). 請把參數AddrPC設置成指向當前指令指針,分別是EIP(x86),Rip(x64),stIIP(IA64)
c). 請把AddrStack設置成指向當前的SP指針,分別是ESP(x86),RSP(x64),IntSP(IA64)
d). 如果當前的Frame Pointer是有意義的,請把AddrFrame設置成指向當前的Frame Pointer,分別是EBP(x86),RBP(x64)[作者的斜體字部分:當時在VC2005B2的環境下,該寄存器無法使用,取而代之的是Rdi],RsBSP(IA64)。StackWalk64會在沒有必要的情況忽略這個參數的值。
e). 在IA64的平台下請把AddrStore設置成指向RsBSP。
遍歷當前線程的callstack
在x86的系統上(在XP之前),是沒有一個直接的API用來獲得當前線程的上下文的。當時提倡的做法是在捕獲系統異常中來獲得。現在在我們的代碼中,我們實現了有效的獲取上下文的方法。默認的情況下,我們是通過內聯匯編代碼來獲取EIP,ESP和EBP的值。如果你想使用我剛才提到的捕獲異常的方法來實現的話,那你就需要定義一個CURRENT_THREAD_VIA_EXCEPTION這樣的宏。但是我們應該意識到,其實GET_CURRENT_CONTEXT也是一個宏,內部也是使用了捕捉異常的原理。我們的函數都必須要能包含這些宏的聲明。
從XP開始以及在x64與IA64平台上,目前已經有API來獲得當前線程的上下文,就是RtlCaptureContext.。
演示代碼7
StackWalker sw;
sw.ShowCallstack();
在同一個進程內遍歷其他線程的callstack(略)
遍歷另一個進程內的某線程callstack(略)
(譯者注:由於時間原因,上述兩部分的翻譯暫時省略了,內容也比較簡單,只是調用了StackWalker的不同構造函數)
重用StackWalk的實例
重用StackWalk的實例是沒有任何問題的,只要你想在同一個進程內遍歷callstack。如果你重復多次用到callstack的遍歷,我強烈你推薦重用一個實例。原因很簡單:當你創建一個新的實例的時候,symbol文件就要被重新加載一次,這個是非常耗時的。而且多個StackWalk跨線程工作也是不可靠的,因為dbghelp.dll不是線程安全的。綜上,在一個進程中保持只有一個StackWalker實例是最合理的做法。
Symbol的搜索路徑
通常情況下,Symbol的搜索路徑(SymBuildPath 和 SymUseSymSrv)主要是用來搜索這個文件dbghelp.dll。這個路徑通常包含一下目錄:
1, szSymPath是否提供是可選擇的,如果提供的話,那么SymBuildPath會自動生成。在szSymPath中每個路徑之間要用分號“;”來分開。
2, 當前工作目錄
3, 可執行文件的目錄,如exel
4, _NT_SYMBOL_PATH的環境變量
5, _NT_ALTERNATE_SYMBOL_PATH的環境變量
6, SYSTEMROOT的環境變量
7, SYSTEMROOT\system32的環境變量
8, MS符號服務器SRV*%SYSTEMDRIVE%\websymbols*http://msdl.microsoft.com/download/symbols
符號服務器
如果你想使用MS的公共信號服務器,你可以選擇安裝windbg(這樣symsrv.dll和最新dbghelp.dll會被自動查詢到),你也可以選擇從網絡傳輸中獲取這些最新符號,不過推薦前者,這樣就不會因為網絡故障而出現加載符號失敗。
加載程式和符號
為了能成功遍歷線程的callstack,dbghelp.dll要獲得所有被加載模塊的信息。所有你需要通過SymLoadModule64這個API來注冊所有被加載的模塊,在注冊之前,第一步是枚舉出所有的模塊。
在win9x之后。利用ToolHelp32_API可以實現這個需求,需要用的API有,CreateToolhelp32SnapShot,Module32First和Module32Next。通常情況下這些API包含在kernel32.dll,但是在win9x的系統上,這些API包含在tlhelp32.dll中,所以在代碼中要做分支判斷。
如果你是在NT4上干活的話,那么使用ToolHelp32-API只是一個夢想。但是你可以使用PSAPI來取而代之。你需要使用到一下API:EnumProcessModules, GetModuleInformation,GetModuleBaseName, GetModuleFileNameEx。
Dbghelp.dll
下面就來隨便啰嗦幾句dbghelp.dll
1, 首先,在MS,有兩個team在負責開發dbghelp.dll,一個是os-team,另一個是debug-team。通常情況下,你會以為windbg提供的dbghelp.dll是最新的版本。但是有個問題就是這兩個小組發布的dbghelp.dll的版本是不同的。舉個例子來說:xp-sp1的dbghelp.dll版本是5.1.2600.1106(2002-08-29)。但是debug-team發布的6.0.0017.0版本時間卻是2002-04-31。(譯者注:寒,MS也會犯這種錯誤)。這樣版本的發布就會有沖突,所以很難通過版本好來確定哪個更好,更有效。
2, 從Winme/W2k開始,system32目錄下面的dbghelp.dll文件是受保護的。所以如果你想成功遍歷callstack,,最好去下載個最新版本的dbghelp.dll放在你的exe目錄下面。否則在W2k上會導致一個問題,就是,如果你想遍歷一個用VC7+編譯的工程就會出錯。因為VC7+的編譯器生成的PDB格式文件不能被dbghelp.dll識別,這樣你就不會得到有效的callstack信息。總之,保險起見,不要使用 OS的dbghelp.dll,去下載最新的dbghelp.dll來使用。(譯者注:我在論壇中看到很多人無法正確遍歷棧,都是dbghelp.dll的版本較老造成的。)
3, V6.5.3.7版本的dbghelp.dll有個bug,或是說StackWalk64函數的文檔發生了變化。文檔中描述:
如果STACKFRAME64的兩個成員AddrPC和AddrFrame沒有被初始化就作為參數傳給StackWalk64的話,那么這個函數在第一次被調用的時候就會失敗。而且,只有當參數MachineType不是IAMGE_FILE_MACHINE_I386的時候,參數ContectRecord才要求被初始化。
但是這個是錯誤的。在x86上,當你給ContextRecord傳NULL的時候,並不能獲得到callstack。以我的觀點,這是比較大的文檔改動。現在你既可以通過初始話AddrStack,也可以通過包含EIP,EBP,ESP的ContextRecord來成功獲取callstack。
Stackwalker的操作開關
你可以按照自己的需求來定義操作開關
演示代碼7
typedef enum StackWalkOptions
{
// No addition info will be retrived
// (only the address is available)
RetrieveNone = 0,
// Try to get the symbol-name
RetrieveSymbol = 1,
// Try to get the line for this symbol
RetrieveLine = 2,
// Try to retrieve the module-infos
RetrieveModuleInfo = 4,
// Also retrieve the version for the DLL/EXE
RetrieveFileVersion = 8,
// Contains all the abouve
RetrieveVerbose = 0xF,
// Generate a "good" symbol-search-path
SymBuildPath = 0x10,
// Also use the public Microsoft-Symbol-Server
SymUseSymSrv = 0x20,
// Contains all the abouve "Sym"-options
SymAll = 0x30,
// Contains all options (default)
OptionsAll = 0x3F
} StackWalkOptions;
五,使用須知
1, NT/Win9x:這個工程只支持StackWalk64這個API。如果你想在NT4/win9x上使用的話,你需要重新配置dbghelp.dll。
2, 當前工程在遍歷過程中只支持ANSI名稱符,(譯者注:C++中沒看到過有人用中文命名的函數名,但java大有人在),當然,如果你也可以選擇以unicode的編碼方式來編譯工程來解決中文函數名的問題。
3, 在NT4/win9x的平台上,用“OpenThread”來打開遠程線程是不支持的,如果你想實現,請參考Remote Library。
4, 遍歷混合模式的callstack(包含managed和unmanaged)並不會返回unmanaged的函數。