Easy_backtraces_on_windows
When I work on C++, I tend to use assert
a lot. But the default output of c assert (at least using
MSVC, but pretty sure other compilers as well) is fairly simplistic:
Assertion failed: strlen(some_path) > 0, file C:\users\crist\_bazel_crist\3l4axkhv\execroot\_main\apps\cpp_test\main.cpp, line 62
It gets useful information out, but we can do better. In particular, the stack trace of what called the function with this error. Since I worked on Fuchsia, I had the idea that it was a fairly involved process, likely needing to roll libunwind as a dependency and the whole shabbang. Turns out that Windows offers this out of the box.
Windows CaptureStackBackTrace
Using the provided Windows function CaptureStackBackTrace makes it very easy to get the current stack trace.
constexpr u32 kFramesToCapture = 16;
std::array<void*, kFramesToCapture> frames;
u32 frame_count = CaptureStackBackTrace(frames_to_skip, kFramesToCapture, frames.data(), NULL);
Basically just provide an array of pointers that Windows will fill in. This is already useful and the information required for sending crash stack traced, but that is not that useful for humans. The really useful information is to know which file and line number, perhaps even function names, are to be interpreted from this stack trace.
For that we need the debug symbols.
Getting Windows to interpret symbols
Symbols are generated by the compiler on demand, and Windows keeps track of those when loading a module into the address space, either via starting the process or calling LoadLibrary to load a DLL on runtime.
For that we simply initialize the symbol module and ask for the information. You have to allocate a couple of structs (or alternatively create all that information on the stack if you only want to print). In my example I use an arena (bump) allocator, but a normal unique_ptr will do as well:
// In theory you should not be using |GetCurrentProcess| but everyone seems to do it...
HANDLE handle = GetCurrentProcess();
if (!SymInitialize(handle, nullptr, true)) {
// Print Windows error...
return;
}
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME);
// Symbol info buffer.
SYMBOL_INFO* symbol = (SYMBOL_INFO*)ArenaPushZero(arena, sizeof(SYMBOL_INFO) + 256);
symbol->MaxNameLen = 255;
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
IMAGEHLP_LINE64 line = {};
line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
std::printf("--- BACKTRACE ----------------------------------------------------------------\n");
bool has_seen_valid_frame = false;
u32 frames_skipped = 0;
for (u32 i = 0; i < frame_count; i++) {
u64 addr = (u64)frames[i];
// Get the file and line info.
const char* file = "<unknown>";
u32 line_number = 0;
DWORD displacement = 0;
if (SymGetLineFromAddr64(handle, addr, &displacement, &line)) {
file = line.FileName;
line_number = (u32)line.LineNumber;
file = paths::CleanPathFromBazel(file);
}
const char* function_name = "<unknown>";
if (SymFromAddr(handle, addr, nullptr, symbol)) {
function_name = symbol->Name;
}
std::printf("Frame %02d: %s (%s:%d)\n",
i - frames_skipped,
function_name,
file,
line_number);
}
SymCleanup(handle);
Basically you need to:
- Initialize the Symbol module
- Go over all the found frames found in the previous call to CaptureStackBackTrace and try to get the file, line and function name.
- Cleanup
Result
I rolled some more fancyness to my result:
- skipping the initial batch of unknown frames, which can happen if you call things like
std::abort
. - Trimming the paths if the path is from my project (I don’t need to see the whole path).
But roughly the flow is the same and the result is nice (in this case I triggered on purpose an OpenGL error):
GL_ERROR: 1282: ERROR of HIGH severity, raised from API: GL_INVALID_OPERATION error generated. Wrong component type or count.
--- BACKTRACE ----------------------------------------------------------------
Skipped 4 frames (were "<unknown>")
Frame 00: kdk::SetI32 (kandinsky\graphics\opengl.cpp:716)
Frame 01: RenderScene (apps\learn_opengl\learn_opengl.cpp:519)
Frame 02: GameRender (apps\learn_opengl\learn_opengl.cpp:611)
Frame 03: Render (kandinsky\main.cpp:35)
Frame 04: main (kandinsky\main.cpp:96)
Frame 05: invoke_main (D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:79)
Frame 06: __scrt_common_main_seh (D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288)
Frame 07: __scrt_common_main (D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:331)
Frame 08: mainCRTStartup (D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp:17)
Frame 09: BaseThreadInitThunk (<unknown>:0)
Frame 10: RtlUserThreadStart (<unknown>:0)
I rolled my own ASSERT
macro that permits me to add some messaging, but that is just icing over
the stack-traced cake.
Conclusion
For seasoned Windows developers (like game devs), this is likely nothing new, but I was pleasantly surprised with how easy was to get this functionality in, which now I roll into every project that I work on Windows.
As I understand, linux has a similar functionality with backtrace , so hopefully I can roll a cross-platform easy solution for me later on.