mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-07-04 05:55:47 -07:00
move exception-specific stuff into wseh. it now grabs the entry point and wraps it in a __try block (but only for the main EXE). this works around issues with the previous SEH registration code (exceptions weren't caught in release mode when not running in debugger) This was SVN commit r5151.
451 lines
11 KiB
C++
451 lines
11 KiB
C++
/**
|
|
* =========================================================================
|
|
* File : wdbg.cpp
|
|
* Project : 0 A.D.
|
|
* Description : Win32 debug support code and exception handler.
|
|
* =========================================================================
|
|
*/
|
|
|
|
// license: GPL; see lib/license.txt
|
|
|
|
#include "precompiled.h"
|
|
#include "wdbg.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <typeinfo>
|
|
|
|
#include "lib/bits.h"
|
|
#include "lib/posix/posix_pthread.h"
|
|
#include "lib/app_hooks.h"
|
|
#include "lib/sysdep/cpu.h"
|
|
#include "win.h"
|
|
#include "wdbg_sym.h"
|
|
#include "wutil.h"
|
|
|
|
|
|
// protects the breakpoint helper thread.
|
|
static void lock()
|
|
{
|
|
win_lock(WDBG_CS);
|
|
}
|
|
|
|
static void unlock()
|
|
{
|
|
win_unlock(WDBG_CS);
|
|
}
|
|
|
|
|
|
static NT_TIB* get_tib()
|
|
{
|
|
#if CPU_IA32
|
|
NT_TIB* tib;
|
|
__asm
|
|
{
|
|
mov eax, fs:[NT_TIB.Self]
|
|
mov [tib], eax
|
|
}
|
|
return tib;
|
|
#endif
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// debug memory allocator
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// check heap integrity (independently of mmgr).
|
|
// errors are reported by the CRT or via debug_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
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// thread suspension
|
|
|
|
// suspend a thread, execute a user callback, revive the thread.
|
|
|
|
// to avoid deadlock, be VERY CAREFUL to avoid anything that may block,
|
|
// including locks taken by the OS (e.g. malloc, GetProcAddress).
|
|
typedef LibError (*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");
|
|
|
|
WhileSuspendedParam* param = (WhileSuspendedParam*)user_arg;
|
|
|
|
DWORD 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.
|
|
|
|
LibError ret = param->func(param->hThread, param->user_arg);
|
|
|
|
WARN_IF_FALSE(ResumeThread(param->hThread));
|
|
|
|
return (void*)(intptr_t)ret;
|
|
}
|
|
|
|
|
|
static LibError 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)
|
|
WARN_RETURN(ERR::FAIL);
|
|
|
|
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 (LibError)(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.
|
|
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;
|
|
};
|
|
|
|
static BreakInfo brk_info;
|
|
|
|
// Local Enable bits of all registers we enabled (used when restoring all).
|
|
static DWORD brk_all_local_enables;
|
|
|
|
// IA-32 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 <context>.
|
|
// called while target is suspended.
|
|
static LibError brk_disable_all_in_ctx(BreakInfo* UNUSED(bi), CONTEXT* context)
|
|
{
|
|
context->Dr7 &= ~brk_all_local_enables;
|
|
return INFO::OK;
|
|
}
|
|
|
|
|
|
// find a free register, set type according to <bi> and
|
|
// mark it as enabled in <context>.
|
|
// called while target is suspended.
|
|
static LibError brk_enable_in_ctx(BreakInfo* bi, CONTEXT* context)
|
|
{
|
|
uint 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;
|
|
}
|
|
WARN_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;
|
|
NODEFAULT;
|
|
}
|
|
|
|
// 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 INFO::OK;
|
|
}
|
|
|
|
|
|
// carry out the request stored in the BreakInfo* parameter.
|
|
// called while target is suspended.
|
|
static LibError brk_do_request(HANDLE hThread, void* arg)
|
|
{
|
|
LibError ret;
|
|
BreakInfo* bi = (BreakInfo*)arg;
|
|
|
|
CONTEXT context;
|
|
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
|
if(!GetThreadContext(hThread, &context))
|
|
WARN_RETURN(ERR::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))
|
|
WARN_RETURN(ERR::FAIL);
|
|
|
|
RETURN_ERR(ret);
|
|
return INFO::OK;
|
|
}
|
|
|
|
|
|
// 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.
|
|
LibError debug_set_break(void* p, DbgBreakType type)
|
|
{
|
|
lock();
|
|
|
|
brk_info.addr = (uintptr_t)p;
|
|
brk_info.type = type;
|
|
LibError 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.
|
|
LibError debug_remove_all_breaks()
|
|
{
|
|
lock();
|
|
|
|
brk_info.want_all_disabled = true;
|
|
LibError ret = call_while_suspended(brk_do_request, &brk_info);
|
|
brk_info.want_all_disabled = false;
|
|
|
|
unlock();
|
|
return ret;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// 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;
|
|
}
|
|
|
|
|
|
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: IA-32 stack grows downwards)
|
|
NT_TIB* tib = get_tib();
|
|
if(!(tib->StackLimit < p && p < tib->StackBase))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
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, the debugger didn't handle the exception.
|
|
debug_warn("thread name hack doesn't work under this debugger");
|
|
}
|
|
}
|