1
0
forked from 0ad/0ad
0ad/source/lib/sysdep/win/wdbg.cpp
janwas 1d69845cee snd: better snd_disable dox
ia32: fix comments and fix EBP warning
wdbg: fix bug if exception locus is unavailable
wdetect: store full DS driver path (-> version reported correctly)

This was SVN commit r2491.
2005-07-20 00:54:39 +00:00

1086 lines
29 KiB
C++
Executable File

// Windows debug backend
// Copyright (c) 2002-2005 Jan Wassenberg
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// Contact info:
// Jan.Wassenberg@stud.uni-karlsruhe.de
// http://www.stud.uni-karlsruhe.de/~urkt/
#include "precompiled.h"
#include <stdlib.h>
#include <stdio.h>
#include "lib.h"
#include "win_internal.h"
#include "posix.h"
#include "sysdep/cpu.h"
#include "wdbg.h"
#include "byte_order.h" // FOURCC
// optional: enables translation of the "unhandled exception" dialog.
#ifdef I18N
#include "ps/i18n.h"
#endif
#pragma data_seg(WIN_CALLBACK_PRE_MAIN(b))
WIN_REGISTER_FUNC(wdbg_init);
#pragma data_seg()
// used to prevent the vectored exception handler from taking charge when
// an exception is raised from the main thread (allows __try blocks to
// get control). latched in wdbg_init.
static DWORD main_thread_id;
// protects the breakpoint helper thread.
static void lock()
{
win_lock(WDBG_CS);
}
static void unlock()
{
win_unlock(WDBG_CS);
}
// used in a desperate attempt to avoid deadlock in wdbg_exception_handler.
static bool is_locked()
{
return win_is_locked(WDBG_CS) == 1;
}
// return localized version of <text>, if i18n functionality is available.
// this is used to translate the "unhandled exception" dialog strings.
// WARNING: leaks memory returned by wcsdup, but that's ok since the
// program will terminate soon after. fixing this is hard and senseless.
static const wchar_t* translate(const wchar_t* text)
{
#ifdef HAVE_I18N
// make sure i18n system is (already|still) initialized.
if(g_CurrentLocale)
{
// be prepared for this to fail, because translation potentially
// involves script code and the JS context might be corrupted.
__try
{
const wchar_t* text2 = wcsdup(I18n::translate(text).c_str());
// only overwrite if wcsdup succeeded, i.e. not out of memory.
if(text2)
text = text2;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
}
}
#endif
return text;
}
// convenience wrapper using translate.
static void translate_and_display_msg(const wchar_t* caption, const wchar_t* text)
{
wdisplay_msg(translate(caption), translate(text));
}
//////////////////////////////////////////////////////////////////////////////
// need to shoehorn printf-style variable params into
// the OutputDebugString call.
// - don't want to split into multiple calls - would add newlines to output.
// - fixing Win32 _vsnprintf to return # characters that would be written,
// as required by C99, looks difficult and unnecessary. if any other code
// needs that, implement GNU vasprintf.
// - fixed size buffers aren't nice, but much simpler than vasprintf-style
// allocate+expand_until_it_fits. these calls are for quick debug output,
// not loads of data, anyway.
// max # characters (including \0) output by debug_(w)printf in one call.
static const int MAX_CNT = 512;
void debug_printf(const char* fmt, ...)
{
char buf[MAX_CNT];
buf[MAX_CNT-1] = '\0';
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, MAX_CNT-1, fmt, ap);
va_end(ap);
OutputDebugStringA(buf);
}
void debug_wprintf(const wchar_t* fmt, ...)
{
wchar_t buf[MAX_CNT];
buf[MAX_CNT-1] = '\0';
va_list ap;
va_start(ap, fmt);
vswprintf(buf, MAX_CNT-1, fmt, ap);
va_end(ap);
OutputDebugStringW(buf);
}
//-----------------------------------------------------------------------------
// debug memory allocator
//-----------------------------------------------------------------------------
// check heap integrity (independently of mmgr).
// errors are reported by the CRT or via display_error.
void debug_heap_check()
{
int ret;
__try
{
ret = _heapchk();
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
ret = _HEAPBADNODE;
}
if(ret != _HEAPOK)
DISPLAY_ERROR(L"debug_heap_check: heap is corrupt");
}
// call at any time; from then on, the specified checks will be performed.
// if not called, the default is DEBUG_HEAP_NONE, i.e. do nothing.
void debug_heap_enable(DebugHeapChecks what)
{
#ifdef HAVE_VC_DEBUG_ALLOC
uint flags = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG);
switch(what)
{
case DEBUG_HEAP_NONE:
flags = 0;
break;
case DEBUG_HEAP_NORMAL:
flags |= _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF;
break;
case DEBUG_HEAP_ALL:
flags |= _CRTDBG_CHECK_ALWAYS_DF | _CRTDBG_DELAY_FREE_MEM_DF |
_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF;
break;
default:
debug_assert("debug_heap_enable: invalid what");
}
_CrtSetDbgFlag(flags);
#endif // HAVE_DEBUGALLOC
}
// return 1 if the pointer appears to be totally bogus, otherwise 0.
// this check is not authoritative in that the pointer may in truth
// be invalid regardless of the return value here, but can be used
// to filter out obviously wrong values in a portable manner.
int debug_is_bogus_pointer(const void* p)
{
#ifdef _M_IX86
if(p < (void*)0x10000)
return true;
if(p >= (void*)(uintptr_t)0x80000000)
return true;
#endif
return IsBadReadPtr(p, 1) != 0;
}
//-----------------------------------------------------------------------------
// to avoid deadlock, be VERY CAREFUL to avoid anything that may block,
// including locks taken by the OS (e.g. malloc, GetProcAddress).
typedef int(*WhileSuspendedFunc)(HANDLE hThread, void* user_arg);
struct WhileSuspendedParam
{
HANDLE hThread;
WhileSuspendedFunc func;
void* user_arg;
};
static void* while_suspended_thread_func(void* user_arg)
{
DWORD err;
WhileSuspendedParam* param = (WhileSuspendedParam*)user_arg;
err = SuspendThread(param->hThread);
// abort, since GetThreadContext only works if the target is suspended.
if(err == (DWORD)-1)
{
debug_warn("while_suspended_thread_func: SuspendThread failed");
goto fail;
}
// target is now guaranteed to be suspended,
// since the Windows counter never goes negative.
int ret = param->func(param->hThread, param->user_arg);
err = ResumeThread(param->hThread);
debug_assert(err != 0);
return (void*)(intptr_t)ret;
fail:
return (void*)(intptr_t)-1;
}
static int call_while_suspended(WhileSuspendedFunc func, void* user_arg)
{
int err;
// we need a real HANDLE to the target thread for use with
// Suspend|ResumeThread and GetThreadContext.
// alternative: DuplicateHandle on the current thread pseudo-HANDLE.
// this way is a bit more obvious/simple.
const DWORD access = THREAD_GET_CONTEXT|THREAD_SET_CONTEXT|THREAD_SUSPEND_RESUME;
HANDLE hThread = OpenThread(access, FALSE, GetCurrentThreadId());
if(hThread == INVALID_HANDLE_VALUE)
{
debug_warn("OpenThread failed");
return -1;
}
WhileSuspendedParam param = { hThread, func, user_arg };
pthread_t thread;
err = pthread_create(&thread, 0, while_suspended_thread_func, &param);
debug_assert(err == 0);
void* ret;
err = pthread_join(thread, &ret);
debug_assert(err == 0 && ret == 0);
return (int)(intptr_t)ret;
}
//////////////////////////////////////////////////////////////////////////////
//
// breakpoints
//
//////////////////////////////////////////////////////////////////////////////
// breakpoints are set by storing the address of interest in a
// debug register and marking it 'enabled'.
//
// the first problem is, they are only accessible from Ring0;
// we get around this by updating their values via SetThreadContext.
// that in turn requires we suspend the current thread,
// spawn a helper to change the registers, and resume.
// parameter passing to helper thread. currently static storage,
// but the struct simplifies switching to a queue later.
static struct BreakInfo
{
uintptr_t addr;
DbgBreakType type;
// determines what brk_thread_func will do.
// set/reset by debug_remove_all_breaks.
bool want_all_disabled;
}
brk_info;
// Local Enable bits of all registers we enabled (used when restoring all).
static DWORD brk_all_local_enables;
static const uint MAX_BREAKPOINTS = 4;
// IA-32 limit; if this changes, make sure brk_enable still works!
// (we assume CONTEXT has contiguous Dr0..Dr3 register fields)
// remove all breakpoints enabled by debug_set_break from <context>.
// called while target is suspended.
static int brk_disable_all_in_ctx(BreakInfo* bi, CONTEXT* context)
{
context->Dr7 &= ~brk_all_local_enables;
return 0;
}
// find a free register, set type according to <bi> and
// mark it as enabled in <context>.
// called while target is suspended.
static int brk_enable_in_ctx(BreakInfo* bi, CONTEXT* context)
{
int reg; // index (0..3) of first free reg
uint LE; // local enable bit for <reg>
// find free debug register.
for(reg = 0; reg < MAX_BREAKPOINTS; reg++)
{
LE = BIT(reg*2);
// .. this one is currently not in use.
if((context->Dr7 & LE) == 0)
goto have_reg;
}
debug_warn("brk_enable_in_ctx: no register available");
return ERR_LIMIT;
have_reg:
// set value and mark as enabled.
(&context->Dr0)[reg] = (DWORD)bi->addr; // see MAX_BREAKPOINTS
context->Dr7 |= LE;
brk_all_local_enables |= LE;
// build Debug Control Register value.
// .. type
uint rw = 0;
switch(bi->type)
{
case DBG_BREAK_CODE:
rw = 0; break;
case DBG_BREAK_DATA:
rw = 1; break;
case DBG_BREAK_DATA_WRITE:
rw = 3; break;
default:
debug_warn("brk_enable_in_ctx: invalid type");
}
// .. length (determined from addr's alignment).
// note: IA-32 requires len=0 for code breakpoints.
uint len = 0;
if(bi->type != DBG_BREAK_CODE)
{
const uint alignment = (uint)(bi->addr % 4);
// assume 2 byte range
if(alignment == 2)
len = 1;
// assume 4 byte range
else if(alignment == 0)
len = 3;
// else: 1 byte range; len already set to 0
}
const uint shift = (16 + reg*4);
const uint field = (len << 2) | rw;
// clear previous contents of this reg's field
// (in case the previous user didn't do so on disabling).
const uint mask = 0xFu << shift;
context->Dr7 &= ~mask;
context->Dr7 |= field << shift;
return 0;
}
// carry out the request stored in the BreakInfo* parameter.
// called while target is suspended.
static int brk_do_request(HANDLE hThread, void* arg)
{
int ret;
BreakInfo* bi = (BreakInfo*)arg;
CONTEXT context;
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
if(!GetThreadContext(hThread, &context))
{
debug_warn("brk_do_request: GetThreadContext failed");
goto fail;
}
#if defined(_M_IX86)
if(bi->want_all_disabled)
ret = brk_disable_all_in_ctx(bi, &context);
else
ret = brk_enable_in_ctx (bi, &context);
if(!SetThreadContext(hThread, &context))
{
debug_warn("brk_do_request: SetThreadContext failed");
goto fail;
}
#else
#error "port"
#endif
return 0;
fail:
return -1;
}
// arrange for a debug exception to be raised when <addr> is accessed
// according to <type>.
// for simplicity, the length (range of bytes to be checked) is
// derived from addr's alignment, and is typically 1 machine word.
// breakpoints are a limited resource (4 on IA-32); abort and
// return ERR_LIMIT if none are available.
int debug_set_break(void* p, DbgBreakType type)
{
lock();
brk_info.addr = (uintptr_t)p;
brk_info.type = type;
int ret = call_while_suspended(brk_do_request, &brk_info);
unlock();
return ret;
}
// remove all breakpoints that were set by debug_set_break.
// important, since these are a limited resource.
int debug_remove_all_breaks()
{
lock();
brk_info.want_all_disabled = true;
int ret = call_while_suspended(brk_do_request, &brk_info);
brk_info.want_all_disabled = false;
unlock();
return ret;
}
//////////////////////////////////////////////////////////////////////////////
//
// exception handler
//
//////////////////////////////////////////////////////////////////////////////
/*
CSmartHandle hImpersonationToken = NULL;
if(!GetImpersonationToken(&hImpersonationToken.m_h))
{
return FALSE;
}
// We need the SeDebugPrivilege to be able to run MiniDumpWriteDump
TOKEN_PRIVILEGES tp;
BOOL bPrivilegeEnabled = EnablePriv(SE_DEBUG_NAME, hImpersonationToken, &tp);
// DBGHELP.DLL is not thread safe
EnterCriticalSection(pCS);
bRet = pDumpFunction(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, MiniDumpWithDataSegs, &stInfo, NULL, NULL);
LeaveCriticalSection(pCS);
if(bPrivilegeEnabled)
{
// Restore the privilege
RestorePriv(hImpersonationToken, &tp);
}
static BOOL GetImpersonationToken(HANDLE* phToken)
{
*phToken = NULL;
if(!OpenThreadToken(GetCurrentThread(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, TRUE, phToken))
{
if(GetLastError() == ERROR_NO_TOKEN)
{
// No impersonation token for the curren thread available - go for the process token
if(!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, phToken))
{
return FALSE;
}
}
else
{
return FALSE;
}
}
return TRUE;
}
static BOOL EnablePriv(LPCTSTR pszPriv, HANDLE hToken, TOKEN_PRIVILEGES* ptpOld)
{
BOOL bOk = FALSE;
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
bOk = LookupPrivilegeValue( 0, pszPriv, &tp.Privileges[0].Luid);
if(bOk)
{
DWORD cbOld = sizeof(*ptpOld);
bOk = AdjustTokenPrivileges(hToken, FALSE, &tp, cbOld, ptpOld, &cbOld);
}
return (bOk && (ERROR_NOT_ALL_ASSIGNED != GetLastError()));
}
static BOOL RestorePriv(HANDLE hToken, TOKEN_PRIVILEGES* ptpOld)
{
BOOL bOk = AdjustTokenPrivileges(hToken, FALSE, ptpOld, 0, 0, 0);
return (bOk && (ERROR_NOT_ALL_ASSIGNED != GetLastError()));
}
BOOL SetPrivilege(
HANDLE hToken, // token handle
LPCTSTR Privilege, // Privilege to enable/disable
BOOL bEnablePrivilege // TRUE to enable. FALSE to disable
)
{
TOKEN_PRIVILEGES tp;
LUID luid;
TOKEN_PRIVILEGES tpPrevious;
DWORD cbPrevious=sizeof(TOKEN_PRIVILEGES);
if(!LookupPrivilegeValue( NULL, Privilege, &luid )) return FALSE;
//
// first pass. get current privilege setting
//
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = 0;
AdjustTokenPrivileges(
hToken,
FALSE,
&tp,
sizeof(TOKEN_PRIVILEGES),
&tpPrevious,
&cbPrevious
);
if (GetLastError() != ERROR_SUCCESS) return FALSE;
//
// second pass. set privilege based on previous setting
//
tpPrevious.PrivilegeCount = 1;
tpPrevious.Privileges[0].Luid = luid;
if(bEnablePrivilege) {
tpPrevious.Privileges[0].Attributes |= (SE_PRIVILEGE_ENABLED);
}
else {
tpPrevious.Privileges[0].Attributes ^= (SE_PRIVILEGE_ENABLED &
tpPrevious.Privileges[0].Attributes);
}
AdjustTokenPrivileges(
hToken,
FALSE,
&tpPrevious,
cbPrevious,
NULL,
NULL
);
if (GetLastError() != ERROR_SUCCESS) return FALSE;
return TRUE;
}
BOOL SetPrivilege2(
HANDLE hToken, // token handle
LPCTSTR Privilege, // Privilege to enable/disable
BOOL bEnablePrivilege // TRUE to enable. FALSE to disable
)
{
TOKEN_PRIVILEGES tp = { 0 };
// Initialize everything to zero
LUID luid;
DWORD cb=sizeof(TOKEN_PRIVILEGES);
if(!LookupPrivilegeValue( NULL, Privilege, &luid ))
return FALSE;
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
if(bEnablePrivilege) {
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
} else {
tp.Privileges[0].Attributes = 0;
}
AdjustTokenPrivileges( hToken, FALSE, &tp, cb, NULL, NULL );
if (GetLastError() != ERROR_SUCCESS)
return FALSE;
return TRUE;
}
extern WINBASEAPI LANGID WINAPI GetSystemDefaultLangID (void);
void DisplayError(
LPTSTR szAPI // pointer to failed API name
)
{
LPTSTR MessageBuffer;
DWORD dwBufferLength;
fprintf(stderr,"%s() error!\n", szAPI);
/*
if(dwBufferLength=FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
GetLastError(),
GetSystemDefaultLangID(),
(LPTSTR) &MessageBuffer,
0,
NULL
))
{
DWORD dwBytesWritten;
//
// Output message string on stderr
//
WriteFile(
GetStdHandle(STD_ERROR_HANDLE),
MessageBuffer,
dwBufferLength,
&dwBytesWritten,
NULL
);
//
// free the buffer allocated by the system
//
LocalFree(MessageBuffer);
}
}
static int screwaround()
{
HANDLE hProcess;
HANDLE hToken;
int dwRetVal=RTN_OK; // assume success from main()
if(!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &hToken))
{
if (GetLastError() == ERROR_NO_TOKEN)
{
if (!ImpersonateSelf(SecurityImpersonation))
return RTN_ERROR;
if(!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &hToken)){
DisplayError("OpenThreadToken");
return RTN_ERROR;
}
}
else
return RTN_ERROR;
}
// enable SeDebugPrivilege
if(!SetPrivilege(hToken, SE_DEBUG_NAME, TRUE))
{
DisplayError("SetPrivilege");
// close token handle
CloseHandle(hToken);
// indicate failure
return RTN_ERROR;
}
// disable SeDebugPrivilege
SetPrivilege(hToken, SE_DEBUG_NAME, FALSE);
}
*/
//
// analyze exceptions; determine their type and locus
//
// storage for strings built by get_SEH_exception_description and get_cpp_exception_description.
static wchar_t description_buf[128];
// VC++ exception handling internals.
// see http://www.codeproject.com/cpp/exceptionhandler.asp
struct XTypeInfo
{
DWORD _;
const std::type_info* ti;
// ..
};
struct XTypeInfoArray
{
DWORD count;
const XTypeInfo* types[1];
};
struct XInfo
{
DWORD _[3];
const XTypeInfoArray* array;
};
// does the given SEH exception look like a C++ exception?
// (compiler-specific).
static bool isCppException(const EXCEPTION_RECORD* er)
{
#ifdef _MSC_VER
// note: representation of 'msc' isn't specified, so use FOURCC
if(er->ExceptionCode != FOURCC(0xe0, 'm','s','c'))
return false;
// exception info = (magic, &thrown_Cpp_object, &XInfo)
if(er->NumberParameters != 3)
return false;
// MAGIC_NUMBER1 from exsup.inc
if(er->ExceptionInformation[0] != 0x19930520)
return false;
return true;
#else
# error "port"
#endif
}
// if <er> is not a C++ exception, return 0. otherwise, return a description
// of the exception type and cause (in English). uses static storage.
static const wchar_t* get_cpp_exception_description(const EXCEPTION_RECORD* er)
{
if(!isCppException(er))
return 0;
// see above for interpretation
const ULONG_PTR* const ei = er->ExceptionInformation;
// note: we can't share a __try below - the failure of
// one attempt must not abort the others.
// get std::type_info
char type_buf[100] = {'\0'};
const char* type_name = type_buf;
__try
{
const XInfo* xi = (XInfo*)ei[2];
const XTypeInfoArray* xta = xi->array;
const XTypeInfo* xti = xta->types[0];
const std::type_info* ti = xti->ti;
// strip "class " from start of string (clutter)
strcpy_s(type_buf, ARRAY_SIZE(type_buf), ti->name());
if(!strncmp(type_buf, "class ", 6))
type_name += 6;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
}
// std::exception.what()
char what[100] = {'\0'};
__try
{
std::exception* e = (std::exception*)ei[1];
strcpy_s(what, ARRAY_SIZE(what), e->what());
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
}
// we got meaningful data; format and return it.
if(type_name[0] != '\0' || what[0] != '\0')
{
swprintf(description_buf, ARRAY_SIZE(description_buf), L"%hs(\"%hs\")", type_name, what);
return description_buf;
}
// not a C++ exception; we can't say anything about it.
return 0;
}
// return a description of the exception type (in English).
// uses static storage.
static const wchar_t* get_SEH_exception_description(const EXCEPTION_RECORD* er)
{
const DWORD code = er->ExceptionCode;
const ULONG_PTR* ei = er->ExceptionInformation;
// special case for access violations: display type and address.
if(code == EXCEPTION_ACCESS_VIOLATION)
{
const wchar_t* op = (ei[0])? L"writing" : L"reading";
const wchar_t* fmt = L"Access violation %s 0x%08X";
swprintf(description_buf, ARRAY_SIZE(description_buf), translate(fmt), translate(op), ei[1]);
return description_buf;
}
// rationale: we don't use FormatMessage because it is unclear whether
// NTDLL's symbol table will always include English-language strings
// (we don't want crashlogs in foreign gobbledygook).
// it also adds unwanted formatting (e.g. {EXCEPTION} and trailing .).
switch(code)
{
// case EXCEPTION_ACCESS_VIOLATION: return L"Access violation";
case EXCEPTION_DATATYPE_MISALIGNMENT: return L"Datatype misalignment";
case EXCEPTION_BREAKPOINT: return L"Breakpoint";
case EXCEPTION_SINGLE_STEP: return L"Single step";
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return L"Array bounds exceeded";
case EXCEPTION_FLT_DENORMAL_OPERAND: return L"FPU denormal operand";
case EXCEPTION_FLT_DIVIDE_BY_ZERO: return L"FPU divide by zero";
case EXCEPTION_FLT_INEXACT_RESULT: return L"FPU inexact result";
case EXCEPTION_FLT_INVALID_OPERATION: return L"FPU invalid operation";
case EXCEPTION_FLT_OVERFLOW: return L"FPU overflow";
case EXCEPTION_FLT_STACK_CHECK: return L"FPU stack check";
case EXCEPTION_FLT_UNDERFLOW: return L"FPU underflow";
case EXCEPTION_INT_DIVIDE_BY_ZERO: return L"Integer divide by zero";
case EXCEPTION_INT_OVERFLOW: return L"Integer overflow";
case EXCEPTION_PRIV_INSTRUCTION: return L"Privileged instruction";
case EXCEPTION_IN_PAGE_ERROR: return L"In page error";
case EXCEPTION_ILLEGAL_INSTRUCTION: return L"Illegal instruction";
case EXCEPTION_NONCONTINUABLE_EXCEPTION: return L"Noncontinuable exception";
case EXCEPTION_STACK_OVERFLOW: return L"Stack overflow";
case EXCEPTION_INVALID_DISPOSITION: return L"Invalid disposition";
case EXCEPTION_GUARD_PAGE: return L"Guard page";
case EXCEPTION_INVALID_HANDLE: return L"Invalid handle";
}
// anything else => unknown; display its exception code.
// we don't punt to get_exception_description because anything
// we get called for will actually be a SEH exception.
swprintf(description_buf, ARRAY_SIZE(description_buf), L"Unknown (0x%08X)", code);
return description_buf;
}
// return a description of the exception <er> (in English).
// it is only valid until the next call, since static storage is used.
static const wchar_t* get_exception_description(const EXCEPTION_POINTERS* ep)
{
const EXCEPTION_RECORD* const er = ep->ExceptionRecord;
// note: more specific than SEH, so try it first.
const wchar_t* d = get_cpp_exception_description(er);
if(d)
return d;
return get_SEH_exception_description(er);
}
// return an indication of where the exception <er> occurred (lang. neutral).
// it is only valid until the next call, since static storage is used.
static const wchar_t* get_exception_locus(const EXCEPTION_POINTERS* ep)
{
// HACK: <ep> provides no useful information - ExceptionAddress always
// points to kernel32!RaiseException. we use debug_get_nth_caller to
// determine the real location.
void* func = debug_get_nth_caller(1, ep->ContextRecord);
// skip RaiseException
char func_name[DBG_SYMBOL_LEN];
char file[DBG_FILE_LEN] = {0};
int line = 0;
(void)debug_resolve_symbol(func, func_name, file, &line);
// note: file is the base path only (no drive letter), so there are
// no problems with wdbg_exception_filter's "%[^:]" format string.
// note: keep formatting in sync with wdbg_exception_filter, which
// extracts file/line for use with display_error.
static wchar_t locus[256];
swprintf(locus, ARRAY_SIZE(locus), L"%hs (%hs:%d)", func_name, file, line);
return locus;
}
// called* when an SEH exception was not caught by the app;
// provides detailed debugging information and exits.
// (via win.cpp!entry's __except or vectored_exception_handler; see below)
//
// note: keep memory allocs and locking to an absolute minimum, because
// they may deadlock the process!
//
// rationale:
// we want to replace the OS "program error" dialog box because
// it is not all too helpful in debugging. to that end, there are
// 5 ways to make sure unhandled SEH exceptions are caught:
// - via WaitForDebugEvent; the app is run from a separate debugger process.
// this complicates analysis, since the exception is in another
// address space. also, we are basically implementing a full-featured
// debugger - overkill.
// - by wrapping all threads in __try (necessary since the handler chain
// is in TLS). this is very difficult to guarantee.
// - with a vectored exception handler. this works across threads, but
// is never called when the process is being debugged
// (messing with the PEB flag doesn't help; root cause is the
// Win32 KiUserExceptionDispatcher implementation).
// worse, it's only available on WinXP (unacceptable).
// - by setting the per-process unhandled exception filter. as above,
// this works across threads and isn't called while a debugger is active;
// it is at least portable across Win32. unfortunately, some Win32 DLLs
// appear to register their own handlers, so this isn't reliable.
// - by hooking the exception dispatcher. this isn't future-proof.
//
// so, SNAFU. we compromise and register a regular __except filter at
// program entry point and add a vectored exception handler
// (if supported by the OS) to cover other threads.
// we steer clear of the process-wide unhandled exception filter,
// because it is not understood in what cases it can be overwritten;
// this precludes reliable use.
//
// since C++ exceptions are implemented via SEH, we can also catch those here;
// it's nicer than a global try{} and avoids duplicating this code.
// we can still get at the C++ information (std::exception.what()) by
// examining the internal exception data structures. these are
// compiler-specific, but haven't changed from VC5-VC7.1.
// alternatively, _set_se_translator could be used to translate all
// SEH exceptions to C++. this way is more reliable/documented, but has
// several drawbacks:
// - it wouldn't work at all in C programs,
// - a new fat exception class would have to be created to hold the
// SEH exception information (e.g. CONTEXT for a stack trace), and
// - this information would not be available for C++ exceptions.
extern void wdbg_write_minidump(EXCEPTION_POINTERS* ep);
LONG WINAPI wdbg_exception_filter(EXCEPTION_POINTERS* ep)
{
// note: we risk infinite recursion if someone raises an SEH exception
// from within this function. therefore, abort immediately if we've
// already been called; the first error is the most important, anyway.
static uintptr_t already_crashed = 0;
if(!CAS(&already_crashed, 0, 1))
return EXCEPTION_CONTINUE_SEARCH;
// someone is already holding the dbghelp lock - this is bad.
// we'll report this problem first and then try to display the
// exception info regardless (maybe dbghelp won't blow up).
if(is_locked())
DISPLAY_ERROR(L"Exception raised while critical section is held - may deadlock..");
// extract details from ExceptionRecord.
const wchar_t* description = get_exception_description(ep);
const wchar_t* locus = get_exception_locus (ep);
wchar_t fmt[50]; wchar_t func_name[DBG_SYMBOL_LEN] = L"?"; char file[DBG_FILE_LEN] = "?"; int line = 0;
swprintf(fmt, ARRAY_SIZE(fmt), L"%%%ds (%%%dh[^:]:%%d)", DBG_SYMBOL_LEN, DBG_FILE_LEN);
// bake in the string limits (future-proof)
(void)swscanf(locus, fmt, func_name, file, &line);
// don't care if all 3 fields were filled (they default to "?")
wchar_t buf[500];
const wchar_t* msg_fmt =
L"Much to our regret we must report the program has encountered an error.\r\n"
L"\r\n"
L"Please let us know at http://bugs.wildfiregames.com/ and attach the crashlog.txt and crashlog.dmp files.\r\n"
L"\r\n"
L"Details: unhandled exception (%s at %s)\r\n";
swprintf(buf, ARRAY_SIZE(buf), msg_fmt, description, locus);
int flags = 0;
if(ep->ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE)
flags = DE_NO_CONTINUE;
ErrorReaction er = display_error(buf, flags, 1, ep->ContextRecord, file, line);
debug_assert(er > 0);
wdbg_write_minidump(ep);
// invoke the Win32 default handler - it calls ExitProcess for
// most exception types.
return EXCEPTION_CONTINUE_SEARCH;
}
static LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ep)
{
// since we're called from the vectored handler chain,
// ignore exceptions from the main thread. this allows
// __try blocks to take charge; entry() catches all exceptions with a
// standard filter and relays them to wdbg_exception_filter.
if(main_thread_id == GetCurrentThreadId())
return EXCEPTION_CONTINUE_SEARCH;
return wdbg_exception_filter(ep);
}
static int wdbg_init(void)
{
// see decl
main_thread_id = GetCurrentThreadId();
// add vectored exception handler (if supported by the OS).
// see rationale above.
#if _WIN32_WINNT >= 0x0500 // this is how winbase.h tests for it
const HMODULE hKernel32Dll = LoadLibrary("kernel32.dll");
PVOID (WINAPI *pAddVectoredExceptionHandler)(IN ULONG FirstHandler, IN PVECTORED_EXCEPTION_HANDLER VectoredHandler);
*(void**)&pAddVectoredExceptionHandler = GetProcAddress(hKernel32Dll, "AddVectoredExceptionHandler");
FreeLibrary(hKernel32Dll);
// make sure the reference is released so BoundsChecker
// doesn't complain. it won't actually be unloaded anyway -
// there is at least one other reference.
if(pAddVectoredExceptionHandler)
pAddVectoredExceptionHandler(TRUE, vectored_exception_handler);
#endif
return 0;
}