Detecting and reporting unhandled exceptions with SetUnhandledExceptionFilter seemed logical, and, in fact, it worked... for a while. Eventually, we started to notice failures that should have been reported as a last-chance exception but weren't. After much investigation, we discovered that both Direct3D and Flash were installing their own unhandled exception filters! Worse, they were fighting over it, installing their handlers several times per second! In practice, this meant our last-chance crash reports were rarely generated, convincing us our crash metrics were better than they were. (Bad, bad libraries!)
It's pretty ridiculous that we had to solve this problem, but, as Avery Lee says, "Just because it is not your fault does not mean it is not your problem."
The obvious solution is to join the fray, calling SetUnhandledExceptionFilter
every frame, right? How about we try something a bit more reliable... I hate implementing solutions that have obvious flaws. Thus, we chose to disable (with code modification) the SetUnhandledExceptionFilter
function immediately after installing our own handler. When Direct3D and Flash try to call it, their requests will be ignored, leaving our exception handler installed.
Code modification... isn't that scary? With a bit of knowledge and defensive programming, it's not that bad. In fact, I'll show you the code up front:
// If this doesn't make sense, skip the code and come back! void lockUnhandledExceptionFilter() { HMODULE kernel32 = LoadLibraryA("kernel32.dll"); Assert(kernel32); if (FARPROC gpaSetUnhandledExceptionFilter = GetProcAddress(kernel32, "SetUnhandledExceptionFilter")) { unsigned char expected_code[] = { 0x8B, 0xFF, // mov edi,edi 0x55, // push ebp 0x8B, 0xEC, // mov ebp,esp }; // only replace code we expect if (memcmp(expected_code, gpaSetUnhandledExceptionFilter, sizeof(expected_code)) == 0) { unsigned char new_code[] = { 0x33, 0xC0, // xor eax,eax 0xC2, 0x04, 0x00, // ret 4 }; BOOST_STATIC_ASSERT(sizeof(expected_code) == sizeof(new_code)); DWORD old_protect; if (VirtualProtect(gpaSetUnhandledExceptionFilter, sizeof(new_code), PAGE_EXECUTE_READWRITE, &old_protect)) { CopyMemory(gpaSetUnhandledExceptionFilter, new_code, sizeof(new_code)); DWORD dummy; VirtualProtect(gpaSetUnhandledExceptionFilter, sizeof(new_code), old_protect, &dummy); FlushInstructionCache(GetCurrentProcess(), gpaSetUnhandledExceptionFilter, sizeof(new_code)); } } } FreeLibrary(kernel32); }
If that's obvious to you, then great: We're hiring!
Otherwise, here is an overview:
Use GetProcAddress
to grab the real address of SetUnhandledExceptionFilter
. (If you just type &SetUnhandledExceptionFilter
you'll get the relocatable import thunk, not the actual SetUnhandledExceptionFilter
function.)
Most Windows functions begin with five bytes of prologue:
mov edi, edi ; 2 bytes for hotpatching support push ebp ; stack frame mov ebp, esp ; stack frame (con't)
We want to replace those five bytes with return 0;
. Remember that __stdcall
functions return values in the eax
register. We want to replace the above code with:
xor eax, eax ; eax = 0 ret 4 ; pops 4 bytes (arg) and returns
Also five bytes! How convenient! Before we replace the prologue, we verify that the first five bytes match our expectations. (If not, we can't feel comfortable about the effects of the code replacement.) The VirtualProtect and FlushInstructionCache calls are standard fare for code modification.
After implementing this, it's worth stepping through the assembly in a debugger to verify that SetUnhandledExceptionFilter
no longer has any effect. (If you really enjoy writing unit tests, it's definitely possible to unit test the desired behavior. I'll leave that as an exercise for the reader.)
Finally, our last-chance exception reporting actually works!