From ddffb4193a222c73c9b65f9515644da3b9bd2545 Mon Sep 17 00:00:00 2001 From: Magnus Norddahl Date: Fri, 18 Apr 2025 14:43:29 +0200 Subject: [PATCH] Dbgeng didn't work - let's try do it ourselves --- src/common/platform/win32/i_crash.cpp | 519 ++++++++++++++++++++++++-- 1 file changed, 481 insertions(+), 38 deletions(-) diff --git a/src/common/platform/win32/i_crash.cpp b/src/common/platform/win32/i_crash.cpp index a0e3b9f2c..323118537 100644 --- a/src/common/platform/win32/i_crash.cpp +++ b/src/common/platform/win32/i_crash.cpp @@ -31,9 +31,22 @@ #ifdef _MSC_VER #include #include -#include +#if defined(USE_DBGENG) +#include #pragma comment(lib, "dbgeng.lib") +#else +#pragma comment(lib, "dbghelp.lib") +#endif + +struct StackFrameList +{ + enum { max_frames = 16 }; + int frame_count; + void* frames[max_frames]; +}; + +enum { UserStackFrameListStream = LastReservedStream + 1 }; class CrashReporter { @@ -53,6 +66,7 @@ private: int thread_id; PEXCEPTION_POINTERS exception_pointers; unsigned int exception_code; + StackFrameList stack_frames; }; void load_dbg_help(); @@ -70,6 +84,8 @@ private: static void on_se_unhandled_exception(unsigned int exception_code, PEXCEPTION_POINTERS exception_pointers); static LONG WINAPI on_win32_unhandled_exception(PEXCEPTION_POINTERS exception_pointers); + static int CaptureStackTrace(int max_frames, void** out_frames); + typedef BOOL(WINAPI* MiniDumpWriteDumpPointer)(HANDLE, DWORD, HANDLE, MINIDUMP_TYPE, CONST PMINIDUMP_EXCEPTION_INFORMATION, CONST PMINIDUMP_USER_STREAM_INFORMATION, CONST PMINIDUMP_CALLBACK_INFORMATION); HMODULE module_dbghlp; static MiniDumpWriteDumpPointer func_MiniDumpWriteDump; @@ -177,6 +193,15 @@ void CrashReporter::create_dump(DumpParams* dump_params, bool launch_uploader) if (file == INVALID_HANDLE_VALUE) return; + MINIDUMP_USER_STREAM userStream = {}; + userStream.Type = UserStackFrameListStream; + userStream.BufferSize = sizeof(StackFrameList); + userStream.Buffer = &dump_params->stack_frames; + + MINIDUMP_USER_STREAM_INFORMATION userStreamList = {}; + userStreamList.UserStreamCount = 1; + userStreamList.UserStreamArray = &userStream; + MINIDUMP_EXCEPTION_INFORMATION info; info.ThreadId = dump_params->thread_id; info.ExceptionPointers = dump_params->exception_pointers; @@ -186,7 +211,7 @@ void CrashReporter::create_dump(DumpParams* dump_params, bool launch_uploader) MiniDumpWithProcessThreadData | MiniDumpWithFullMemoryInfo | MiniDumpWithThreadInfo); - func_MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), file, type, &info, 0, 0); + func_MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), file, type, &info, &userStreamList, 0); CloseHandle(file); WCHAR log_filename[1024]; @@ -265,6 +290,56 @@ void CrashReporter::on_sigabort(int) invoke(); } +int CrashReporter::CaptureStackTrace(int max_frames, void** out_frames) +{ + memset(out_frames, 0, sizeof(void*) * max_frames); + +#ifdef _WIN64 + // RtlCaptureStackBackTrace doesn't support RtlAddFunctionTable.. + + CONTEXT context; + RtlCaptureContext(&context); + + UNWIND_HISTORY_TABLE history; + memset(&history, 0, sizeof(UNWIND_HISTORY_TABLE)); + + ULONG64 establisherframe = 0; + PVOID handlerdata = nullptr; + + int frame; + for (frame = 0; frame < max_frames; frame++) + { + ULONG64 imagebase; + PRUNTIME_FUNCTION rtfunc = RtlLookupFunctionEntry(context.Rip, &imagebase, &history); + + KNONVOLATILE_CONTEXT_POINTERS nvcontext; + memset(&nvcontext, 0, sizeof(KNONVOLATILE_CONTEXT_POINTERS)); + if (!rtfunc) + { + // Leaf function + context.Rip = (ULONG64)(*(PULONG64)context.Rsp); + context.Rsp += 8; + } + else + { + RtlVirtualUnwind(UNW_FLAG_NHANDLER, imagebase, context.Rip, rtfunc, &context, &handlerdata, &establisherframe, &nvcontext); + } + + if (!context.Rip) + break; + + out_frames[frame] = (void*)context.Rip; + } + return frame; + +#elif defined(WIN32) + // JIT isn't supported here, so just do nothing. + return 0;//return RtlCaptureStackBackTrace(0, min(max_frames, 32), out_frames, nullptr); +#else + return backtrace(out_frames, max_frames); +#endif +} + void CrashReporter::on_se_unhandled_exception(unsigned int exception_code, PEXCEPTION_POINTERS exception_pointers) { // Ignore those bloody breakpoints! @@ -276,6 +351,7 @@ void CrashReporter::on_se_unhandled_exception(unsigned int exception_code, PEXCE dump_params.thread_id = GetCurrentThreadId(); dump_params.exception_pointers = exception_pointers; dump_params.exception_code = exception_code; + dump_params.stack_frames.frame_count = CaptureStackTrace(StackFrameList::max_frames, dump_params.stack_frames.frames); // Ensure we only get a dump of the first thread crashing - let other threads block here. std::unique_lock mutex_lock(mutex); @@ -296,6 +372,7 @@ LONG CrashReporter::on_win32_unhandled_exception(PEXCEPTION_POINTERS exception_p dump_params.thread_id = GetCurrentThreadId(); dump_params.exception_pointers = exception_pointers; dump_params.exception_code = 0; + dump_params.stack_frames.frame_count = CaptureStackTrace(StackFrameList::max_frames, dump_params.stack_frames.frames); // Ensure we only get a dump of the first thread crashing - let other threads block here. std::unique_lock mutex_lock(mutex); @@ -409,42 +486,6 @@ const BYTE CrashReporter::patch_bytes[5] = { 0x33, 0xC0, 0xC2, 0x04, 0x00 }; #endif -template -class DbgPtr -{ -public: - DbgPtr() { Ptr = nullptr; } - DbgPtr(const DbgPtr& other) { Ptr = other.Ptr; if (Ptr) Ptr->AddRef(); } - DbgPtr(DbgPtr&& move) { Ptr = move.Ptr; move.Ptr = nullptr; } - ~DbgPtr() { reset(); } - - void reset() { if (Ptr) Ptr->Release(); Ptr = nullptr; } - T* get() { return Ptr; } - - static IID GetIID() { return __uuidof(T); } - - void** InitPtr() { return (void**)TypedInitPtr(); } - T** TypedInitPtr() { reset(); return &Ptr; } - - DbgPtr& operator=(const DbgPtr& other) - { - if (this != &other) - { - if (Ptr) - Ptr->Release(); - Ptr = other.Ptr; - if (Ptr) - Ptr->AddRef(); - } - return *this; - } - - operator T* () const { return Ptr; } - T* operator ->() const { return Ptr; } - - T* Ptr; -}; - static std::wstring GetExeFilename() { wchar_t buffer[1024] = {}; @@ -481,6 +522,44 @@ static bool IsPdbFileMissing() return false; } +#if defined(USE_DBGENG) + +template +class DbgPtr +{ +public: + DbgPtr() { Ptr = nullptr; } + DbgPtr(const DbgPtr& other) { Ptr = other.Ptr; if (Ptr) Ptr->AddRef(); } + DbgPtr(DbgPtr&& move) { Ptr = move.Ptr; move.Ptr = nullptr; } + ~DbgPtr() { reset(); } + + void reset() { if (Ptr) Ptr->Release(); Ptr = nullptr; } + T* get() { return Ptr; } + + static IID GetIID() { return __uuidof(T); } + + void** InitPtr() { return (void**)TypedInitPtr(); } + T** TypedInitPtr() { reset(); return &Ptr; } + + DbgPtr& operator=(const DbgPtr& other) + { + if (this != &other) + { + if (Ptr) + Ptr->Release(); + Ptr = other.Ptr; + if (Ptr) + Ptr->AddRef(); + } + return *this; + } + + operator T* () const { return Ptr; } + T* operator ->() const { return Ptr; } + + T* Ptr; +}; + void I_AddMinidumpCallstack(const FString& minidumpFilename, FString& text, FString& logText) { if (IsPdbFileMissing()) @@ -584,3 +663,367 @@ void I_AddMinidumpCallstack(const FString& minidumpFilename, FString& text, FStr } } } + +#else + +struct MinidumpStream +{ + MINIDUMP_DIRECTORY* Directory = nullptr; + void* Data = nullptr; + ULONG Size = 0; +}; + +class MinidumpFile +{ +public: + MinidumpFile(const wchar_t* filename) + { + FileHandle = CreateFile(filename, FILE_GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, 0); + if (!FileHandle) + I_FatalError("Could not open minidump file"); + + MappingHandle = CreateFileMapping(FileHandle, nullptr, PAGE_READONLY, 0, 0, nullptr); + if (!MappingHandle) + { + CloseHandle(FileHandle); + I_FatalError("Could not create minidump file mapping"); + } + + BaseOfDump = MapViewOfFile(MappingHandle, FILE_MAP_READ, 0, 0, 0); + if (!BaseOfDump) + { + CloseHandle(MappingHandle); + CloseHandle(FileHandle); + I_FatalError("Could not map minidump file"); + } + } + + ~MinidumpFile() + { + UnmapViewOfFile(BaseOfDump); + CloseHandle(MappingHandle); + CloseHandle(FileHandle); + } + + std::vector GetThreadList() + { + MinidumpStream s = ReadStream(ThreadListStream); + MINIDUMP_THREAD_LIST* list = static_cast(s.Data); + std::vector threads(list->NumberOfThreads); + for (size_t i = 0; i < threads.size(); i++) + threads[i] = list->Threads[i]; + return threads; + } + + std::vector GetThreadExList() + { + MinidumpStream s = ReadStream(ThreadExListStream); + MINIDUMP_THREAD_EX_LIST* list = static_cast(s.Data); + std::vector threads(list->NumberOfThreads); + for (size_t i = 0; i < threads.size(); i++) + threads[i] = list->Threads[i]; + return threads; + } + + std::vector GetModuleList() + { + MinidumpStream s = ReadStream(ModuleListStream); + MINIDUMP_MODULE_LIST* list = static_cast(s.Data); + std::vector modules(list->NumberOfModules); + for (size_t i = 0; i < modules.size(); i++) + modules[i] = list->Modules[i]; + return modules; + } + + std::vector GetMemoryList() + { + MinidumpStream s = ReadStream(MemoryListStream); + MINIDUMP_MEMORY_LIST* list = static_cast(s.Data); + std::vector descriptors(list->NumberOfMemoryRanges); + for (size_t i = 0; i < descriptors.size(); i++) + descriptors[i] = list->MemoryRanges[i]; + return descriptors; + } + + std::vector GetMemory64List() + { + MinidumpStream s = ReadStream(Memory64ListStream); + MINIDUMP_MEMORY64_LIST* list = static_cast(s.Data); + std::vector descriptors(list->NumberOfMemoryRanges); + for (size_t i = 0; i < descriptors.size(); i++) + descriptors[i] = list->MemoryRanges[i]; + return descriptors; + } + + StackFrameList* GetUserStackFrameList() + { + MinidumpStream s = ReadStream(UserStackFrameListStream); + if (s.Size == sizeof(StackFrameList)) + return static_cast(s.Data); + else + return nullptr; + } + + MINIDUMP_EXCEPTION_STREAM GetException() + { + MinidumpStream s = ReadStream(ExceptionStream); + return *static_cast(s.Data); + } + + MINIDUMP_SYSTEM_INFO GetSystemInfo() + { + MinidumpStream s = ReadStream(SystemInfoStream); + return *static_cast(s.Data); + } + + MinidumpStream ReadStream(ULONG streamNumber) + { + MinidumpStream stream; + BOOL result = MiniDumpReadDumpStream(BaseOfDump, streamNumber, &stream.Directory, &stream.Data, &stream.Size); + if (!result) + I_FatalError("MiniDumpReadDumpStream failed"); + return stream; + } + + template + T* GetPtr(RVA rva) + { + return reinterpret_cast(static_cast(BaseOfDump) + rva); + } + + std::wstring GetString(RVA rva) + { + MINIDUMP_STRING* str = GetPtr(rva); + return std::wstring(str->Buffer, str->Length); + } + + HANDLE FileHandle = {}; + HANDLE MappingHandle = {}; + void* BaseOfDump = nullptr; + +private: + MinidumpFile(const MinidumpFile&) = delete; + MinidumpFile& operator=(const MinidumpFile&) = delete; +}; + +class MinidumpDebugger +{ +public: + MinidumpDebugger() + { + // We need an unique legal process handle for our debug session. Yes, this is really how Microsoft designed this API! + HANDLE currentProcess = GetCurrentProcess(); + BOOL result = DuplicateHandle(currentProcess, currentProcess, currentProcess, &process, 0, FALSE, DUPLICATE_SAME_ACCESS); + if (!result) + I_FatalError("DuplicateHandle failed"); + + DWORD flags = + SYMOPT_UNDNAME | // Return undecorated symbol names + SYMOPT_DEFERRED_LOADS | // Delay loading of modules and pdb files until we need them + SYMOPT_EXACT_SYMBOLS | // Only load modules that match the minidump + SYMOPT_FAIL_CRITICAL_ERRORS | // Never show any UI dialogs to the user + SYMOPT_LOAD_LINES; // Load symbol line info + SymSetOptions(flags); + + result = SymInitialize(process, nullptr, FALSE); + if (!result) + { + CloseHandle(process); + I_FatalError("SymInitialize failed"); + } + } + + ~MinidumpDebugger() + { + for (auto& module : Modules) + { + if (module.BaseOfDll != 0) + SymUnloadModule64(process, module.BaseOfDll); + } + + SymCleanup(process); + CloseHandle(process); + } + + void Open(const wchar_t* filename) + { + minidump = std::make_unique(filename); + LoadModuleList(); + LoadExceptionInfo(); + LoadUserStackFrameList(); + } + + const StackFrameList& GetStackFrameList() const { return stackFrameList; } + + FString GetExceptionText() + { + switch (ExceptionInfo.Record.ExceptionCode) + { + case EXCEPTION_ACCESS_VIOLATION: return "Access violation"; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "Array bounds exceeded"; + case EXCEPTION_BREAKPOINT: return "Breakpoint"; + case EXCEPTION_DATATYPE_MISALIGNMENT: return "Datatype misalignment"; + case EXCEPTION_FLT_DENORMAL_OPERAND: return "Floating point denormal operand"; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: return "Floating point divide by zero"; + case EXCEPTION_FLT_INEXACT_RESULT: return "Floating point inexact result"; + case EXCEPTION_FLT_INVALID_OPERATION: return "Floating point invalid operation"; + case EXCEPTION_FLT_OVERFLOW: return "Floating point overflow"; + case EXCEPTION_FLT_STACK_CHECK: return "Floating point stack check"; + case EXCEPTION_FLT_UNDERFLOW: return "Floating point underflow"; + case EXCEPTION_ILLEGAL_INSTRUCTION: return "Floating point illegal instruction"; + case EXCEPTION_IN_PAGE_ERROR: return "In page error"; + case EXCEPTION_INT_DIVIDE_BY_ZERO: return "Integer divide by zero"; + case EXCEPTION_INT_OVERFLOW: return "Integer overflow"; + case EXCEPTION_INVALID_DISPOSITION: return "Invalid disposition"; + case EXCEPTION_NONCONTINUABLE_EXCEPTION: return "Noncontinuable exception"; + case EXCEPTION_PRIV_INSTRUCTION: return "Priv instruction"; + case EXCEPTION_SINGLE_STEP: return "Single step"; + case EXCEPTION_STACK_OVERFLOW: return "Stack overflow"; + default: return {}; + } + } + + FString GetModuleName(void* frame) + { + IMAGEHLP_MODULE64 moduleInfo = { sizeof(IMAGEHLP_MODULE64) }; + SymGetModuleInfo64(process, (DWORD64)frame, &moduleInfo); + return moduleInfo.ModuleName; + } + + FString GetCalledFromText(void* frame) + { + FString s; + + unsigned char buffer[sizeof(IMAGEHLP_SYMBOL64) + 128]; + IMAGEHLP_SYMBOL64* symbol64 = reinterpret_cast(buffer); + memset(symbol64, 0, sizeof(IMAGEHLP_SYMBOL64) + 128); + symbol64->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL64); + symbol64->MaxNameLength = 128; + + DWORD64 displacement = 0; + BOOL result = SymGetSymFromAddr64(process, (DWORD64)frame, &displacement, symbol64); + if (result) + { + IMAGEHLP_LINE64 line64; + DWORD displacement1 = 0; + memset(&line64, 0, sizeof(IMAGEHLP_LINE64)); + line64.SizeOfStruct = sizeof(IMAGEHLP_LINE64); + result = SymGetLineFromAddr64(process, (DWORD64)frame, &displacement1, &line64); + if (result) + { + s.Format("Called from %s at %s, line %d\n", symbol64->Name, line64.FileName, (int)line64.LineNumber); + } + else + { + s.Format("Called from %s\n", symbol64->Name); + } + } + + return s; + } + +private: + struct Module + { + std::string ImageName; + DWORD64 BaseOfDll = 0; + }; + + std::vector Modules; + + struct + { + int ThreadId = 0; + CONTEXT* Context = nullptr; + MINIDUMP_EXCEPTION Record = {}; + } ExceptionInfo; + + void LoadModuleList() + { + for (const MINIDUMP_MODULE& module : minidump->GetModuleList()) + { + Module m; + m.ImageName = from_utf16(minidump->GetString(module.ModuleNameRva)); + m.BaseOfDll = SymLoadModuleEx(process, 0, m.ImageName.c_str(), nullptr, module.BaseOfImage, module.SizeOfImage, nullptr, 0); + Modules.push_back(std::move(m)); + } + } + + void LoadExceptionInfo() + { + auto info = minidump->GetException(); + ExceptionInfo.ThreadId = info.ThreadId; + if (info.ThreadContext.DataSize == sizeof(CONTEXT)) + ExceptionInfo.Context = minidump->GetPtr(info.ThreadContext.Rva); + ExceptionInfo.Record = info.ExceptionRecord; + } + + void LoadUserStackFrameList() + { + StackFrameList* list = minidump->GetUserStackFrameList(); + if (list) + stackFrameList = *list; + } + + static std::string from_utf16(const std::wstring& str) + { + if (str.empty()) return {}; + int needed = WideCharToMultiByte(CP_UTF8, 0, str.data(), (int)str.size(), nullptr, 0, nullptr, nullptr); + if (needed == 0) + I_FatalError("WideCharToMultiByte failed"); + std::string result; + result.resize(needed); + needed = WideCharToMultiByte(CP_UTF8, 0, str.data(), (int)str.size(), &result[0], (int)result.size(), nullptr, nullptr); + if (needed == 0) + I_FatalError("WideCharToMultiByte failed"); + return result; + } + + std::unique_ptr minidump; + HANDLE process = 0; + StackFrameList stackFrameList = {}; +}; + +void I_AddMinidumpCallstack(const FString& minidumpFilename, FString& text, FString& logText) +{ + if (IsPdbFileMissing()) + logText.AppendFormat("\nWarning: could not find " GAMENAMELOWERCASE ".pdb - No call stack will be displayed for the crash.\n"); + + try + { + MinidumpDebugger debugger; + debugger.Open(minidumpFilename.WideString().c_str()); + + FString exceptionText = debugger.GetExceptionText(); + if (!exceptionText.IsEmpty()) + { + text = exceptionText; + logText.AppendFormat("\n%s\n", exceptionText.GetChars()); + } + + FString lastExternalCode; + const StackFrameList& callstack = debugger.GetStackFrameList(); + for (int i = 1; i < callstack.frame_count; i++) + { + FString text = debugger.GetCalledFromText(callstack.frames[i]); + if (!text.IsEmpty()) + { + logText += text; + lastExternalCode = ""; + } + else + { + FString moduleName = debugger.GetModuleName(callstack.frames[i]); + if (!moduleName.IsEmpty() && moduleName != lastExternalCode) + logText.AppendFormat("Called from external code [%s]\n", moduleName.GetChars()); + lastExternalCode = moduleName; + } + } + } + catch (...) + { + // Just ignore errors for now. + } +} + +#endif