很早以前翻译的一篇文章,今天用到,又温习了一下,从博客转过来和大家分享。

时间:2020-11-28 18:03:42

很早以前翻译的一篇文章,今天用到,又温习了一下,从博客转过来和大家分享。


Hello。


很早以前翻译的一篇文章,今天用到,又温习了一下,从博客转过来和大家分享。


作者: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的函数。