Printing a Stack Trace with MinGW

18/02/2015

If you're developing a C application on linux you can print a backtrace of the program using the backtrace library. Unfortunately this library isn't avaliable for Windows. If you're developing using MinGW you can use gdb to get a backtrace of a crashing program, but if you want to actually print out a backtrace to your users (or to file) when something goes wrong you are going to find it a bit more difficult.

There are two main reasons. The first is that the Windows stack walk API is extremely sensitive and fails under the most minor misconfiguration. And the second is that the Windows debugging symbols format is different to that used by linux and MinGW, and so you need to do the conversion before you can display them.

Here are the steps required to print out a stacktrace on Windows using MinGW:

  1. Walk the stack using the StackWalk64 API to print out the results.
  2. Compile your executable using MinGW, including debug information.
  3. Convert the debugging information to the Windows format using cv2pdb.
  4. Run your program and produce stack trace.

The most common way to walk the stack on Windows is the StackWalk64 function. There are plenty of resources online for this function which you are advised to read over before getting started. There is also an unofficial, but definitive, resource on using StackWalk64 which contains lots of answers to smaller details, and solved lots of my issues. It can be found here.

So let's assume we want to write a function which will print out a stack trace for the running program. The first step is to get references to the current thread, process, and context. Getting references to the current thread and process is easy.

HANDLE process = GetCurrentProcess();
HANDLE thread = GetCurrentThread();

But getting a reference to the current context is more difficult. On 64 bit windows you need to use the RtlCaptureContext function. Don't try using the GetThreadContext function on the current thread - it wont work. There are some workarounds for getting the context on 32-bit windows, but it isn't pretty.

CONTEXT context;
memset(&context, 0, sizeof(CONTEXT));
context.ContextFlags = CONTEXT_FULL;
RtlCaptureContext(&context);

Once the context is loaded we need to initalize the symbol hander. This assumes our debugging information is somewhere on the symbol search path. The easiest way to ensure this is the case is to put the generated debugging file in the same directory as the executable.

SymInitialize(process, NULL, TRUE);

We then need to prepare a STACKFRAME64 struct for the StackWalk64 function. This structure essentially tells the function what state the stack is in I.E where the stack and frame pointers are located. This is what we needed the current context for as it contains this information. This part of the code is platform dependant, so needs to be done differently for 32-bit and 64-bit. It can be done like this:

DWORD image;
STACKFRAME64 stackframe;
ZeroMemory(&stackframe, sizeof(STACKFRAME64));

#ifdef _M_IX86
image = IMAGE_FILE_MACHINE_I386;
stackframe.AddrPC.Offset = context.Eip;
stackframe.AddrPC.Mode = AddrModeFlat;
stackframe.AddrFrame.Offset = context.Ebp;
stackframe.AddrFrame.Mode = AddrModeFlat;
stackframe.AddrStack.Offset = context.Esp;
stackframe.AddrStack.Mode = AddrModeFlat;
#elif _M_X64
image = IMAGE_FILE_MACHINE_AMD64;
stackframe.AddrPC.Offset = context.Rip;
stackframe.AddrPC.Mode = AddrModeFlat;
stackframe.AddrFrame.Offset = context.Rsp;
stackframe.AddrFrame.Mode = AddrModeFlat;
stackframe.AddrStack.Offset = context.Rsp;
stackframe.AddrStack.Mode = AddrModeFlat;
#elif _M_IA64
image = IMAGE_FILE_MACHINE_IA64;
stackframe.AddrPC.Offset = context.StIIP;
stackframe.AddrPC.Mode = AddrModeFlat;
stackframe.AddrFrame.Offset = context.IntSp;
stackframe.AddrFrame.Mode = AddrModeFlat;
stackframe.AddrBStore.Offset = context.RsBSP;
stackframe.AddrBStore.Mode = AddrModeFlat;
stackframe.AddrStack.Offset = context.IntSp;
stackframe.AddrStack.Mode = AddrModeFlat;
#endif

As a side note, in researching for this problem I found people using lots of different intialisations for this structure - but this is the only one I've found that I am certain works. So please look carefully if copying code from the internet that the configuration matches this one.

Now it's time to walk the stack. To do this we repeatedly call StackWalk64 until it returns FALSE (or we reach some maximum depth). Each time we call it we print out the symbol name for the function address. To do this we construct a buffer the size of SYMBOL_INFO plus some space for the symbol name MAX_SYM_NAME. We then pass this to the function SymFromAddr, which should get the symbol name for that address using the debugging information. If we can't find the symbol for any reason then SymFromAddr will return FALSE and we'll just print out ??? to signify the missing symbol.

for (size_t i = 0; i < 25; i++) {
  
  BOOL result = StackWalk64(
    image, process, thread,
    &stackframe, &context, NULL, 
    SymFunctionTableAccess64, SymGetModuleBase64, NULL);
  
  if (!result) { break; }
  
  char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)];
  PSYMBOL_INFO symbol = (PSYMBOL_INFO)buffer;
  symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
  symbol->MaxNameLen = MAX_SYM_NAME;
  
  DWORD64 displacement = 0;
  if (SymFromAddr(process, stackframe.AddrPC.Offset, &displacement, symbol)) {
    printf("[%i] %s\n", i, symbol->Name);
  } else {
    printf("[%i] ???\n", i);
  }
  
}

All this leaves is for us to clean up the symbol handler.

SymCleanup(process);

Here is the code for the full program:

#include <windows.h>
#include <DbgHelp.h>

#include <stdio.h>
#include <stdlib.h>

static void stack_trace(void) {

  HANDLE process = GetCurrentProcess();
  HANDLE thread = GetCurrentThread();
  
  CONTEXT context;
  memset(&context, 0, sizeof(CONTEXT));
  context.ContextFlags = CONTEXT_FULL;
  RtlCaptureContext(&context);
  
  SymInitialize(process, NULL, TRUE);
  
  DWORD image;
  STACKFRAME64 stackframe;
  ZeroMemory(&stackframe, sizeof(STACKFRAME64));
  
#ifdef _M_IX86
  image = IMAGE_FILE_MACHINE_I386;
  stackframe.AddrPC.Offset = context.Eip;
  stackframe.AddrPC.Mode = AddrModeFlat;
  stackframe.AddrFrame.Offset = context.Ebp;
  stackframe.AddrFrame.Mode = AddrModeFlat;
  stackframe.AddrStack.Offset = context.Esp;
  stackframe.AddrStack.Mode = AddrModeFlat;
#elif _M_X64
  image = IMAGE_FILE_MACHINE_AMD64;
  stackframe.AddrPC.Offset = context.Rip;
  stackframe.AddrPC.Mode = AddrModeFlat;
  stackframe.AddrFrame.Offset = context.Rsp;
  stackframe.AddrFrame.Mode = AddrModeFlat;
  stackframe.AddrStack.Offset = context.Rsp;
  stackframe.AddrStack.Mode = AddrModeFlat;
#elif _M_IA64
  image = IMAGE_FILE_MACHINE_IA64;
  stackframe.AddrPC.Offset = context.StIIP;
  stackframe.AddrPC.Mode = AddrModeFlat;
  stackframe.AddrFrame.Offset = context.IntSp;
  stackframe.AddrFrame.Mode = AddrModeFlat;
  stackframe.AddrBStore.Offset = context.RsBSP;
  stackframe.AddrBStore.Mode = AddrModeFlat;
  stackframe.AddrStack.Offset = context.IntSp;
  stackframe.AddrStack.Mode = AddrModeFlat;
#endif

  for (size_t i = 0; i < 25; i++) {
    
    BOOL result = StackWalk64(
      image, process, thread,
      &stackframe, &context, NULL, 
      SymFunctionTableAccess64, SymGetModuleBase64, NULL);
    
    if (!result) { break; }
    
    char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)];
    PSYMBOL_INFO symbol = (PSYMBOL_INFO)buffer;
    symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
    symbol->MaxNameLen = MAX_SYM_NAME;
    
    DWORD64 displacement = 0;
    if (SymFromAddr(process, stackframe.AddrPC.Offset, &displacement, symbol)) {
      printf("[%i] %s\n", i, symbol->Name);
    } else {
      printf("[%i] ???\n", i);
    }
    
  }
  
  SymCleanup(process);

}

static void function_c(void) {
  stack_trace();
}

static void function_b(void) {
  function_c();
}

static void function_a(void) {
  function_b();
}

int main(int argc, char *argv[]) {
  function_a();
}

We can compile this program with the following command.

gcc -std=c99 -g test.c -lDbgHelp -o test.exe

Now initially when we run it we'll just get ??? for all the locations where our program's symbols would be. This is because we still need to create the windows debugging file for our program...

[0] ???
[1] ???
[2] ???
[3] ???
[4] ???
[5] ???
[6] ???
[7] BaseThreadInitThunk
[8] RtlUserThreadStart

When you compile a program using gcc with the -g flag it embeds debugging information such as function names and line numbers into the executable in a format called DWARF. This can be read by programs such as gdb to give you human readable information in situations such as backtraces. But native windows applications take a different approach - they create a separate file with the .pdb extension that contains all of the debugging information.

If we want to use the Windows APIs for debugging we need to first convert the debugging information to the correct format. To do this you can use the program cv2pdb.

I found it easiest to compile this program from source and add it to somewhere on my PATH. Make sure to compile it 64-bit if you are making 64-bit applications. If you compile it succesfully you should be able to run it on your executable and it should create a matching .pdb file of the same name.

cv2pdb test.exe

If it gives an error like "cannot load PDB helper DLL" try running it from the Visual Studio Developer Command Prompt. Now when you run your program because there is a .pdb file present with the symbols for your program the symbol handler will correctly find the names for the backtrace and print them out.

[0] stack_trace
[1] function_c
[2] function_b
[3] function_a
[4] main
[5] __tmainCRTStartup
[6] mainCRTStartup
[7] BaseThreadInitThunk
[8] RtlUserThreadStart

Voila!