// 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 #include #include "lib.h" #include "win_internal.h" #include "posix.h" #include "sysdep/cpu.h" #include "wdbg.h" #include "byte_order.h" // FOURCC #include "app_hooks.h" #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; } void debug_puts(const char* text) { OutputDebugStringA(text); } ////////////////////////////////////////////////////////////////////////////// // inform the debugger of the current thread's description, which it then // displays instead of just the thread handle. void wdbg_set_thread_name(const char* name) { // we pass information to the debugger via a special exception it // swallows. if not running under one, bail now to avoid // "first chance exception" warnings. if(!IsDebuggerPresent()) return; // presented by Jay Bazuzi (from the VC debugger team) at TechEd 1999. const struct ThreadNameInfo { DWORD type; const char* name; DWORD thread_id; // any valid ID or -1 for current thread DWORD flags; } info = { 0x1000, name, (DWORD)-1, 0 }; __try { RaiseException(0x406D1388, 0, sizeof(info)/sizeof(DWORD), (DWORD*)&info); } __except(EXCEPTION_EXECUTE_HANDLER) { // if we get here, apparently this hack is not longer supported. debug_warn("TODO: find alternative thread name implementation"); } } //----------------------------------------------------------------------------- // 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) { // note: if MMGR is enabled, crtdbg.h will not have been included. // in that case, we do nothing here - this interface is too basic to // control mmgr; use its API instead. #if !CONFIG_USE_MMGR && HAVE_VC_DEBUG_ALLOC uint flags = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); switch(what) { case DEBUG_HEAP_NONE: // note: do not set flags to zero because we might trash some // important flag value. flags &= ~(_CRTDBG_CHECK_ALWAYS_DF|_CRTDBG_DELAY_FREE_MEM_DF | _CRTDBG_ALLOC_MEM_DF|_CRTDBG_LEAK_CHECK_DF); 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); #else UNUSED2(what); #endif // HAVE_DEBUGALLOC } //----------------------------------------------------------------------------- // 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) { debug_set_thread_name("suspender"); 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"); return (void*)(intptr_t)-1; } // 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; } 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; WARN_ERR(pthread_create(&thread, 0, while_suspended_thread_func, ¶m)); 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; // IA32 limit; will need to update brk_enable_in_ctx if this changes. static const uint MAX_BREAKPOINTS = 4; // remove all breakpoints enabled by debug_set_break from . // called while target is suspended. static int brk_disable_all_in_ctx(BreakInfo* UNUSED(bi), CONTEXT* context) { context->Dr7 &= ~brk_all_local_enables; return 0; } // find a free register, set type according to and // mark it as enabled in . // called while target is suspended. static int brk_enable_in_ctx(BreakInfo* bi, CONTEXT* context) { uint reg; // index (0..3) of first free reg uint LE; // local enable bit for // 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: // store breakpoint address in debug register. DWORD addr = (DWORD)bi->addr; // .. note: treating Dr0..Dr3 as an array is unsafe due to // possible struct member padding. switch(reg) { case 0: context->Dr0 = addr; break; case 1: context->Dr1 = addr; break; case 2: context->Dr2 = addr; break; case 3: context->Dr3 = addr; break; default: UNREACHABLE; } // choose breakpoint settings: // .. 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("invalid type"); } // .. length (determine 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 } // update Debug Control register const uint shift = (16 + reg*4); // .. clear previous contents of this reg's field // (in case the previous user didn't do so on disabling). context->Dr7 &= ~(0xFu << shift); // .. write settings context->Dr7 |= ((len << 2)|rw) << shift; // .. mark as enabled context->Dr7 |= LE; brk_all_local_enables |= LE; 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 CPU_IA32 if(bi->want_all_disabled) ret = brk_disable_all_in_ctx(bi, &context); else ret = brk_enable_in_ctx (bi, &context); #else #error "port" #endif if(!SetThreadContext(hThread, &context)) { debug_warn("brk_do_request: SetThreadContext failed"); goto fail; } return 0; fail: return -1; } // arrange for a debug exception to be raised when is accessed // according to . // 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 // ////////////////////////////////////////////////////////////////////////////// // // 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) { #if MSC_VERSION // 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 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), ah_translate(fmt), ah_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 to receive 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 (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 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: 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 whether 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; } // return 1 if the pointer appears to be totally bogus, otherwise 0. // this check is not authoritative (the pointer may be "valid" but incorrect) // but can be used to filter out obviously wrong values in a portable manner. int debug_is_pointer_bogus(const void* p) { #if CPU_IA32 if(p < (void*)0x10000) return true; if(p >= (void*)(uintptr_t)0x80000000) return true; #endif // notes: // - we don't check alignment because nothing can be assumed about a // string pointer and we mustn't reject any actually valid pointers. // - nor do we bother checking the address against known stack/heap areas // because that doesn't cover everything (e.g. DLLs, VirtualAlloc). // - cannot use IsBadReadPtr because it accesses the mem // (false alarm for reserved address space). return false; } bool debug_is_code_ptr(void* p) { uintptr_t addr = (uintptr_t)p; // totally invalid pointer if(debug_is_pointer_bogus(p)) return false; // comes before load address static const HMODULE base = GetModuleHandle(0); if(addr < (uintptr_t)base) return false; return true; } static NT_TIB* get_tib() { NT_TIB* tib; __asm { mov eax, fs:[NT_TIB.Self] mov [tib], eax } return tib; } bool debug_is_stack_ptr(void* p) { uintptr_t addr = (uintptr_t)p; // totally invalid pointer if(debug_is_pointer_bogus(p)) return false; // not aligned if(addr % sizeof(void*)) return false; // out of bounds (note: IA32 stack grows downwards) NT_TIB* tib = get_tib(); if(!(tib->StackLimit < p && p < tib->StackBase)) return false; return true; }