How to debug a MU client crash (dump analysis)
Turn that mysterious Main.exe crash into a root cause using memory dumps, WinDbg, symbols and stack trace reading, with a repeatable workflow to find the guilty module and fix it.
A player opens a ticket: "the game closes by itself when I enter Lorencia." With no information, that turns into guesswork — swap the driver, reinstall, try again, and the problem comes back. The professional way to solve it is to turn the crash into an analyzable artifact: a memory dump. With the r
A player opens a ticket: "the game closes by itself when I enter Lorencia." With no information, that turns into guesswork — swap the driver, reinstall, try again, and the problem comes back. The professional way to solve it is to turn the crash into an analyzable artifact: a memory dump. With the right dump, configured symbols and a methodical read of the stack, you go from "closes by itself" to "null-pointer access inside d3d9.dll called from the particle render in build 1.04g." This tutorial builds that workflow from start to finish, repeatable for any Main.exe crash. All offsets, module names and paths appear as examples and vary by season/client.
Prerequisites
This guide assumes a client that already runs on most machines and crashes in specific situations. If you're still building the server base, first see how to set up a MU Online server. Debugging a crash makes sense when the client already works in the common case and fails in the exception.
| Requirement | Detail (example — varies by season/client) |
|---|---|
| WinDbg | From the Windows SDK (classic WinDbg or WinDbg Preview) |
| Microsoft symbols | Local cache + public srv |
| Main.exe PDB | From the exact build, if you compile the client |
| Process Hacker | To generate a dump on demand |
| Healthy reference machine | Where the client runs without crashing |
| The crash dump | Minidump from the affected machine |
The item that separates fast analysis from suffering is the correct PDB. Without it, you read the stack in offsets; with it, in function names. If you have the client's source, archive the PDB of every released version alongside the binary — days later, an old dump without the right PDB is nearly useless for the client's own frames.
Stage 1 — Capture the dump
No dump, no analysis. There are three ways, in order of practicality for production.
Step 1 — Automatic WER (recommended)
Configure Windows Error Reporting to capture every Main.exe crash into a local folder. This catches the crash on the spot, without relying on the player reproducing it with a tool open.
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\Main.exe]
"DumpFolder"=hex(2):43,00,3a,00,5c,00,43,00,72,00,61,00,73,00,68,00,00,00
"DumpType"=dword:00000001
"DumpCount"=dword:0000000a
DumpType=1 generates a minidump; 2 would generate a full dump. The dump appears in the configured folder as soon as Main.exe closes on an exception. Distribute this tweak in your launcher to collect dumps from players who agree to send them.
Step 2 — On-demand dump with Process Hacker
When the crash is a "freeze" before closing, or when you want the state at the right moment: open Process Hacker, find Main.exe, right-click → Create dump file. It generates a manual minidump of the live process. Useful for hangs (not crashes), when the process doesn't die on its own.
Step 3 — MiniDumpWriteDump in the client itself
If you compile the client, install an unhandled-exception handler that calls MiniDumpWriteDump. That way the client generates its own dump with extra context (version, current map, player) written alongside a log. This is the richest option, because you control what gets captured.
// Sketch — install early, before the rest of init
LONG WINAPI MyHandler(EXCEPTION_POINTERS* ep) {
HANDLE h = CreateFile(L"C:\\Crash\\main_crash.dmp", GENERIC_WRITE, 0,
nullptr, CREATE_ALWAYS, 0, nullptr);
MINIDUMP_EXCEPTION_INFORMATION mei{ GetCurrentThreadId(), ep, FALSE };
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), h,
MiniDumpNormal, &mei, nullptr, nullptr);
return EXCEPTION_EXECUTE_HANDLER;
}
// SetUnhandledExceptionFilter(MyHandler);
Stage 2 — Prepare WinDbg
Badly configured symbols turn a readable stack into offset soup. Configure it before opening any dump.
Step 4 — Symbol path
In WinDbg, set the local cache plus Microsoft's public server and reload:
.sympath srv*C:\Symbols*https://msdl.microsoft.com/download/symbols
.reload /f
This resolves ntdll, kernel32, d3d9, user32 and the like. For Main.exe's own frames, add the folder with the build's PDB:
.sympath+ C:\MU\pdb\1.04g
.reload /f Main.exe
Step 5 — Open the dump
File → Open Crash Dump, select the .dmp. WinDbg loads the modules and stops at the exception context. The first thing to run is the automatic analysis.
Stage 3 — Automatic analysis and reading the stack
Step 6 — !analyze -v
!analyze -v
This command does the heavy lifting up front: it identifies the exception code, the address that faulted, the likely guilty module and prints the stack of the thread that crashed. Read these fields carefully:
| Field | What it tells you |
|---|---|
EXCEPTION_CODE | Type of error (e.g. c0000005 = access violation) |
FAULTING_IP | Instruction/address where it blew up (e.g. Main+0x3f21a) |
MODULE_NAME | Module flagged as guilty |
STACK_TEXT | The call stack up to the crash |
PROCESS_NAME | Should be Main.exe |
c0000005 (access violation) is by far the most common: the client tried to read/write at an invalid address — almost always a null pointer or an already-freed one. If the faulting address is close to zero (e.g. 0x00000008), it's a null pointer with a struct-field offset: someone accessed object->field with object == null.
Step 7 — Read the stack manually
k
k prints the current thread's stack. Read it bottom to top (oldest at the bottom, the crash at the top). You want to answer: is the fault the client's, a third-party DLL's or the system's?
# illustrative example — varies by season/client
00 d3d9!CDevice::DrawPrimitive+0x2a <- top: where it blew up
01 Main+0x3f21a <- client code calling the render
02 Main+0x41008
03 Main+0x51c30
04 kernel32!BaseThreadInitThunk <- base: thread start
Here the top is in d3d9, but the one who called with a bad argument was Main+0x3f21a. That points to the client's render/graphics. If the top were in a strange module (e.g. overlay_x.dll or an antivirus), suspicion would shift to injected third-party software.
Step 8 — List modules and find the intruder
lm
lm lists all loaded modules. Compare with those of a healthy machine. Modules that appear only in the problematic dump — recording overlays, injectors, antivirus hooks — are strong suspects, especially if they show up in the stack.
Stage 4 — Triage by crash pattern
With the stack and modules in hand, classify. Most MU client crashes fall into a few patterns.
| Pattern in the stack | Likely cause | Fix direction |
|---|---|---|
Top in d3d9/d3d8 called by the client | Null graphics resource, missing texture/effect | Check for a missing asset; update the GPU driver |
Top in Main.exe with c0000005 near zero | Null pointer in the client code | Locate the offset in the PDB; fix it in the source |
| Third-party module at the top | Overlay/AV/injector | Remove the software; test without it |
Stack overflow (c00000fd) | Infinite recursion | Review the client's loop/callback |
Heap corruption | Write past a buffer | Full dump + gflags/PageHeap |
| Crash only when loading a map | Corrupt asset of that map | Repatch the files of that map |
Step 9 — From offset to line (with the PDB)
If the crash is in Main.exe and you have the PDB, resolve the address:
ln Main+0x3f21a
ln shows the nearest symbol — the function name and offset. With source, you reach the line. Without a PDB, Main+0x3f21a is still useful: it is stable for that build, so repeated crashes at the same offset confirm the same bug, and you can compare reports from several players.
Stage 5 — Reproduce and confirm
Analysis without reproduction is a hypothesis. Try to reproduce it on the reference machine following the player's scenario (same map, same action). If it reproduces, generate your own dump and check whether it lands on the same offset/module. A match confirms the cause; a mismatch indicates an environmental factor (driver, overlay, the player's local DLL).
For intermittent heap crashes, turn on PageHeap for Main.exe with gflags before reproducing — it makes the corruption blow up at the moment of the invalid write, not later, letting the stack point to the real culprit.
Common errors and fixes
| Symptom in the analysis | Cause | Fix |
|---|---|---|
| Stack with only offsets, no names | PDB/symbols missing | Configure .sympath; archive the PDB per build |
!analyze -v blames a system module | The real fault is with the caller | Read k and find the client frame that called it |
| Empty or truncated dump | Insufficient minidump | Generate a full dump for the case |
| Crash won't reproduce on your machine | The player's environmental factor | Compare lm; suspect an overlay/driver/AV |
| Offset changes with each report | Different builds among players | Standardize the client version via a launcher |
| WinDbg won't open the dump | Dump of a different architecture (x86/x64) | Use the WinDbg matching the architecture |
| Symbols won't download | Firewall blocks msdl | Allow access or pre-populate the local cache |
Best practices to avoid debugging blind
Debugging starts before the crash. Standardize the client version with a launcher so all dumps come from the same build; archive the PDB of every released version; embed an exception handler that writes a dump + context (map, version, player); and keep a healthy reference machine for module comparison. With that, every "closes by itself" ticket arrives already with a dump and context, and analysis becomes routine instead of archaeology.
Launch checklist
- WER configured to capture
Main.exedumps - Exception handler with
MiniDumpWriteDumpin the client (if you compile it) - Context (version, map, player) written alongside the dump
- PDB of each build archived alongside the binary
- WinDbg with
.sympathfor local cache + MS server - Healthy reference machine available for comparison
!analyze -v→k→lm→lnworkflow documented for the team- Client version standardized via a launcher
- Procedure for the player to send the dump safely defined
- Crashes cataloged by offset/module to detect recurrence
- PageHeap/gflags available to hunt heap corruption
- Fix validated by reproducing at the same offset before closing the case
Frequently asked questions
What's the difference between a minidump and a full dump?
A minidump is small (a few MB) and contains thread stacks, registers and loaded modules, enough for most crash analyses. A full dump includes the entire process memory, is huge (can exceed 1 GB) and is only worth it when you need to inspect the heap, buffers and object states that the minidump doesn't keep. Always start with the minidump.
Do I need the Main.exe source code to analyze the dump?
It helps a lot, but it's not required. Without source and without symbols (PDB), you still see the stack in terms of modules and offsets (e.g. Main.exe+0x3F21A), identify whether the crash is in the client, a third-party DLL or the system, and recognize patterns like a null-pointer access. With symbols you gain function names; with source, the exact line.
Where does Windows save crash dumps?
It depends on the configuration. WER (Windows Error Reporting) usually writes to %LOCALAPPDATA%\\CrashDumps when enabled via the registry. You can also generate one on demand with Process Hacker/Task Manager (Create dump file) or programmatically via MiniDumpWriteDump in the client itself. Configuring WER to capture Main.exe automatically is the most practical option in production.
The crash only happens on some players' machines, now what?
A machine-specific crash is almost always environmental: an old GPU driver, an injected third-party DLL (overlay, antivirus), missing DirectX/redistributables, or the wrong compatibility mode. Ask the player for the dump, compare the loaded modules with those of a healthy machine and look for the strange module in the stack. Often the fix is to update a driver or remove an overlay.
How do I configure symbols in WinDbg?
Point the symbol path to Microsoft's server plus a local cache folder, for example with .sympath and .reload. That resolves the names of system functions (ntdll, kernel32, d3d9). For Main.exe itself, you need the PDB matching that build; without it, the client frames show up only as offsets. Keep the PDBs of every version you release.