focus on linux, c/c++, lua

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一起下载(译者注:windbgMS发布的一款调试工具,当你下载并安装的时候,相应的安装目录下会有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输出到文件或是使用其他的输出结构(译者注:比如英雄岛项目中就是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)指针的值。简而言之,SPFP指针的区别在于: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的两个成员AddrPCAddrFrame没有被初始化就作为参数传给StackWalk64的话,那么这个函数在第一次被调用的时候就会失败。

根据这篇文档所述,大多数的程序只需要初始化AddrPCAddrFrame这两个参数,而且这种方式在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


posted on 2010-10-20 10:24 zuhd 阅读(755) 评论(1)  编辑 收藏 引用 所属分类: c/c++

评论

# re: Walking the callstack [译文 part1] 2010-10-28 16:57 ugg boots buy

但是对于CPU来说,只有SP是必不可少的,FP是提供给编译器用的,你可以取消FP的使用开关。<strong><a href="http://www.uggbootskaufende.com/">ugg boots tall</a></strong>  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理