You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
535 lines
16 KiB
535 lines
16 KiB
/* |
|
crashhandler.c - advanced crashhandler |
|
Copyright (C) 2016 Mittorn |
|
|
|
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 3 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. |
|
*/ |
|
#ifndef _GNU_SOURCE |
|
#define _GNU_SOURCE |
|
#endif |
|
|
|
#include "common.h" |
|
|
|
/* |
|
================ |
|
Sys_Crash |
|
|
|
Crash handler, called from system |
|
================ |
|
*/ |
|
#if XASH_WIN32 |
|
|
|
#if DBGHELP |
|
|
|
#pragma comment( lib, "dbghelp" ) |
|
|
|
#include <winnt.h> |
|
#include <dbghelp.h> |
|
#include <psapi.h> |
|
#include <time.h> |
|
|
|
#ifndef XASH_SDL |
|
typedef ULONG_PTR DWORD_PTR, *PDWORD_PTR; |
|
#endif |
|
|
|
static int Sys_ModuleName( HANDLE process, char *name, void *address, int len ) |
|
{ |
|
DWORD_PTR baseAddress = 0; |
|
static HMODULE *moduleArray; |
|
static unsigned int moduleCount; |
|
LPBYTE moduleArrayBytes; |
|
DWORD bytesRequired; |
|
int i; |
|
|
|
if( len < 3 ) |
|
return 0; |
|
|
|
if( !moduleArray && EnumProcessModules( process, NULL, 0, &bytesRequired ) ) |
|
{ |
|
if ( bytesRequired ) |
|
{ |
|
moduleArrayBytes = (LPBYTE)LocalAlloc( LPTR, bytesRequired ); |
|
|
|
if( moduleArrayBytes && EnumProcessModules( process, (HMODULE *)moduleArrayBytes, bytesRequired, &bytesRequired ) ) |
|
{ |
|
moduleCount = bytesRequired / sizeof( HMODULE ); |
|
moduleArray = (HMODULE *)moduleArrayBytes; |
|
} |
|
} |
|
} |
|
|
|
for( i = 0; i < moduleCount; i++ ) |
|
{ |
|
MODULEINFO info; |
|
GetModuleInformation( process, moduleArray[i], &info, sizeof(MODULEINFO) ); |
|
|
|
if( ( address > info.lpBaseOfDll ) && |
|
( (DWORD64)address < (DWORD64)info.lpBaseOfDll + (DWORD64)info.SizeOfImage ) ) |
|
return GetModuleBaseName( process, moduleArray[i], name, len ); |
|
} |
|
return Q_snprintf( name, len, "???" ); |
|
} |
|
|
|
static void Sys_StackTrace( PEXCEPTION_POINTERS pInfo ) |
|
{ |
|
char message[1024]; |
|
int len = 0; |
|
size_t i; |
|
HANDLE process = GetCurrentProcess(); |
|
HANDLE thread = GetCurrentThread(); |
|
IMAGEHLP_LINE64 line; |
|
DWORD dline = 0; |
|
DWORD options; |
|
CONTEXT context; |
|
STACKFRAME64 stackframe; |
|
DWORD image; |
|
|
|
memcpy( &context, pInfo->ContextRecord, sizeof( CONTEXT )); |
|
|
|
options = SymGetOptions(); |
|
options |= SYMOPT_DEBUG; |
|
options |= SYMOPT_LOAD_LINES; |
|
SymSetOptions( options ); |
|
|
|
SymInitialize( process, NULL, TRUE ); |
|
|
|
ZeroMemory( &stackframe, sizeof( STACKFRAME64 )); |
|
|
|
#ifdef _M_IX86 |
|
image = IMAGE_FILE_MACHINE_I386; |
|
stackframe.AddrPC.Offset = context.Eip; |
|
stackframe.AddrPC.Mode = AddrModeFlat; |
|
stackframe.AddrFrame.Offset = context.Ebp; |
|
stackframe.AddrFrame.Mode = AddrModeFlat; |
|
stackframe.AddrStack.Offset = context.Esp; |
|
stackframe.AddrStack.Mode = AddrModeFlat; |
|
#elif _M_X64 |
|
image = IMAGE_FILE_MACHINE_AMD64; |
|
stackframe.AddrPC.Offset = context.Rip; |
|
stackframe.AddrPC.Mode = AddrModeFlat; |
|
stackframe.AddrFrame.Offset = context.Rsp; |
|
stackframe.AddrFrame.Mode = AddrModeFlat; |
|
stackframe.AddrStack.Offset = context.Rsp; |
|
stackframe.AddrStack.Mode = AddrModeFlat; |
|
#elif _M_IA64 |
|
image = IMAGE_FILE_MACHINE_IA64; |
|
stackframe.AddrPC.Offset = context.StIIP; |
|
stackframe.AddrPC.Mode = AddrModeFlat; |
|
stackframe.AddrFrame.Offset = context.IntSp; |
|
stackframe.AddrFrame.Mode = AddrModeFlat; |
|
stackframe.AddrBStore.Offset = context.RsBSP; |
|
stackframe.AddrBStore.Mode = AddrModeFlat; |
|
stackframe.AddrStack.Offset = context.IntSp; |
|
stackframe.AddrStack.Mode = AddrModeFlat; |
|
#elif |
|
#error |
|
#endif |
|
len += Q_snprintf( message + len, 1024 - len, "Sys_Crash: address %p, code %p\n", |
|
pInfo->ExceptionRecord->ExceptionAddress, (void*)pInfo->ExceptionRecord->ExceptionCode ); |
|
if( SymGetLineFromAddr64( process, (DWORD64)pInfo->ExceptionRecord->ExceptionAddress, &dline, &line ) ) |
|
{ |
|
len += Q_snprintf(message + len, 1024 - len, "Exception: %s:%d:%d\n", |
|
(char*)line.FileName, (int)line.LineNumber, (int)dline); |
|
} |
|
if( SymGetLineFromAddr64( process, stackframe.AddrPC.Offset, &dline, &line ) ) |
|
{ |
|
len += Q_snprintf(message + len, 1024 - len,"PC: %s:%d:%d\n", |
|
(char*)line.FileName, (int)line.LineNumber, (int)dline); |
|
} |
|
if( SymGetLineFromAddr64( process, stackframe.AddrFrame.Offset, &dline, &line ) ) |
|
{ |
|
len += Q_snprintf(message + len, 1024 - len,"Frame: %s:%d:%d\n", |
|
(char*)line.FileName, (int)line.LineNumber, (int)dline); |
|
} |
|
for( i = 0; i < 25; i++ ) |
|
{ |
|
char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)]; |
|
PSYMBOL_INFO symbol = (PSYMBOL_INFO)buffer; |
|
BOOL result = StackWalk64( |
|
image, process, thread, |
|
&stackframe, &context, NULL, |
|
SymFunctionTableAccess64, SymGetModuleBase64, NULL); |
|
DWORD64 displacement = 0; |
|
|
|
if( !result ) |
|
break; |
|
|
|
symbol->SizeOfStruct = sizeof(SYMBOL_INFO); |
|
symbol->MaxNameLen = MAX_SYM_NAME; |
|
|
|
len += Q_snprintf( message + len, 1024 - len, "% 2d %p", |
|
i, (void*)stackframe.AddrPC.Offset ); |
|
if( SymFromAddr( process, stackframe.AddrPC.Offset, &displacement, symbol ) ) |
|
{ |
|
len += Q_snprintf( message + len, 1024 - len, " %s ", symbol->Name ); |
|
} |
|
if( SymGetLineFromAddr64( process, stackframe.AddrPC.Offset, &dline, &line ) ) |
|
{ |
|
len += Q_snprintf(message + len, 1024 - len,"(%s:%d:%d) ", |
|
(char*)line.FileName, (int)line.LineNumber, (int)dline); |
|
} |
|
len += Q_snprintf( message + len, 1024 - len, "("); |
|
len += Sys_ModuleName( process, message + len, (void*)stackframe.AddrPC.Offset, 1024 - len ); |
|
len += Q_snprintf( message + len, 1024 - len, ")\n"); |
|
} |
|
|
|
#if XASH_SDL == 2 |
|
if( host.type != HOST_DEDICATED ) // let system to restart server automaticly |
|
SDL_ShowSimpleMessageBox( SDL_MESSAGEBOX_ERROR, "Sys_Crash", message, host.hWnd ); |
|
#endif |
|
|
|
Sys_PrintLog( message ); |
|
|
|
SymCleanup( process ); |
|
} |
|
|
|
static void Sys_GetProcessName( char *processName, size_t bufferSize ) |
|
{ |
|
char fullpath[MAX_PATH]; |
|
|
|
GetModuleBaseName( GetCurrentProcess(), NULL, fullpath, sizeof( fullpath ) - 1 ); |
|
COM_FileBase( fullpath, processName, bufferSize ); |
|
} |
|
|
|
static void Sys_GetMinidumpFileName( const char *processName, char *mdmpFileName, size_t bufferSize ) |
|
{ |
|
time_t currentUtcTime = time( NULL ); |
|
struct tm *currentLocalTime = localtime( ¤tUtcTime ); |
|
|
|
Q_snprintf( mdmpFileName, bufferSize, "%s_%s_crash_%d%.2d%.2d_%.2d%.2d%.2d.mdmp", |
|
processName, |
|
Q_buildcommit(), |
|
currentLocalTime->tm_year + 1900, |
|
currentLocalTime->tm_mon + 1, |
|
currentLocalTime->tm_mday, |
|
currentLocalTime->tm_hour, |
|
currentLocalTime->tm_min, |
|
currentLocalTime->tm_sec); |
|
} |
|
|
|
static qboolean Sys_WriteMinidump(PEXCEPTION_POINTERS exceptionInfo, MINIDUMP_TYPE minidumpType) |
|
{ |
|
HRESULT errorCode; |
|
string processName; |
|
string mdmpFileName; |
|
MINIDUMP_EXCEPTION_INFORMATION minidumpInfo; |
|
|
|
Sys_GetProcessName( processName, sizeof( processName )); |
|
Sys_GetMinidumpFileName( processName, mdmpFileName, sizeof( mdmpFileName )); |
|
|
|
SetLastError( NOERROR ); |
|
HANDLE fileHandle = CreateFile( |
|
mdmpFileName, GENERIC_WRITE, FILE_SHARE_WRITE, |
|
NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); |
|
|
|
errorCode = HRESULT_FROM_WIN32( GetLastError( )); |
|
if( !SUCCEEDED( errorCode )) { |
|
CloseHandle( fileHandle ); |
|
return false; |
|
} |
|
|
|
minidumpInfo.ThreadId = GetCurrentThreadId(); |
|
minidumpInfo.ExceptionPointers = exceptionInfo; |
|
minidumpInfo.ClientPointers = FALSE; |
|
|
|
qboolean status = MiniDumpWriteDump( |
|
GetCurrentProcess(), GetCurrentProcessId(), fileHandle, |
|
minidumpType, &minidumpInfo, NULL, NULL); |
|
|
|
CloseHandle( fileHandle ); |
|
return status; |
|
} |
|
|
|
#endif /* DBGHELP */ |
|
|
|
LPTOP_LEVEL_EXCEPTION_FILTER oldFilter; |
|
static long _stdcall Sys_Crash( PEXCEPTION_POINTERS pInfo ) |
|
{ |
|
// save config |
|
if( host.status != HOST_CRASHED ) |
|
{ |
|
// check to avoid recursive call |
|
host.crashed = true; |
|
|
|
#if DBGHELP |
|
Sys_StackTrace( pInfo ); |
|
#else |
|
Sys_Warn( "Sys_Crash: call %p at address %p", pInfo->ExceptionRecord->ExceptionAddress, pInfo->ExceptionRecord->ExceptionCode ); |
|
#endif |
|
|
|
if( host.type == HOST_NORMAL ) |
|
CL_Crashed(); // tell client about crash |
|
else host.status = HOST_CRASHED; |
|
|
|
#if DBGHELP |
|
if( Sys_CheckParm( "-minidumps" )) |
|
{ |
|
int minidumpFlags = ( |
|
MiniDumpWithDataSegs | |
|
MiniDumpWithCodeSegs | |
|
MiniDumpWithHandleData | |
|
MiniDumpWithFullMemory | |
|
MiniDumpWithFullMemoryInfo | |
|
MiniDumpWithIndirectlyReferencedMemory | |
|
MiniDumpWithThreadInfo | |
|
MiniDumpWithModuleHeaders); |
|
|
|
if( !Sys_WriteMinidump( pInfo, (MINIDUMP_TYPE)minidumpFlags )) { |
|
// fallback method, create minidump with minimal info in it |
|
Sys_WriteMinidump( pInfo, MiniDumpWithDataSegs ); |
|
} |
|
} |
|
#endif |
|
|
|
if( host_developer.value <= 0 ) |
|
{ |
|
// no reason to call debugger in release build - just exit |
|
Sys_Quit(); |
|
return EXCEPTION_CONTINUE_EXECUTION; |
|
} |
|
|
|
// all other states keep unchanged to let debugger find bug |
|
Sys_DestroyConsole(); |
|
} |
|
|
|
if( oldFilter ) |
|
return oldFilter( pInfo ); |
|
return EXCEPTION_CONTINUE_EXECUTION; |
|
} |
|
|
|
void Sys_SetupCrashHandler( void ) |
|
{ |
|
SetErrorMode( SEM_FAILCRITICALERRORS ); // no abort/retry/fail errors |
|
oldFilter = SetUnhandledExceptionFilter( Sys_Crash ); |
|
host.hInst = GetModuleHandle( NULL ); |
|
} |
|
|
|
void Sys_RestoreCrashHandler( void ) |
|
{ |
|
// restore filter |
|
if( oldFilter ) SetUnhandledExceptionFilter( oldFilter ); |
|
} |
|
|
|
#elif XASH_FREEBSD || XASH_NETBSD || XASH_OPENBSD || XASH_ANDROID || XASH_LINUX |
|
// Posix signal handler |
|
#ifndef XASH_OPENBSD |
|
#include <ucontext.h> |
|
#endif |
|
#include <signal.h> |
|
#include <sys/mman.h> |
|
#include "library.h" |
|
|
|
#define STACK_BACKTRACE_STR "Stack backtrace:\n" |
|
#define STACK_DUMP_STR "Stack dump:\n" |
|
|
|
#define STACK_BACKTRACE_STR_LEN ( sizeof( STACK_BACKTRACE_STR ) - 1 ) |
|
#define STACK_DUMP_STR_LEN ( sizeof( STACK_DUMP_STR ) - 1 ) |
|
#define ALIGN( x, y ) (((uintptr_t) ( x ) + (( y ) - 1 )) & ~(( y ) - 1 )) |
|
|
|
static struct sigaction oldFilter; |
|
|
|
static int Sys_PrintFrame( char *buf, int len, int i, void *addr ) |
|
{ |
|
Dl_info dlinfo; |
|
if( len <= 0 ) |
|
return 0; // overflow |
|
|
|
if( dladdr( addr, &dlinfo )) |
|
{ |
|
if( dlinfo.dli_sname ) |
|
return Q_snprintf( buf, len, "%2d: %p <%s+%lu> (%s)\n", i, addr, dlinfo.dli_sname, |
|
(unsigned long)addr - (unsigned long)dlinfo.dli_saddr, dlinfo.dli_fname ); // print symbol, module and address |
|
|
|
return Q_snprintf( buf, len, "%2d: %p (%s)\n", i, addr, dlinfo.dli_fname ); // print module and address |
|
} |
|
|
|
return Q_snprintf( buf, len, "%2d: %p\n", i, addr ); // print only address |
|
} |
|
|
|
static void Sys_Crash( int signal, siginfo_t *si, void *context) |
|
{ |
|
void *pc = NULL, **bp = NULL, **sp = NULL; // this must be set for every OS! |
|
char message[8192]; |
|
int len, logfd, i = 0; |
|
|
|
#if XASH_OPENBSD |
|
struct sigcontext *ucontext = (struct sigcontext*)context; |
|
#else |
|
ucontext_t *ucontext = (ucontext_t*)context; |
|
#endif |
|
|
|
#if XASH_AMD64 |
|
#if XASH_FREEBSD |
|
pc = (void*)ucontext->uc_mcontext.mc_rip; |
|
bp = (void**)ucontext->uc_mcontext.mc_rbp; |
|
sp = (void**)ucontext->uc_mcontext.mc_rsp; |
|
#elif XASH_NETBSD |
|
pc = (void*)ucontext->uc_mcontext.__gregs[_REG_RIP]; |
|
bp = (void**)ucontext->uc_mcontext.__gregs[_REG_RBP]; |
|
sp = (void**)ucontext->uc_mcontext.__gregs[_REG_RSP]; |
|
#elif XASH_OPENBSD |
|
pc = (void*)ucontext->sc_rip; |
|
bp = (void**)ucontext->sc_rbp; |
|
sp = (void**)ucontext->sc_rsp; |
|
#else |
|
pc = (void*)ucontext->uc_mcontext.gregs[REG_RIP]; |
|
bp = (void**)ucontext->uc_mcontext.gregs[REG_RBP]; |
|
sp = (void**)ucontext->uc_mcontext.gregs[REG_RSP]; |
|
#endif |
|
#elif XASH_X86 |
|
#if XASH_FREEBSD |
|
pc = (void*)ucontext->uc_mcontext.mc_eip; |
|
bp = (void**)ucontext->uc_mcontext.mc_ebp; |
|
sp = (void**)ucontext->uc_mcontext.mc_esp; |
|
#elif XASH_NETBSD |
|
pc = (void*)ucontext->uc_mcontext.__gregs[_REG_EIP]; |
|
bp = (void**)ucontext->uc_mcontext.__gregs[_REG_EBP]; |
|
sp = (void**)ucontext->uc_mcontext.__gregs[_REG_ESP]; |
|
#elif XASH_OPENBSD |
|
pc = (void*)ucontext->sc_eip; |
|
bp = (void**)ucontext->sc_ebp; |
|
sp = (void**)ucontext->sc_esp; |
|
#else |
|
pc = (void*)ucontext->uc_mcontext.gregs[REG_EIP]; |
|
bp = (void**)ucontext->uc_mcontext.gregs[REG_EBP]; |
|
sp = (void**)ucontext->uc_mcontext.gregs[REG_ESP]; |
|
#endif |
|
#elif XASH_ARM && XASH_64BIT |
|
pc = (void*)ucontext->uc_mcontext.pc; |
|
bp = (void*)ucontext->uc_mcontext.regs[29]; |
|
sp = (void*)ucontext->uc_mcontext.sp; |
|
#elif XASH_ARM |
|
pc = (void*)ucontext->uc_mcontext.arm_pc; |
|
bp = (void*)ucontext->uc_mcontext.arm_fp; |
|
sp = (void*)ucontext->uc_mcontext.arm_sp; |
|
#endif |
|
|
|
// safe actions first, stack and memory may be corrupted |
|
len = Q_snprintf( message, sizeof( message ), "Ver: " XASH_ENGINE_NAME " " XASH_VERSION " (build %i-%s, %s-%s)\n", |
|
Q_buildnum(), Q_buildcommit(), Q_buildos(), Q_buildarch() ); |
|
|
|
#if !XASH_FREEBSD && !XASH_NETBSD && !XASH_OPENBSD |
|
len += Q_snprintf( message + len, sizeof( message ) - len, "Crash: signal %d errno %d with code %d at %p %p\n", signal, si->si_errno, si->si_code, si->si_addr, si->si_ptr ); |
|
#else |
|
len += Q_snprintf( message + len, sizeof( message ) - len, "Crash: signal %d errno %d with code %d at %p\n", signal, si->si_errno, si->si_code, si->si_addr ); |
|
#endif |
|
|
|
write( STDERR_FILENO, message, len ); |
|
|
|
// flush buffers before writing directly to descriptors |
|
fflush( stdout ); |
|
fflush( stderr ); |
|
|
|
// now get log fd and write trace directly to log |
|
logfd = Sys_LogFileNo(); |
|
write( logfd, message, len ); |
|
|
|
if( pc && bp && sp ) |
|
{ |
|
size_t pagesize = sysconf( _SC_PAGESIZE ); |
|
|
|
// try to print backtrace |
|
write( STDERR_FILENO, STACK_BACKTRACE_STR, STACK_BACKTRACE_STR_LEN ); |
|
write( logfd, STACK_BACKTRACE_STR, STACK_BACKTRACE_STR_LEN ); |
|
Q_strncpy( message + len, STACK_BACKTRACE_STR, sizeof( message ) - len ); |
|
len += STACK_BACKTRACE_STR_LEN; |
|
|
|
// false on success, true on failure |
|
#define try_allow_read(pointer, pagesize) \ |
|
((mprotect( (char *)ALIGN( (pointer), (pagesize) ), (pagesize), PROT_READ | PROT_WRITE | PROT_EXEC ) == -1) && \ |
|
( mprotect( (char *)ALIGN( (pointer), (pagesize) ), (pagesize), PROT_READ | PROT_EXEC ) == -1) && \ |
|
( mprotect( (char *)ALIGN( (pointer), (pagesize) ), (pagesize), PROT_READ | PROT_WRITE ) == -1) && \ |
|
( mprotect( (char *)ALIGN( (pointer), (pagesize) ), (pagesize), PROT_READ ) == -1)) |
|
|
|
do |
|
{ |
|
int line = Sys_PrintFrame( message + len, sizeof( message ) - len, ++i, pc); |
|
write( STDERR_FILENO, message + len, line ); |
|
write( logfd, message + len, line ); |
|
len += line; |
|
//if( !dladdr(bp,0) ) break; // only when bp is in module |
|
if( try_allow_read( bp, pagesize ) ) |
|
break; |
|
if( try_allow_read( bp[0], pagesize ) ) |
|
break; |
|
pc = bp[1]; |
|
bp = (void**)bp[0]; |
|
} |
|
while( bp && i < 128 ); |
|
|
|
// try to print stack |
|
write( STDERR_FILENO, STACK_DUMP_STR, STACK_DUMP_STR_LEN ); |
|
write( logfd, STACK_DUMP_STR, STACK_DUMP_STR_LEN ); |
|
Q_strncpy( message + len, STACK_DUMP_STR, sizeof( message ) - len ); |
|
len += STACK_DUMP_STR_LEN; |
|
|
|
if( !try_allow_read( sp, pagesize ) ) |
|
{ |
|
for( i = 0; i < 32; i++ ) |
|
{ |
|
int line = Sys_PrintFrame( message + len, sizeof( message ) - len, i, sp[i] ); |
|
write( STDERR_FILENO, message + len, line ); |
|
write( logfd, message + len, line ); |
|
len += line; |
|
} |
|
} |
|
|
|
#undef try_allow_read |
|
} |
|
|
|
// put MessageBox as Sys_Error |
|
Msg( "%s\n", message ); |
|
#ifdef XASH_SDL |
|
SDL_SetWindowGrab( host.hWnd, SDL_FALSE ); |
|
#endif |
|
host.crashed = true; |
|
Platform_MessageBox( "Xash Error", message, false ); |
|
|
|
// log saved, now we can try to save configs and close log correctly, it may crash |
|
if( host.type == HOST_NORMAL ) |
|
CL_Crashed(); |
|
host.status = HOST_CRASHED; |
|
|
|
|
|
Sys_Quit(); |
|
} |
|
|
|
void Sys_SetupCrashHandler( void ) |
|
{ |
|
struct sigaction act = { 0 }; |
|
act.sa_sigaction = Sys_Crash; |
|
act.sa_flags = SA_SIGINFO | SA_ONSTACK; |
|
sigaction( SIGSEGV, &act, &oldFilter ); |
|
sigaction( SIGABRT, &act, &oldFilter ); |
|
sigaction( SIGBUS, &act, &oldFilter ); |
|
sigaction( SIGILL, &act, &oldFilter ); |
|
} |
|
|
|
void Sys_RestoreCrashHandler( void ) |
|
{ |
|
sigaction( SIGSEGV, &oldFilter, NULL ); |
|
sigaction( SIGABRT, &oldFilter, NULL ); |
|
sigaction( SIGBUS, &oldFilter, NULL ); |
|
sigaction( SIGILL, &oldFilter, NULL ); |
|
} |
|
|
|
#else |
|
|
|
void Sys_SetupCrashHandler( void ) |
|
{ |
|
// stub |
|
} |
|
|
|
void Sys_RestoreCrashHandler( void ) |
|
{ |
|
// stub |
|
} |
|
#endif
|
|
|