C#-Style Stack Tracing in MSVC++ Applications with Filenames and Line Numbers

A Little Background

So, you want to trace the stack of C++ applications in Windows, you say? You’ve come to the right place…luckily for you the dbghelp library is what you’re probably after, and I’ll explain that API shortly (I’ve made a very minimalistic class for this exact purpose because it’s just a fantastic library to use in and of itself when debugging, especially if you have some sort of logging mechanisms in place in your code, be it for production or debugging). But before we get into that, you also have the option of using Windows Error Reporting and crash dumps to catch those nasty production crashes, and often times its much, MUCH easier to do that than to use extra WINAPI libraries for stack tracing. So I will begin with a short detour on Windows Error Reporting…

Windows Error Reporting

Windows Error Reporting in an integrated OS feature within Microsoft Windows that allows you to generate crash dumps whenever a crash happens on any application running on your system. Generated crash dumps are files that you can open in Visual Studio and use to hook the Visual Studio debugger into. If you’ve never done that, I suggest you try it out right away! The crashed state of your application is essentially snapshot-ed into the dump file, so you’ll see the exact thread that threw the exception and its associated error messages. In fact, you’ll even be able to see where all running threads are at. If you’ve never used the Threads view while debugging, you’re really missing out. Its available in the top menu of Visual Studio while you’re debugging: Debug -> Windows -> Threads. You can even generate crash dumps on the fly for most running processes on your system through Task Manager (CTRL+SHIFT+ESCAPE) -> Right click the process you want a dump of -> Generate dump file. Note: I may be wrong but I think you may need to also deploy your application’s .pdb files in order for WER to generate full-on dumps. However, to actually hook the debugger in from your dump, the version of your solution’s code needs to match the version of your binaries that generated the dump.

Tracing a Windows MSVC++ Project’s Stack C#-Style

So if you’re like me and you just got into C++ applications in Windows (MFC, MSVC++, Win32 Console, WINAPI stuff, etc) from a C# background, you’d be pretty upset to find out that tracing just got a whole lot worse. If you’re also like me and want a nice and minimalistic library to use in order to trace your code, please feel free to use my StackTracer.h and StackTracer.cpp files. You can modify these as you wish but the only thing I do ask is that you keep the comment in my StackTracer.h header with my name in it. Also, and as always, you must agree to assume all liability and responsibility when using any code I provide. The StackTracing header and implementation basically just use the dbghelp.lib library (part of the WINAPI) to call functions like CaptureStackBackTrace or StackWalk, which basically allow you to walk the stack. The stack is, well, as you’ve come to know from C#, a place where stack frames reside, frames which store all kinds of neat information like return addresses and other things which we use to walk backwards up the call stack in order to see what went wrong.

How to Properly Set Up StackTracer

  • Recommended: Download the latest Standalone Debugging Tools for Windows (WinDbg) from MSDN. Install it. Find where it installed dbghelp.lib on your system as well as the dbghelp.dll file. Copy dbghelp.lib into a directory your project includes for libraries, and copy dbghelp.dll into the location where you run your project from (like the bin directory or something).
  • You must include dbghelp.lib into your project’s Linker’s dependencies in order to make use of these APIs in your project (Project Properties -> Configuration Properties -> Linker -> Input -> Additional Dependencies and add dbghelp.lib).
  • In order for everything to work with stack tracing, the PDB files must be present. Please ensure that your project is set to output debug information and has its PDBs in the working directory. You may set this by right-clicking your project, navigating to Properties -> Configuration Properties -> Linker -> Debugging, and set Generate Debug Info to Yes (/DEBUG), also making sure to set GenerateProgramDatabaseFile right below it to your output directory (typically, this is $(OutDir)$(TargetName).pdb).
  • Once enabled, you might see partial stack traces. This is because the inline optimization is enabled, or because your method is explicitly marked as inline. You can disable this feature on your project, by right-clicking it and navigating to Properties -> Configuration Properties -> C++ -> Optimization -> Inline Function Expansion and setting it to Disabled (/Ob0). For inline methods, you should search your project for the inline keyword and remove these methods temporarily.
  • Also, if you ever need to print a complete stack trace from somewhere in your code, you may use the PrintStackTrace() method. This will help if you have multiple threads contending for the same method, and are trying to figure out why something of that nature isn’t working. Inline methods and the inline optimizations may also cause you to see partial stack traces with this method as well, so it is recommended that you disable it while testing.
  • Yes, it catches C++ exceptions as well as SEH exceptions and I’m willing to bet that it also catches MFC exceptions. I’ve seen it work magic on runtime and believe me, it caught everything I pitched it so far and I didn’t even start playing baseball with it.
  • Yes, as you can tell from my overloads, it supports Win32, x64, x86, Unicode, Multibyte, you name it.
  • If you’ve read this far and understood everything thus far, then just include StackTracer.h and StackTracer.cpp in your project and create a new instance of the StackTracer class.

A Note on Inline

So why take a quick detour? Because you should know this. The inline function optimization, in a nutshell: Assume its on in a particular C++ project, and say you have two methods in this project: Method A and Method B. Method A calls Method B. The inline optimization will try to optimize your binaries so that, essentially, the compiled code in Method B is directly copied into Method A. If this happens, when the stack trace is printed, you will only see a partial stack trace, so your trace will likely start from Method A, because it never loaded a stack frame for the call to Method B (oh yes, these are the intricacies of methods in Windows). You may wonder why you always get a complete stack trace in C# applications? Well, actually there wasn’t any support for inline methods in C# as far as I know until .NET 4.5…that’s REALLY fresh considering when I wrote this article, and I’m not sure how it handles exceptions thrown within an inline C# method…something for you to try.

StackTracer.h

#pragma once

/*
StackTracer - v1.0 by Alexandru Dima

By default, StackTracer enables WER (Windows Error Reporting) to create crash dumps. This can be toggled by setting ENABLE_WER_CRASH_DUMPS to TRUE or FALSE. What it does is it creates a registry
key that tells Windows Error Reporting to generate crash dumps whenever an application crashes. The location of the dumps depend on what account you run the application with. They can can be dumped
out at C:\Windows\System32\TRACE_DUMP_FOLDER_LOCATION for example if running under the LOCAL SYSTEM account in Windows, or the respective user account folder for your current user account, just as an
example. WER will then try dumping all application crashes on the current machine there, not just for the application you run this code on. These dumps (.dmp files) located at TRACE_DUMP_FOLDER_LOCATION
can be opened in Visual Studio and used, against matching code (meaning your binaries have to match your code), to debug into the crash location. Therefore, it is recommended that you perfectly
save the code used in a particular deployment.

You can set EXCEPTION_STACK_TRACING_ENABLED to TRUE if you wish to capture stack traces on unhandled exceptions, as they will go to the vectored exception handler and print out the stack for that
exception. By default this feature is enabled (TRUE). Once enabled, you might see partial stack traces. This is because the inline optimization is enabled, or because your method is explicitly marked
as inline. You can disable this feature on your project, by right-clicking it and navigating to Properties -> Configuration Properties -> C++ -> Optimization -> Inline Function Expansion and
setting it to Disabled (/Ob0). For inline methods, you should search your project for the inline keyword and remove these methods temporarily.

Also, if you ever need to print a complete stack trace from somewhere in your code, you may use the PrintStackTrace() method. This will help if you have multiple threads contending for the
same method, and are trying to figure out why something of that nature isn't working. Inline methods and the inline optimizations may also cause you to see partial stack traces with this
method as well, so it is recommended that you disable it while testing.

And as always, in order for everything to work with stack tracing, the PDB files must be present. Please ensure that your project is set to output debug information and has its PDBs in the
working directory. You may set this by right-clicking your project, navigating to Properties -> Configuration Properties -> Linker -> Debugging, and set Generate Debug Info to Yes (/DEBUG),
also making sure to set GenerateProgramDatabaseFile right below it to your output directory (typically, this is $(OutDir)$(TargetName).pdb).

Once enabled, StackTracer's methods output to the standard output. Note, you must include dbghelp.lib in the Linker's dependencies in order to make use of these API's in another project.
*/

#include <Windows.h>
#include <dbghelp.h>
#include <sstream>
#include <iostream>

// Enables or disables stack tracing.
#define ENABLE_WER_CRASH_DUMPS TRUE
// Enables or disables stack tracing.
#define EXCEPTION_STACK_TRACING_ENABLED TRUE
// Sets the maximum function name of all functions when doing stack tracing.
#define TRACE_MAX_FUNCTION_NAME_LENGTH 1024
// Sets the maximum number of stack frames to walk when calling PrintStackTrace().
#define TRACE_MAX_STACK_FRAMES 1024
// Determines wether or not to catch exceptions as part of the WINAPI stack tracing methods.
#define TRACE_LOG_ERRORS FALSE
// This setting tells Windows Error Reporting to dump crashes to C:\Windows\System32\TRACE_DUMP_FOLDER_LOCATION under Local System credentials (services)...once set, it tries to dump ALL application crashes.
#define TRACE_DUMP_FOLDER_LOCATION "CrashDumps"
// Sets the maximum number of dump files for your TRACE_DUMP_FOLDER_LOCATION. Once max'd out, it will roll over.
#define TRACE_DUMP_MAXIMUM_DUMP_FILES 100
// Sets the dump type to a mini-dump. Possible values are: 0 (Custom Dump), 1 (Mini Dump), or 2 (Full Dump).
#define TRACE_DUMP_TYPE 1

void PrintStackTrace();

class StackTracer
{
public:
	StackTracer();
	~StackTracer();
};

StackTracer.cpp

#include "stdafx.h"
#include "StackTracer.h"

using namespace std;

#ifdef UNICODE
#define tstringstream wstringstream
#define tstring wstring
#define tcout wcout
#else
#define tstringstream stringstream
#define tstring string
#define tcout cout
#endif

// This function may be called at any point in time to get a print-out of the stack trace.
void PrintStackTrace()
{
	tstringstream stream;
	stream << _T("Started a stack trace") << std::endl;
	void *stack[TRACE_MAX_STACK_FRAMES];
	HANDLE process = GetCurrentProcess();
	SymInitialize(process, NULL, TRUE);
	WORD numberOfFrames = CaptureStackBackTrace(0, TRACE_MAX_STACK_FRAMES, stack, NULL);
	SYMBOL_INFO *symbol = (SYMBOL_INFO *)malloc(sizeof(SYMBOL_INFO) + (TRACE_MAX_FUNCTION_NAME_LENGTH - 1) * sizeof(TCHAR));
	memset(symbol, 0, sizeof(SYMBOL_INFO) + (TRACE_MAX_FUNCTION_NAME_LENGTH - 1) * sizeof(TCHAR));
	symbol->MaxNameLen = TRACE_MAX_FUNCTION_NAME_LENGTH;
	symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
	DWORD displacement;
	IMAGEHLP_LINE64 *line = (IMAGEHLP_LINE64 *)malloc(sizeof(IMAGEHLP_LINE64));
	memset(line, 0, sizeof(IMAGEHLP_LINE64));
	line->SizeOfStruct = sizeof(IMAGEHLP_LINE64);
	for (int i = 0; i < numberOfFrames; i++)
	{
		DWORD64 address = (DWORD64)(stack[i]);
		if (SymFromAddr(process, address, NULL, symbol))
		{
			if (SymGetLineFromAddr64(process, address, &displacement, line))
			{
				stream << _T("\tat ") << symbol->Name << _T(" in ") << line->FileName << _T(": line: ") << std::dec << line->LineNumber << _T(": address: ") << std::hex << symbol->Address << std::endl;
			}
			else
			{
				if (TRACE_LOG_ERRORS)
				{
					stream << _T("Error from SymGetLineFromAddr64: ") << std::hex << GetLastError() << std::endl;
				}
				stream << _T("\tat ") << symbol->Name << _T(": address: ") << std::hex << symbol->Address << std::endl;
			}
		}
		else if (TRACE_LOG_ERRORS)
		{
			stream << _T("Error from SymFromAddr: ") << std::hex << GetLastError() << std::endl;
		}
	}
	tstring str = stream.str();
	LPCTSTR message = str.c_str();
	std::tcout << message;
	free(symbol);
	free(line);
}

void PrintStackTraceFromExceptionContext(CONTEXT context, DWORD exceptionCode)
{
	tstringstream stream;
	if (exceptionCode)
	{
		stream << _T("Caught exception ") << std::hex << exceptionCode << std::endl;
	}
	else
	{
		stream << _T("Caught an exception") << std::endl;
	}
	HANDLE thread = GetCurrentThread();
	HANDLE process = GetCurrentProcess();
	// The image and frame will be set based on the processor architecture. I don't set these addresses at random or through some voodoo; they come from the documentation at http://msdn.microsoft.com/en-us/library/windows/desktop/ms680646(v=vs.85).aspx.
	DWORD image;
#ifdef _M_IX86
	STACKFRAME *frame = (STACKFRAME *)malloc(sizeof(STACKFRAME));
	memset(frame, 0, sizeof(STACKFRAME));
	image = IMAGE_FILE_MACHINE_I386;
	frame->AddrPC.Offset = context.Eip;
	frame->AddrPC.Mode = AddrModeFlat;
	frame->AddrFrame.Offset = context.Ebp;
	frame->AddrFrame.Mode = AddrModeFlat;
	frame->AddrStack.Offset = context.Esp;
	frame->AddrStack.Mode = AddrModeFlat;
#elif _M_X64
	STACKFRAME64 *frame = (STACKFRAME64 *)malloc(sizeof(STACKFRAME64));
	memset(frame, 0, sizeof(STACKFRAME64));
	image = IMAGE_FILE_MACHINE_AMD64;
	frame->AddrPC.Offset = context.Rip;
	frame->AddrPC.Mode = AddrModeFlat;
	frame->AddrFrame.Offset = context.Rbp;
	frame->AddrFrame.Mode = AddrModeFlat;
	frame->AddrStack.Offset = context.Rsp;
	frame->AddrStack.Mode = AddrModeFlat;
#elif _M_IA64
	STACKFRAME64 *frame = (STACKFRAME64 *)malloc(sizeof(STACKFRAME64));
	memset(frame, 0, sizeof(STACKFRAME64));
	image = IMAGE_FILE_MACHINE_IA64;
	frame->AddrPC.Offset = context.StIIP;
	frame->AddrPC.Mode = AddrModeFlat;
	frame->AddrFrame.Offset = context.IntSp;
	frame->AddrFrame.Mode = AddrModeFlat;
	frame->AddrBStore.Offset = context.RsBSP;
	frame->AddrBStore.Mode = AddrModeFlat;
	frame->AddrStack.Offset = context.IntSp;
	frame->AddrStack.Mode = AddrModeFlat;
#else
#error "This platform is not supported."
#endif
	SYMBOL_INFO *symbol = (SYMBOL_INFO *)malloc(sizeof(SYMBOL_INFO) + (TRACE_MAX_FUNCTION_NAME_LENGTH - 1) * sizeof(TCHAR));
	memset(symbol, 0, sizeof(SYMBOL_INFO) + (TRACE_MAX_FUNCTION_NAME_LENGTH - 1) * sizeof(TCHAR));
	symbol->MaxNameLen = TRACE_MAX_FUNCTION_NAME_LENGTH;
	symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
	IMAGEHLP_LINE64 *line = (IMAGEHLP_LINE64 *)malloc(sizeof(IMAGEHLP_LINE64));
	memset(line, 0, sizeof(IMAGEHLP_LINE64));
	line->SizeOfStruct = sizeof(IMAGEHLP_LINE64);
	DWORD displacement = 0;
	while (StackWalk(image, process, thread, frame, &context, NULL, NULL, NULL, NULL) && frame->AddrPC.Offset != frame->AddrReturn.Offset && frame->AddrPC.Offset != 0)
	{
		if (SymFromAddr(process, frame->AddrPC.Offset, NULL, symbol))
		{
			if (SymGetLineFromAddr64(process, frame->AddrPC.Offset, &displacement, line))
			{
				stream << _T("\tat ") << symbol->Name << _T(" in ") << line->FileName << _T(": line: ") << std::dec << line->LineNumber << _T(": address: ") << std::hex << symbol->Address << std::endl;
			}
			else
			{
				if (TRACE_LOG_ERRORS)
				{
					stream << _T("Error from SymGetLineFromAddr64: ") << std::hex << GetLastError() << std::endl;
				}
				stream << _T("\tat ") << symbol->Name << _T(": address: ") << std::hex << symbol->Address << std::endl;
			}
		}
		else if (TRACE_LOG_ERRORS)
		{
			stream << _T("Error from SymFromAddr: ") << std::hex << GetLastError() << std::endl;
		}
	}
	DWORD error = GetLastError();
	if (error && TRACE_LOG_ERRORS)
	{
		stream << _T("Error from StackWalk64: ") << std::hex << error << std::endl;
	}
	tstring str = stream.str();
	LPCTSTR message = str.c_str();
	std::tcout << message;
	free(symbol);
	free(line);
	free(frame);
}

// This method is called on the tracer's constructor and tells Windows Error Reporting not only where to put crash dumps, but to actually generate them.
void SetRegistryKeyForWindowsErrorReportingToGenerateMiniCrashDumpsOnACrash()
{
	HKEY key;
	if (RegCreateKeyEx(HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\Microsoft\\Windows\\Windows Error Reporting\\LocalDumps"), 0, NULL, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &key, NULL) == ERROR_SUCCESS)
	{
		DWORD dumpCount = TRACE_DUMP_MAXIMUM_DUMP_FILES;
		RegSetValueEx(key, TEXT("DumpCount"), 0, REG_DWORD, (const BYTE *)(&dumpCount), sizeof(dumpCount));
		DWORD dumpType = TRACE_DUMP_TYPE;
		RegSetValueEx(key, TEXT("DumpType"), 0, REG_DWORD, (const BYTE *)(&dumpType), sizeof(dumpType));
		RegSetValueEx(key, TEXT("DumpFolder"), 0, REG_EXPAND_SZ, (const BYTE *)TEXT(TRACE_DUMP_FOLDER_LOCATION), sizeof(TEXT(TRACE_DUMP_FOLDER_LOCATION)));
		RegCloseKey(key);
	}
}

LONG WINAPI VectoredExceptionCallback(PEXCEPTION_POINTERS exception)
{
	if (exception != nullptr)
	{
		if (exception->ExceptionRecord->ExceptionCode && exception->ExceptionRecord->ExceptionCode <= 2147483647)
		{
			// This is a COM success code and can be ignored according to http://msdn.microsoft.com/en-us/library/windows/desktop/ff485841(v=vs.85).aspx. Anything above that, and you'll want to log it out as an actual exception.
			tstringstream stream;
			stream << _T("Caught COM success code ") << std::hex << exception->ExceptionRecord->ExceptionCode << _T(". Ignoring it.") << std::endl;
			tstring str = stream.str();
			LPCTSTR message = str.c_str();
			std::tcout << message;
		}
		else
		{
			// Trace the stack from the exception (note, this may be a partial stack trace in scenarios where inline compiler optimizations are enabled).
			PrintStackTraceFromExceptionContext(*(exception->ContextRecord), exception->ExceptionRecord->ExceptionCode);
		}
	}
	// Send the exception back to the current process.
	return EXCEPTION_CONTINUE_SEARCH;
}

// This constructor should be called at the start of your main class constructor, and you should only ever create one instance of this per application.
StackTracer::StackTracer()
{
	if (ENABLE_WER_CRASH_DUMPS)
	{
		SetRegistryKeyForWindowsErrorReportingToGenerateMiniCrashDumpsOnACrash();
	}
	if (EXCEPTION_STACK_TRACING_ENABLED)
	{
		SymSetOptions(SYMOPT_DEFERRED_LOADS);
		SymInitialize(GetCurrentProcess(), NULL, TRUE);
		AddVectoredExceptionHandler(NULL, VectoredExceptionCallback);
	}
}

StackTracer::~StackTracer()
{
	if (EXCEPTION_STACK_TRACING_ENABLED)
	{
		SymCleanup(GetCurrentProcess());
	}
}

Example Win32 Console Application that Utilizes a StackTracer Instance

#include "stdafx.h"
#include "StackTracer.h"
#include <process.h>
#include <iostream>

using namespace std;

#ifdef UNICODE
#define tcout wcout
#else
#define tcout cout
#endif

void thirdMethod()
{
	std::tcout << "thirdMethod was called." << std::endl;
	throw new exception();
}

void secondMethod()
{
	std::tcout << "secondMethod was called." << std::endl;
	thirdMethod();
}

void firstMethod(void *parameters)
{
	std::tcout << "firstMethod was called." << std::endl;
	secondMethod();
	_endthread();
}

int _tmain(int argc, _TCHAR* argv[])
{
	std::tcout << "About to start the StackTracer test harness." << std::endl;
	StackTracer *tracer = new StackTracer();
	_beginthread(firstMethod, 0, NULL);
	std::tcout << "Started the StackTracer test harness successfully." << std::endl;
	std::cin.get();
	return 0;
}

Example Output

About to start the StackTracer test harness.
Started the StackTracer test harness successfully.
firstMethod was called.
secondMethod was called.
thirdMethod was called.
Caught exception e06d7363
        at RaiseException: address: 76921d05
        at _CxxThrowException: address: 10090af0
        at thirdMethod in c:\users\dima\documents\visual studio 2013\projects\st
acktracing\stacktracing\stacktracing.cpp: line: 18: address: 84ca90
        at secondMethod in c:\users\dima\documents\visual studio 2013\projects\s
tacktracing\stacktracing\stacktracing.cpp: line: 24: address: 84ca10
        at firstMethod in c:\users\dima\documents\visual studio 2013\projects\st
acktracing\stacktracing\stacktracing.cpp: line: 30: address: 84c980
        at _beginthread: address: ffd31e0
        at _endthread: address: ffd33e0
        at BaseThreadInitThunk: address: 74959191
        at RtlInitializeExceptionChain: address: 76f2a847
        at RtlInitializeExceptionChain: address: 76f2a847

Alexandru

"To avoid criticism, say nothing, do nothing, be nothing." - Aristotle

"It is wise to direct your anger towards problems - not people; to focus your energies on answers - not excuses." - William Arthur Ward

"Science does not know its debt to imagination." - Ralph Waldo Emerson

"Money was never a big motivation for me, except as a way to keep score. The real excitement is playing the game." - Donald Trump

"All our dreams can come true, if we have the courage to pursue them." - Walt Disney

"Mitch flashes back to a basketball game held in the Brandeis University gymnasium in 1979. The team is doing well and chants, 'We're number one!' Morrie stands and shouts, 'What's wrong with being number two?' The students fall silent." - Tuesdays with Morrie

I'm not entirely sure what makes me successful in general programming or development, but to any newcomers to this blood-sport, my best guess would be that success in programming comes from some strange combination of interest, persistence, patience, instincts (for example, someone might tell you that something can't be done, or that it can't be done a certain way, but you just know that can't be true, or you look at a piece of code and know something doesn't seem right with it at first glance, but you can't quite put your finger on it until you think it through some more), fearlessness of tinkering, and an ability to take advice because you should be humble. Its okay to be wrong or to have a bad approach, realize it, and try to find a better one, and even better to be wrong and find a better approach to solve something than to have had a bad approach to begin with. I hope that whatever fragments of information I sprinkle across here help those who hit the same roadblocks.

Leave a Reply

Your email address will not be published. Required fields are marked *