Walking the callstack
作者: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輸出到文件或是使用其他的輸出結構(譯者注:比如英雄島項目中就是ITrace*),你只需要繼承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。