Skip to content

Conversation

@slurmlord
Copy link

@slurmlord slurmlord commented Sep 18, 2025

This change introduces the option to generate crash dumps, aka. mini dumps, on fatal errors.
The main minidump functionality is done by explicitly loading the dbghelp.dll from the system directory, as the dbghelp.dll that is bundled with the game is an older version that does not include this functionality. There is an option to create small dumps or extended info dumps, currently both are created.

Small dumps

These mostly contain stacks for the process threads and some stack variables, or to create dumps with extended info. The use case for these is to quickly determine where a crash occured, the type of crash, if it was already fixed etc. In addition, if the memory allocation structures are corrupted enough, an extended info dump might not succeed while the small dump should. The size of these dumps are typically on the order of 250kB.

Extended info

These contain global values, along with the memory regions allocated via the memory pool factory and the dynamic memory allocator. This makes all in-game objects available to the person debugging the crash dump, so for example dt generalszh!TheWritableGlobalData in WinDbg will show the state at the time the dump was created.

An alternative option could be to not traverse the memory structures "manually" to get to the allocations and instead just specify the MiniDumpWithFullMemory flag to MiniDumpWriteDump, but that increases the file size considerably.

As an example, dump of the generalszh process in the main menu with the shell map in the background yields a ~140MB dump when traversing and ~420MB with MiniDumpWithFullMemory. Beyond that, the ~140MB file compresses to ~20MB with 7Z, so should be relatively easily transferable.

Storage Location

Crash dumps are stored in a new folder called 'CrashDumps' under the userDir ("Documents\Command and Conquer Generals Zero Hour Data"), and on startup it will create this directory if it doesn't exist and delete any older dumps so only the 10 newest small and 2 newest extended info dumps are left. This is to preserve disk space, as the extended info files can be several hundred MB.

Integration points

For VS2022 builds, unhandled exceptions end up in the UnhandledExceptionFilter in WinMain, which then get a reference to the actual exception that occurred and includes that in the dump.
For VC6 builds, unhandled exceptions are caught in the catch(...) blocks of GameEngine::execute which then calls RELEASE_CRASH. As there is no exception data available in this case to populate _EXCEPTION_POINTERS from, an intentional exception is triggered to get the trace of the current thread. This makes the stack traces for VC6 a bit more cryptic than VS2022 builds as the C++ exception handling gets included in the trace.

Limitations

In the longer run we'll probably want to replace this code with a more mature solution, like CrashPad, but that currently depends on a newer compiler than VC6.
As the code is intended to be temporary, it's kept behind a new CMake feature so it can be easily removed. There are also some other decisions made with this in mind:

  • Minidump is created in-process. Ideally, the dump should be performed by a process external to the crashing/failing process, but to avoid having to ship an extra binary, in-process was chosen instead. It's being performed in a separate thread to hopefully have a clean stack to work with.
  • Depends on RTS_BUILD_OPTION_VC6_FULL_DEBUG for VC6 builds. The PDBs generated with the default VC6 compile options are lacking in information, making the mini dumps less useful. The option RTS_BUILD_OPTION_VC6_FULL_DEBUG should be enabled for VC6 builds to ensure maximum usability. VS2022 builds produce better PDBs and require no extra options.
  • Directory management code is contained within the MiniDumper class, not re-usable by other components.
  • Many Win32-specific types and functions are used directly without regards for portability.
  • As the MiniDump feature is not available for VC6, a lot of headers have been borrowed from minidumpapiset.h and included in the MiniDumper_compat.h file.
  • Globals are used for storing the current exception info.
  • Only enabled for the games, tools are currently not included.

Copy link

@OmniBlade OmniBlade left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, tested dump generation with a 2022 build.

@xezon xezon added Major Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour Debug Is mostly debug functionality System Is Systems related labels Oct 8, 2025
@slurmlord
Copy link
Author

Another approach I realized after publishing could be to move the GameMemory allocations from using GlobalAlloc to instead create a separate GameMemory heap and use HeapAlloc.
The benefit would be a lot less code required to traverse the allocations for inclusions the dump, as it could be done with HeapWalk instead for only the GameMemory heap.

Copy link

@xezon xezon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall. Just a bunch of small comments.

@slurmlord
Copy link
Author

Pushed a commit attempting to address most of the comments in the previous review:

  • Made TheMiniDumper a pointer, declared in MiniDumper.cpp
  • Moved extern TheMiniDumper to MiniDumper.h
  • Moved MiniDumper ctor body to MiniDumper.cpp
  • (Attempted to) find and correct all the stylistically misplaced "{"
  • Added enum for dump type
  • Fall back to DUMP_TYPE_FULL when DUMP_TYPE_GAMEMEMORY is requested and game memory implementation is turned off
  • Removed cmake feature dependency on RTS_GAMEMEMORY_ENABLE
  • Removed thread Id from dump file name
  • Replace hard-coded list of executable names to match with GetModuleFileNameW
  • (Attempted to) clean up log format
  • Added Enum values for dump thread exit codes
  • Removed MiniDumper::m_endRangeIter

In addition:

  • Added new static init/shutdown methods, using placement new on the process heap
  • Prefixed win32 api function calls with global namespace ::
  • Added asserts for dumping thread active state

Remaining:
Merge with #1066 when merged.

@xezon xezon added the Approved Pull Request was approved label Oct 21, 2025
@Skyaero42
Copy link

I think this should be enabled by default for now and included in our weekly pre-releases and legi.cc/patch distributions. The patch still crashes now and then without being able to find a cause (replays are send to us, but there is no crash to be found). For example, Legi (and only Legi) crashed yesterday on stream while being on the patch. No cause was found.

Players on the patch (with crashdump on) can send us the crashdumps and thus help finding where the issues/instabilities are.

@slurmlord
Copy link
Author

I think this should be enabled by default for now and included in our weekly pre-releases and legi.cc/patch distributions. The patch still crashes now and then without being able to find a cause (replays are send to us, but there is no crash to be found). For example, Legi (and only Legi) crashed yesterday on stream while being on the patch. No cause was found.

Players on the patch (with crashdump on) can send us the crashdumps and thus help finding where the issues/instabilities are.

Agreed, that's the main use case I was hoping this could address. Once this is merged, the remaining hurdle would be to get the weekly builds to enable the Vc6FullDebug cmake option.

Copy link

@xezon xezon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the cleanup in MiniDumper class needs another look. Make it simple and robust.

m_quitting = NULL;
}

DbgHelpLoader::unload();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (m_loadedDbgHelp) ?

Copy link

@xezon xezon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks good to me. A few minor comments that could be looked into.

TheMiniDumper->TriggerMiniDump(DUMP_TYPE_GAMEMEMORY);
}

MiniDumper::shutdownMiniDumper();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code block exists twice in this file. Perhaps consolidate?

#elif RTS_ZEROHOUR
Char product = 'Z';
#endif
Char dumpTypeSpecifier = dumpType == DUMP_TYPE_MINIMAL ? 'M' : 'X';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 3 types, but only identifiers here. Maybe use 3? Maybe use a lookup array or function for these? M, X is also used in InitializeDumpDirectory

// Only include data segments for the game and ntdll modules to keep dump size low
if (output.ModuleWriteFlags & ModuleWriteDataSeg)
{
if (::StrCmpIW(input.Module.FullPath, m_executablePath) && !::StrStrIW(input.Module.FullPath, L"ntdll.dll"))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also keep some other dll's, like kernel32 ?

#endif
memset(m_dumpDir, 0, ARRAY_SIZE(m_dumpDir));
memset(m_dumpFile, 0, ARRAY_SIZE(m_dumpFile));
memset(m_executablePath, 0, ARRAY_SIZE(m_executablePath));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine, but it would also be sufficient to just do m_executablePath = "" or m_executablePath[0] = 0

Bool MiniDumper::InitializeDumpDirectory(const AsciiString& userDirPath)
{
constexpr Int MaxExtendedFileCount = 2;
constexpr Int MaxMiniFileCount = 10;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constexpr const (because VS6 does not see constexpr)

#if RTS_GENERALS
Char product = 'G';
#elif RTS_ZEROHOUR
Char product = 'Z';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can make const

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Approved Pull Request was approved Debug Is mostly debug functionality Gen Relates to Generals Major Severity: Minor < Major < Critical < Blocker System Is Systems related ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants