Walking the callstack
作者:Jochen Kalmbach
翻譯:Hefe
原文出處:www.codeproject.com
關(guān)鍵字:callstack, StackWalker
簡介
有些情況下,我們需要顯示當(dāng)前線程的callstack,或是顯示其他我們感興趣的進(jìn)程或線程的callstack,為此,我專門寫了這篇文章闡述如何獲得callstack。
我寫這篇文章的主要目的如下:
1, 提供一些簡單的接口來生成callstack
2, 基于CPP的特性提供一些方法來用于重載
3, 隱藏具體API的實(shí)現(xiàn)
4, Callstack信息默認(rèn)輸出在debug模式窗口(可以自己定制輸出方式)
5, 支持用戶提供的內(nèi)存只讀函數(shù)
6, 編譯器支持VC5-VC8
7, 提供最便利的callstack生成方案
背景
目前MS已經(jīng)提供API(StackWalker64)用來遍歷callstack。從win9x/w2k開始,這個(gè)接口就被包含在dbghelp.dll的庫中(在NT上,取而代之的是imagehelp.dll),只是這個(gè)接口(StackWalk64)從w2k之后被改名字了,在w2k之前叫StackWalk,沒有尾巴的64。這個(gè)工程只支持最新的Xxx64接口,如果你想在比較舊的平臺(tái)上運(yùn)行,你可以去下載支持相關(guān)的平臺(tái)dll。
最新版本的dbghelp.dll可以和windbg一起下載(譯者注:windbg是MS發(fā)布的一款調(diào)試工具,當(dāng)你下載并安裝的時(shí)候,相應(yīng)的安裝目錄下會(huì)有dbghelp.dll文件)。同時(shí)也包含了symsrv.dll文件,這個(gè)文件主要用來激活MS的公共符號(hào)服務(wù)(這個(gè)服務(wù)主要用來獲取系統(tǒng)文件的調(diào)試信息)。
如何使用代碼
StackWalker這個(gè)類的使用非常簡單。比如:如果你想獲得當(dāng)前線程的callstack,你只需要初始化一個(gè)StackWalk的實(shí)例,然后調(diào)用ShowCallStack即可。(譯者注:一般我們需要繼承StackWalker這個(gè)類,然后聲明并初始化這個(gè)子類的實(shí)例)。
代碼演示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窗口生成相應(yīng)的輸出如下:
[...] (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
你現(xiàn)在可以雙擊任意一行,VS會(huì)自動(dòng)的跳轉(zhuǎn)到你想到的文件并定位到具體行。
定制你自己的輸出結(jié)構(gòu)
如果你想直接把callstack輸出到文件或是使用其他的輸出結(jié)構(gòu)(譯者注:比如英雄島項(xiàng)目中就是ITrace*),你只需要繼承StackWalker類即可。你有兩種選擇來實(shí)現(xiàn)自己的輸出結(jié)構(gòu):1,重寫OnOutput方法。2,重寫所有的OnXXX函數(shù)。當(dāng)然從OO的思想來說,第一種方法是推薦的,符合KISS的原則。
演示代碼2
class MyStackWalker : public StackWalker
{
public:
MyStackWalker() : StackWalker() {}
protected:
virtual void OnOutput(LPCSTR szText)
{ printf(szText); StackWalker::OnOutput(szText); }
};
獲得callstack的具體信息
如果你想獲得關(guān)于callstack的具體信息(比如已加載的模塊,地址信息,以及錯(cuò)誤信息),你可以重載下面提供的相應(yīng)的方法。
演示代碼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);
};
上述的方法會(huì)在callstack的生成過程中被調(diào)用。
callstack的各種類別
在StackWalker的構(gòu)造函數(shù)中,如果你想針對(duì)具體的進(jìn)程生成callstack,那你需要傳入具體的進(jìn)程信息作為參數(shù),比如進(jìn)程ID和進(jìn)程句柄,請(qǐng)看下面的兩個(gè)構(gòu)造函數(shù)。
演示代碼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
);
};
顯示一個(gè)異常的callstack
利用這個(gè)StackWalker你同樣可以獲得一個(gè)異常句柄的callstack。你只需要寫一個(gè)異常過濾器即可。
演示代碼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()))
{
}
本文要點(diǎn)
上下文與callstack
遍歷一個(gè)線程的callstack,你至少要知道以下兩點(diǎn):
1, 當(dāng)前線程的上下文context
線程的上下文主要是用來獲取當(dāng)前IP指針(Instruction Pointer指令指針)和SP(Stack Pointer)指針的值,有時(shí)候也用來獲取FP(Frame Pointer)指針的值。簡而言之,SP和FP指針的區(qū)別在于:SP指針指向最近一次的堆棧地址,FP主要用來指向當(dāng)前函數(shù)的地址,你可以參考以下的文檔來了解更多(Difference Between Stack Pointer and Frame Pointer.)。但是對(duì)于CPU來說,只有SP是必不可少的,FP是提供給編譯器用的,你可以取消FP的使用開關(guān)。
2, Callstack
Callstack其實(shí)就是一塊內(nèi)存區(qū)域,它包含了調(diào)用者的所有的數(shù)據(jù)內(nèi)容和地址信息。這些數(shù)據(jù)內(nèi)容必須用來獲取callstack(譯者注:感覺這句話有點(diǎn)多余了,為了尊重作者,我還是保留了)。最重要的是:在完成stack-walking之前,這些數(shù)據(jù)內(nèi)容必須保持不變。這也就是為什么在獲取有效callstack的時(shí)候,當(dāng)前線程必須要被掛起的原因。如果你想遍歷當(dāng)前線程的stack,那么你也就不能改變callstack的指針內(nèi)容,也就是在上下文中聲明的寄存器指針內(nèi)容。
初始化STACKFRAME64結(jié)構(gòu)
為了能利用StackWalk64來成功的遍歷callstack,我們必須用有意義的值來初始化STACKFRAME64。在STACKFRAME64的文檔中,有一小段要點(diǎn)描述如下:
如果STACKFRAME64的兩個(gè)成員AddrPC和AddrFrame沒有被初始化就作為參數(shù)傳給StackWalk64的話,那么這個(gè)函數(shù)在第一次被調(diào)用的時(shí)候就會(huì)失敗。
根據(jù)這篇文檔所述,大多數(shù)的程序只需要初始化AddrPC和AddrFrame這兩個(gè)參數(shù),而且這種方式在dbghelp.dll最新版本v5.6.3.7發(fā)布之前一直都是正確的。但是,現(xiàn)在你除了要初始化這兩個(gè)參數(shù)之外,還要初始化AddrStack這個(gè)參數(shù)。在發(fā)現(xiàn)一些麻煩和問題后,我和dbghelp開發(fā)小組討論了一下,并得到了如下的答案(2005-08-02,我的觀點(diǎn)是斜體文字)
1, 在所有的平臺(tái)下,AddrStack都要被設(shè)置成指向stack pointer(也就是ESP)。你當(dāng)然可以公布AddsStack應(yīng)該被設(shè)置,甚至你可以說最新版本的dbghelp必須要求這么做。
2, 現(xiàn)在的dbghelp版本,你應(yīng)該遵循下面的做法:
a). 請(qǐng)使用StackWalk64
b). 請(qǐng)把參數(shù)AddrPC設(shè)置成指向當(dāng)前指令指針,分別是EIP(x86),Rip(x64),stIIP(IA64)
c). 請(qǐng)把AddrStack設(shè)置成指向當(dāng)前的SP指針,分別是ESP(x86),RSP(x64),IntSP(IA64)
d). 如果當(dāng)前的Frame Pointer是有意義的,請(qǐng)把AddrFrame設(shè)置成指向當(dāng)前的Frame Pointer,分別是EBP(x86),RBP(x64)[作者的斜體字部分:當(dāng)時(shí)在VC2005B2的環(huán)境下,該寄存器無法使用,取而代之的是Rdi],RsBSP(IA64)。StackWalk64會(huì)在沒有必要的情況忽略這個(gè)參數(shù)的值。
e). 在IA64的平臺(tái)下請(qǐng)把AddrStore設(shè)置成指向RsBSP。