Dbgeng didn't work - let's try do it ourselves

This commit is contained in:
Magnus Norddahl 2025-04-18 14:43:29 +02:00
commit ddffb4193a

View file

@ -31,9 +31,22 @@
#ifdef _MSC_VER
#include <dbghelp.h>
#include <signal.h>
#include <Dbgeng.h>
#if defined(USE_DBGENG)
#include <Dbgeng.h>
#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<std::recursive_mutex> 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<std::recursive_mutex> mutex_lock(mutex);
@ -409,42 +486,6 @@ const BYTE CrashReporter::patch_bytes[5] = { 0x33, 0xC0, 0xC2, 0x04, 0x00 };
#endif
template<typename T>
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<typename T>
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<MINIDUMP_THREAD> GetThreadList()
{
MinidumpStream s = ReadStream(ThreadListStream);
MINIDUMP_THREAD_LIST* list = static_cast<MINIDUMP_THREAD_LIST*>(s.Data);
std::vector<MINIDUMP_THREAD> threads(list->NumberOfThreads);
for (size_t i = 0; i < threads.size(); i++)
threads[i] = list->Threads[i];
return threads;
}
std::vector<MINIDUMP_THREAD_EX> GetThreadExList()
{
MinidumpStream s = ReadStream(ThreadExListStream);
MINIDUMP_THREAD_EX_LIST* list = static_cast<MINIDUMP_THREAD_EX_LIST*>(s.Data);
std::vector<MINIDUMP_THREAD_EX> threads(list->NumberOfThreads);
for (size_t i = 0; i < threads.size(); i++)
threads[i] = list->Threads[i];
return threads;
}
std::vector<MINIDUMP_MODULE> GetModuleList()
{
MinidumpStream s = ReadStream(ModuleListStream);
MINIDUMP_MODULE_LIST* list = static_cast<MINIDUMP_MODULE_LIST*>(s.Data);
std::vector<MINIDUMP_MODULE> modules(list->NumberOfModules);
for (size_t i = 0; i < modules.size(); i++)
modules[i] = list->Modules[i];
return modules;
}
std::vector<MINIDUMP_MEMORY_DESCRIPTOR> GetMemoryList()
{
MinidumpStream s = ReadStream(MemoryListStream);
MINIDUMP_MEMORY_LIST* list = static_cast<MINIDUMP_MEMORY_LIST*>(s.Data);
std::vector<MINIDUMP_MEMORY_DESCRIPTOR> descriptors(list->NumberOfMemoryRanges);
for (size_t i = 0; i < descriptors.size(); i++)
descriptors[i] = list->MemoryRanges[i];
return descriptors;
}
std::vector<MINIDUMP_MEMORY_DESCRIPTOR64> GetMemory64List()
{
MinidumpStream s = ReadStream(Memory64ListStream);
MINIDUMP_MEMORY64_LIST* list = static_cast<MINIDUMP_MEMORY64_LIST*>(s.Data);
std::vector<MINIDUMP_MEMORY_DESCRIPTOR64> 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<StackFrameList*>(s.Data);
else
return nullptr;
}
MINIDUMP_EXCEPTION_STREAM GetException()
{
MinidumpStream s = ReadStream(ExceptionStream);
return *static_cast<MINIDUMP_EXCEPTION_STREAM*>(s.Data);
}
MINIDUMP_SYSTEM_INFO GetSystemInfo()
{
MinidumpStream s = ReadStream(SystemInfoStream);
return *static_cast<MINIDUMP_SYSTEM_INFO*>(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<typename T>
T* GetPtr(RVA rva)
{
return reinterpret_cast<T*>(static_cast<char*>(BaseOfDump) + rva);
}
std::wstring GetString(RVA rva)
{
MINIDUMP_STRING* str = GetPtr<MINIDUMP_STRING>(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<MinidumpFile>(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<IMAGEHLP_SYMBOL64*>(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<Module> 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<CONTEXT>(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<MinidumpFile> 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