Walking the callstack [译文 part1]
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
77E
614C
7 (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最新版本v 5.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。