65.9K
CodeProject 正在变化。 阅读更多。
Home

遍历调用堆栈

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (134投票s)

2005年7月27日

BSD

12分钟阅读

viewsIcon

1691190

downloadIcon

19192

本文档描述了(已文档化)遍历任何线程(自身、其他线程和远程线程)调用堆栈的方法。它包含了一个抽象层,因此调用应用程序无需了解其内部细节。

Sample Image - StackWalker.gif

该项目现已在 GitHub 上维护 / https://github.com/JochenKalmbach/StackWalker

...

引言

在某些情况下,您需要显示当前线程的调用堆栈,或者您只对其他线程/进程的调用堆栈感兴趣。因此,我编写了这个项目。

该项目的目标如下:

  • 生成调用堆栈的简单接口
  • 基于 C++,允许重写多个方法
  • 将实现细节(API)隐藏在类的接口之外
  • 支持 x86、x64 和 IA64 架构
  • 默认输出到调试器输出窗口(但可自定义)
  • 支持用户提供的读内存函数
  • 支持最广泛的开发 IDE(VC5-VC8)
  • 最便携的调用堆栈遍历解决方案

背景

要遍历调用堆栈,有一个已文档化的接口:StackWalk64。从 Win9x/W2K 开始,此接口位于 dbghelp.dll 库中(在 NT 上,它位于 imagehlp.dll 中)。但函数名称(StackWalk64)自 W2K 开始发生变化(之前称为 StackWalk(不带 64))!本项目仅支持较新的 Xxx64 函数。如果您需要在旧系统上使用它,可以下载 NT/W9x 的可再发行组件

最新版本的 dbghelp.dll 始终可以通过 Windows 调试工具下载。它还包含 symsrv.dll,可用于使用公共 Microsoft 符号服务器(可用于检索系统文件的调试信息;请参阅下文)。

Using the Code

该类的用法非常简单。例如,如果您想显示当前线程的调用堆栈,只需实例化一个 StackWalk 对象并调用 ShowCallstack 成员。

#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;
}

这将在调试器输出窗口中产生以下输出:

[...] (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

您现在可以双击一行,IDE 将自动跳转到所需的行/文件。

提供自定义输出机制

如果您想将输出定向到文件或使用其他输出机制,只需从 StackWalker 类派生即可。您有两种选择:仅重写 OnOutput 方法或重写每个 OnXxx 函数。第一种解决方案(OnOutput)非常简单,并使用其他 OnXxx 函数的默认实现(在大多数情况下应该足够了)。要同时输出到控制台,您需要执行以下操作:

class MyStackWalker : public StackWalker
{
public:
  MyStackWalker() : StackWalker() {}
protected:
  virtual void OnOutput(LPCSTR szText)
    { printf(szText); StackWalker::OnOutput(szText); }
};

检索详细的调用堆栈信息

如果您想要有关调用堆栈的详细信息(例如加载的模块、地址、错误等),可以重写相应的方法。提供了以下方法:

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);
};

在生成调用堆栈期间会调用这些方法。

各种类型的调用堆栈

在类的构造函数中,您需要指定是要为当前进程还是为另一个进程生成调用堆栈。可用的构造函数如下:

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
    );
};

要执行实际的堆栈遍历,您需要调用以下函数:

class StackWalker
{
public:
  BOOL ShowCallstack(
    HANDLE hThread = GetCurrentThread(),
    CONTEXT *context = NULL,
    PReadProcessMemoryRoutine readMemoryFunction = NULL,
    LPVOID pUserData = NULL
    );
};

显示异常的调用堆栈

使用此 StackWalker,您还可以在异常处理程序中显示调用堆栈。您只需要编写一个执行堆栈遍历的过滤器函数。

// 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()))
{
}

关注点

上下文和调用堆栈

要遍历给定线程的调用堆栈,您至少需要两个事实:

  • 线程的上下文

    上下文用于检索当前的指令指针以及堆栈指针 (SP) 的值,有时还包括帧指针 (FP)。SP 和 FP 之间的区别简而言之:SP 指向堆栈上的最新地址。FP 用于引用函数的参数。另请参阅堆栈指针和帧指针的区别。但对于处理器来说,只有 SP 是必不可少的。FP 仅由编译器使用。您也可以禁用 FP 的使用(参见:/Oy(省略帧指针))。

  • 调用堆栈

    调用堆栈是一个内存区域,其中包含所有调用者的数据/地址。这些数据必须用于检索调用堆栈(顾名思义)。最重要的一点是:此数据区域在堆栈遍历完成之前不得更改!这也是必须将线程置于挂起状态才能检索有效调用堆栈的原因。如果您想遍历当前线程的堆栈,那么在上下文记录中声明的地址之后,您不得更改调用堆栈内存。

初始化 STACKFRAME64 结构

为了成功使用 StackWalk64 遍历调用堆栈,您需要使用有意义的值初始化 STACKFRAME64 结构。在 StackWalk64 的文档中,只有关于此结构的小提示:

  • 如果传递到 StackFrame 参数的 STACKFRAME64 结构中的 AddrPCAddrFrame 成员未初始化,则此函数将失败。

根据此文档,大多数程序仅初始化 AddrPCAddrFrame;这在最新的 dbhhelp.dll(v5.6.3.7)之前一直有效。现在,您还需要初始化 AddrStack。在遇到此问题(以及其他问题)后,我与 dbghelp-team 进行了交谈,并得到了以下答复(2005-08-02;我的评论用斜体标出!):

  • AddrStack 应始终设置为所有平台的堆栈指针值。您当然可以发布 AddrStack 应该被设置。您也欢迎说明 dbghelp 的新版本现在需要此项。
  • 给定一个当前的 dbghelp,您的代码应该:
    1. 始终使用 StackWalk64
    2. 始终将 AddrPC 设置为当前指令指针(x86 上为Eip,x64 上为Rip,IA64 上为StIIP)。
    3. 始终将 AddrStack 设置为当前堆栈指针(x86 上为Esp,x64 上为Rsp,IA64 上为IntSp)。
    4. 当有意义时,将 AddrFrame 设置为当前的帧指针。在 x86 上是 Ebp,在 x64 上您可以使用 Rbp但 VC2005B2 不使用它;而是使用 Rdi),在 IA64 上您可以使用 RsBSP。当 StackWalk64 不需要它进行展开时,它将忽略该值。
    5. AddrBStore 设置为 IA64 的 RsBSP

遍历当前线程的调用堆栈

在 XP 之前的 x86 系统上,没有直接支持的函数来检索当前线程的上下文。推荐的方法是抛出异常并捕获它。现在您将拥有一个有效的上下文记录。捕获当前线程上下文的默认方法是执行一些内联汇编来检索 EIPESPEBP。如果您想使用已文档化的方法,则需要为项目定义 CURRENT_THREAD_VIA_EXCEPTION。但您应该意识到 GET_CURRENT_CONTEXT 是一个宏,它内部使用 __try __except。您的函数必须能够包含这些语句。

从 XP 开始,以及在 x64 和 IA64 系统上,有一个已文档化的函数来检索当前线程的上下文:RtlCaptureContext

要遍历当前线程的堆栈,您只需执行以下操作:

  StackWalker sw;
  sw.ShowCallstack();

遍历同一进程中其他线程的调用堆栈

要遍历同一进程中另一个线程的调用堆栈,您需要挂起目标线程(这样调用堆栈在堆栈遍历期间不会改变)。但您应该意识到,在同一进程中挂起线程可能会导致死锁!(请参阅:为什么您永远不应该调用 Suspend/TerminateThread(第一部分第二部分第三部分))

如果您拥有线程的句柄,可以执行以下操作来检索调用堆栈:

  MyStackWalker sw;
  sw.ShowCallstack(hThread);

有关检索另一个线程调用堆栈的完整示例,请查看演示项目。

遍历其他进程中其他线程的调用堆栈

方法与遍历当前进程调用堆栈的方法几乎相同。您只需要提供 ProcessID 和进程句柄(hProcess)。然后,您还需要挂起线程以执行堆栈遍历。有关检索另一个进程调用堆栈的完整示例,请参见演示项目。

重用 StackWalk 实例

只要您要遍历同一进程的堆栈,重用 StackWalk 实例就没有问题。如果您要执行大量堆栈遍历,建议重用实例。原因很简单:如果您创建一个新实例,则必须为每个实例重新加载符号文件。这非常耗时。此外,不允许从不同线程访问 StackWalk 函数(dbghelp.dll不是线程安全的!)。因此,创建一个以上实例没有意义...

符号搜索路径

默认情况下,会向 dbghelp.dll 提供符号搜索路径(SymBuildPathSymUseSymSrv)。此路径包含以下目录:

  • 可选提供的 szSymPath。如果提供了此参数,则自动设置 SymBuildPath 选项。每个路径都必须用“;”分隔。
  • 当前目录
  • EXE 的目录
  • 环境变量 _NT_SYMBOL_PATH
  • 环境变量 _NT_ALTERNATE_SYMBOL_PATH
  • 环境变量 SYSTEMROOT
  • 环境变量 SYSTEMROOT 加上“\system32
  • 公共 Microsoft 符号服务器:SRV*%SYSTEMDRIVE%\websymbols*http://msdl.microsoft.com/download/symbols

符号服务器

如果您想使用 Microsoft 符号服务器提供的操作系统文件的公共符号,您要么需要 Windows 调试工具(这样 symsrv.dll 和最新的 dbghelp.dll 将自动找到),要么需要重新分发此包中的“dbghelp.dllsmysrv.dll”!

加载模块和符号

为了成功遍历线程的调用堆栈,dbghelp.dll 要求库能够识别模块。因此,您需要通过SymLoadModule64“注册”进程的每个模块。要完成此操作,您需要枚举给定进程的模块。

从 Win9x 和 W2K 开始,可以使用 ToolHelp32 API。您需要创建进程的快照 (CreateToolhelp32Snapshot),然后可以通过Module32FirstModule32Next 枚举模块。通常,ToolHelp 函数位于 kernel32.dll 中,但在 Win9x 上,它位于一个单独的 DLL:tlhelp32.dll 中。因此,我们需要检查这两个 DLL 中的函数。

如果您有 NT4,则 ToolHelp32 API 不可用。但在 NT4 中,您可以使用 PSAPI。要枚举所有模块,您需要调用EnumProcessModules,但您只会获得模块的句柄。要向SymLoadModule64 提供信息,您还需要查询 ModuleBaseAddrSizeOfImage(通过GetModuleInformation)、ModuleBaseName(通过GetModuleBaseName)和 ModuleFileName(Path)(通过GetModuleFileNameEx)。

dbghelp.dll

dbghelp.dll 有几个问题。

  • 第一个是,微软有两个“团队”重新分发 dbghelp.dll。一个团队是操作系统团队,另一个是调试工具团队(我不知道实际名称……)。总的来说,您可以说:随 Windows 调试工具提供的 dbghelp.dll 是最新版本。这两个团队之间的一个问题是 dbghelp.dll 的版本不同。例如,对于 XP-SP1,版本是5.1.2600.1106,日期是2002-08-29。从调试团队重新分发的版本6.0.0017.0 的日期是2002-04-31。因此,至少存在日期上的冲突(更新的版本反而更旧)。而且更难判断哪个版本“更好”(或功能更多)。
  • 从 Me/W2K 开始,system32 中的 dbghelp.dll 文件受到系统文件保护的保护。所以如果您想使用更新的 dbghelp.dll,您需要重新分发 Windows 调试工具中的版本(将其放在与您的 EXE 相同的目录中)。这会在 W2K 上导致问题,如果您想遍历使用 VC7 或更高版本构建的应用程序的调用堆栈。VC7 编译器生成一种新的 PDB 格式(称为 DIA)。这种 PDB 格式无法被操作系统自带的 dbghelp.dll 读取。因此,您将无法获得非常有用的调用堆栈(或至少没有调试信息,如文件名、行号、函数名……)。要解决此问题,您需要重新分发更新的 dbghelp.dll
  • dbghelp.dll 版本6.5.3.7 有一个StackWalk64 函数的错误或至少是文档更改。在文档中,您可以读到:

    如果传递到 StackFrame 参数的 STACKFRAME64 结构中的 AddrPCAddrFrame 成员未初始化,则此函数将失败。

    [ContextRecord] 参数仅在 MachineType 参数不是 IMAGE_FILE_MACHINE_I386 时才需要。

    但这不再是真的了。现在,如果您将 ContextRecord 传递为 NULL,则无法在 x86 系统上检索调用堆栈。在我看来,这是一个重大的文档更改。现在,您要么需要初始化 AddrStack,要么提供一个有效的ContextRecord,其中包含 EIPEBPESP 寄存器!

  • 另请参阅Initializing the STACKFRAME64-structure章节中的注释……

选项

要对行为进行某种修改,您可以选择指定一些选项。以下是可用选项的列表:

  typedef enum StackWalkOptions
  {
    // No addition info will be retrieved
    // (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 above
    RetrieveVerbose = 0xF,

    // Generate a "good" symbol-search-path
    SymBuildPath = 0x10,

    // Also use the public Microsoft-Symbol-Server
    SymUseSymSrv = 0x20,

    // Contains all the above "Sym"-options
    SymAll = 0x30,

    // Contains all options (default)
    OptionsAll = 0x3F
  } StackWalkOptions;

已知问题

  • NT/Win9x:本项目仅支持 StackWalk64 函数。如果您需要在 NT4/Win9x 上使用它,您需要重新分发该平台的 dbghelp.dll
  • 目前仅支持回调中的 ANSI 名称(当然,该项目可以与 UNICODE 编译……)。
  • 要打开远程线程,我使用了“OpenThread”,该函数在 NT4/W9x 上不可用。有关在 NT4/W9x 中执行此操作的示例,请参阅远程库
  • 遍历混合模式调用堆栈(托管/非托管)仅返回非托管函数。

历史

  • 2005-07-27
    • 首次公开发布
    • 支持 x86、x64 和 IA64。
  • 2005-07-28
    • 更改了描述,使其不至于给人留下这是遍历调用堆栈的唯一已文档化方法的印象……
    • ShowCallstack(hThread, ...) 现在接受一个 NULL CONTEXT(这会强制捕获该线程的上下文);它也被简化了(现在所有情况只有一个函数)。
    • 添加了章节:Symbol-Search-PathLoading the modules and symbolsdbghelp.dll
  • 2005-08-01
    • 添加了 VC7.0 项目/解决方案文件。
    • GET_CURRENT_CONTEXT 定义添加了更多注释,关于使用 RtlCaptureContext 函数。
    • 解决了未初始化的 cnt 变量问题。
    • 解决了 GetFileVersionInfoSizeGetFileVersionInfo 的错误定义(在旧 PSDK 版本(VC7 及更早版本)中,第一个参数声明为 LPTSTR 而不是 LPCTSTR)。
    • 将使用的 ContextFlags 参数从 CONTEX_ALL 更改为 CONTEXT_FULL(这也正常工作,并且在旧 PSDK 版本中受支持)。
    • 现在可以在没有安装更新的 Platform-SDK 的情况下使用 VC5/6 进行编译(所有缺失的声明现在都已嵌入)。
    • 添加了 VC6 项目/解决方案文件。
    • ShowCallstack 函数和 PReadProcessMemoryRoutine 声明添加了一个 pUserData 成员(用于传递一些用户定义的数据,这些数据可以在 readMemoryFunction 回调中使用)。
  • 2005-08-02
    • OnSymInit 现在默认也输出 OS 版本。
    • main.cpp 中添加了执行异常调用堆栈遍历的示例(感谢owillebo)。
    • 纠正了关于RtlCaptureContext 的文章。此函数从 XP 开始也可用(感谢 Dan Moulding)。
    • 添加了章节:Initializing the STACKFRAME64-structureDisplaying the callstack of an exception
  • 2005-08-05
    • 删除了 Lint 的一些问题……感谢 Okko Willeboordse!
    • 消除了 VC8 的 /analyze 开关的所有警告。
  • 2005-09-06
    • 修复了 VC5/6 和旧 PSDK 版本的一个小 bug(OSVERSIONINFOEX 不存在)。
  • 2005-11-07
    • 检测到“无限循环调用堆栈”并终止堆栈遍历。
    • 仅在 x64 和 IA64 平台上使用RtlCaptureContext
© . All rights reserved.