您的位置:首页 > 其它

StackWalker 堆栈打印

2018-02-24 11:11 92 查看
主要用途:

1.打印异常堆栈

2.生成dump的时候可以同时生成堆栈日志,不一定调试dump

C++输出函数调用堆栈

转: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

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的函数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: