Please choose 'View Source' in your browser to view the HTML, or File | Save to save this file to your hard drive for editing.


Sample Image - StackWalker.gif

Introduction

In some cases you need to display the callstack of the current thread or your are just interested in the callstack of other threads / processes. Therefore I wrote this project.

The goal for this project was the following:

Background

To walk the callstack there is a documented interface: StackWalk64. Starting with Win9x/W2K, this interface is in the dbghelp.dll library (on NT, it is in imagehlp.dll). But the function name (StackWalk64) has changed starting with W2K (before it was called StackWalk (without the 64)! This project only supports the newer Xxx64-funtions. If you need to use it on older systems, you can download the redistributable for NT/W9x.

The latest dbghelp.dll can always be downloaded with the Debugging Tools for Windows. This also contains the symsrv.dll which enables the use of the public Microsoft symbols-server (can be used to retrieve debugging information for system-files; see below).

Using the code

The usage of the class is very simple. For example if you want to display the callstack of the current thread, just instantiate a StackWalk object and call the ShowCallstack member:

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

This produces the following output in the debugger-output window:

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

You can now double-click on a line and the IDE will automatically jump to the desired file/line.

Providing an own output-mechanism

If you want to direct the output in a file or want to use some other output-mechanism, you simply need to derive from the StackWalker class. You have two options to do this: only overwrite the OnOutput method or overwrite each OnXxx-function. The first solution (OnOutput) is very easy and uses the default-implementation of the other OnXxx-functions (which should be enough for most of the cases). To output also the the console, you need to do the following:

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

Retrieving detailed callstack info

If you want detailed info about the callstack (like loaded-modules, addresses, errors, ...) you can overwrite the corresponding methods. The following methods are provided:

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

These methods are called during the generation of the callstack.

Various kinds of callstacks

In the constructor of the class, you need to specify if you want to generate callstacks for the current process or for another process. The following constructors are available:

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

To do the actual stack-walking you need to call the following functions:

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

Dislaying the callstack of an exception

With this StackWalker you can also display the callstack inside an exception handler. You only need to write an filter-function which does the stack-walking:

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

Points of Interest

Context and callstack

To walk the callstack of a given thread, you need at least two facts:

Initializing the STACKFRAME64-structure

To successfully walk the callstack with StackWalk64 you need to initialize the STACKFRAME64-structure with meaningfull values. In the documentation to StackWalk64 there is only a small note about this structure:

According to this documentation, most programs only initialize AddrPC and AddrFrame; and this had worked until the newest dbhhelp.dll (v5.6.3.7). Now you also need to initialize AddrStack. After having some trouble with this (and other problems) I talked to the dbghelp-team and got the following answer (2005-08-02; my own comments are written italic!):

Walking the callstack of the current thread

On x86 systems (prior to XP), there is no direct supported function to retrieve the context of the current thread. The recommended way is to throw an exception and catch it. Now you will have a valid context-record. The default way of capturing the context of the current thread is by doing some inline-assembler to retrieve EIP, ESP and EBP. If you want to use the documented way, then you need to define CURRENT_THREAD_VIA_EXCEPTION for the project. But you should be aware of the fact, that GET_CURRENT_CONTEXT is a macro which internally uses __try __except. Your function must be able to contain these statements.

Starting with XP and on x64 and IA64 systems there is a documented function to retrieve the context of the current thread: RtlCaptureContext.

To do a stack-walking of the current thread you simply need to do:

  StackWalker sw;
  sw.ShowCallstack();

Walking the callstack of other threads in the same process

To walk the callstack of another thread inside the same process, you need to suspend the target thread (so the callstack will not change during the stack-walking). But you should be aware that suspending a thread in the same process might lead to dead-locks! (See: Why you never should call Suspend/TerminateThread (Part I, Part II, Part III.)

If you have the handle to the thread, you can do the following to retrieve the callstack:

  MyStackWalker sw;
  sw.ShowCallstack(hThread);

For a complete sample to retrieve the callstack of another thread you can take a look into the demo-project.

Walking the callstack of other threads in other processes

The approach is almost the same as for walking the callstack for the current process. You only need to provide the ProcessID and a handle to the process (hProcess). Then you also need to suspend the thread to do the stack-walking. A complete sample to retrieve the callstack of another process is in the demo-project.

Reusing the StackWalk instance

It is no problem to reuse the StackWalk instance, as long as you want to do the stack-walking for the same process. If you want to do a lot of stack-walking it is recommended to reuse the instance. The reason is simple: if you create a new instance, then the symbol-files must be re-loaded for each instance. And this is really time-consuming. Also it is not allowed to access the StackWalk functions from different threads (the dbghelp.dll is not thread-safe!). Therefore it makes no sense to create more than one instance...

Symbol-Search-Path

By default (SymBuildPath and SymUseSymSrv, a symbol-search path is provided to the dbghelp.dll. This path contains the following directories:

Symbol-Server

If you want to use the public symbols for the OS-files from the Microsoft-Symbol-Server, you either need the Debugging Tools for Windows (then symsrv.dll and the latest dbghelp.dll will be found automatically) or you need to redistribute "dbghelp.dll" and "smysrv.dll" from this package!

Loading the modules and symbols

To succesfully walk the callstack of a thread, dbghelp.dll requires that the modules are known by the library. Therefor you need to "register" each module of the process via SymLoadModule64. To accomplish this you need to enumerate the modules of the given process.

Starting with Win9x and W2k it is possible to use the ToolHelp32-API. You need to make a snapshot (CreateToolhelp32Snapshot) of the process and the you can enumerate the modules via Module32First and Module32Next.
Normally the ToolHelp functions are located in the kernel32.dll but on Win9x it is located in a separate DLL: tlhelp32.dll. Therefor we need to check the functions in both DLLs.

If you have NT4, then ToolHelp32-API is not available. But in NT4 you can use the PSAPI. To enumerate all modules you need to call EnumProcessModules, but you only get the handles to the modules. To feed SymLoadModule64 you also need to query the ModuleBaseAddr, SizeOfImage (via GetModuleInformation), ModuleBaseName (via GetModuleBaseName) and ModuleFileName(Path) (via GetModuleFileNameEx).

dbghelp.dll

There are a couple of issues with dbghelp.dll.

Options

To do some kind of modification of the behaviour, you can optionally specify some options. Here is the list of the available options:

  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;

Known issues

History