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:
- Walk the stack using the
StackWalk64
API to print out the results. - Compile your executable using MinGW, including debug information.
- Convert the debugging information to the Windows format using
cv2pdb
. - 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!