Skip to content
Lesson 14.3Versioned Evidence, State, and Integrity

Crash Dumps as Process Snapshots

A small dump preserves selected threads, modules, registers, and stacks for later inspection without a running process.

Lesson 14.3
Read or listenPrint chapter or book
About 24 minutes

Keyboard shortcuts

?
Open this guide
Alt + B
Show or hide the lesson list
Alt + O
Show or fold the lesson outline
Alt + N
Open or close notes
Alt + H
Toggle semantic colours
Alt + D
Toggle page brightness
Alt + C
Toggle code brightness
← →
Move through a focused code trace
Home End
First or final step in a focused trace
Escape
Close this guide or an open panel

Reader shortcuts pause while you type in a text field.

Cheatsheet for Lesson 14.3Key ideas, reference and questions

A one-page summary of “Crash Dumps as Process Snapshots” you can keep beside your notes. Plain Markdown.

Lesson 14.3 — Crash Dumps as Process Snapshots: cheatsheet

  • Remember: A dump preserves selected state at capture time; it needs logs or tracing to explain the earlier sequence of events.
  • Also useful: Read stack frames as nested unfinished requests: the current work is on top, with the callers that led there underneath.

Key terms

  • dump_type: In the source, that choice is the dump_type value.

Review

  • Q: What is a minidump? A: A selected snapshot of process state, such as threads, modules, and registers, captured at one moment
  • Q: A dump from a crashed game shows the failing thread. A teammate asks what happened ten seconds earlier. What can the dump tell you? A: Little, because it records one instant; pair it with logs or an ETW trace when history matters
Download cheatsheet (.md)

The live tools in this chapter reported different parts of one process: modules, page regions, threads, and API results. The kernel lessons explained why those observations pass through controlled services. A dump brings selected process data together at one capture moment, so a later investigation can ask which module and thread were involved even after the process is gone.

A live debugger reads a running process. A dump file saves selected process state so it can be inspected later. A minidump may include:

  • basic process and operating-system information;
  • loaded and recently unloaded modules;
  • thread records and selected thread context;
  • stack memory needed to reconstruct calls;
  • exception information when captured during a crash.

The word “mini” describes a selectable subset, not a harmless text log. Dumps can contain file paths, arguments, stack data, and other private values. Treat them like diagnostic data, not screenshots.

does not contain
Minidump
one saved instant
Process identity
and module ranges
Thread records
Selected contexts
and registers
Stack memory needed
to reconstruct calls
Optional streams
Exception information
when captured during a crash
Selected memory pages
according to dump type
A timeline of everything
that happened earlier

The dump type decides which optional streams are saved. That is why a missing heap object or exception record may mean “not captured,” not “never existed.”

A dump records one chosen momentThe dump holds the state at its capture point. It does not hold how the program got there.
  1. 1 One chosen instant, saved

    Timeline

    Time
    earlier history
    capture point
    later

    Saved in the dump

    Records
    modules
    threads
    stacks

    Something chooses one capture point; the dump keeps module, thread and stack records as they were then.

  2. 2 Earlier history is not in the dump

    Timeline

    Time
    earlier history: not saved
    capture point
    later

    Needs another source

    Where history lives
    logs
    tracing

    How the state came about needs logs or tracing, which a dump does not contain.

  3. 3 A missing stream may simply not have been captured

    Saved in the dump

    Records
    modules
    threads
    stacks
    Not captured
    a stream that was not requested

    If a dump lacks something, it may not have been asked for, which is not proof the thing did not exist.

The dump holds the state at its capture point. It does not hold how the program got there.

A reproducible crash often disappears when you attach a debugger and change timing. A dump saves the process state close to the failure without a debugger attached, and from that file alone you can answer:

  • Which thread failed?
  • Which module contained the instruction pointer?
  • What calls led to that point?
  • Which game and DLL versions were loaded?
  • Did the crash occur inside the course patch, the game, or a system DLL?

A dump does not record every earlier event. Combine it with logs or an ETW trace when the history matters.

MiniDumpWriteDump can accept a process handle and PID. This course program deliberately uses GetCurrentProcess and GetCurrentProcessId, so it can dump only itself through this code path. It does not search for a game PID or request another process handle.

It also avoids MiniDumpWithFullMemory. A normal dump plus thread and unloaded-module information is enough to learn the workflow without copying every private page in the process.

In the source, that choice is the dump_type value. Each optional stream has its own bit, and | sets several bits at once:

bits 15–120001
bits 11–80000
bits 7–40010
bits 3–00000
bit 12, thread info
bit 5, unloaded modules
bit 1, full memory, left 0
dump_type in binary. MiniDumpWithThreadInfo is 0x1000 = 4,096 = 2^12, so it is bit 12. MiniDumpWithUnloadedModules is 0x20 = 32 = 2^5, bit 5. MiniDumpNormal is 0: it sets no bit and names the baseline dump. Together they make 0x1020 = 4,096 + 32 = 4,128. MiniDumpWithFullMemory would be 0x2, bit 1, and this lab leaves it clear.
Complete lab source: self_minidump.rs

self_minidump.rs

windows-labs/src/bin/ · Rust, 60 lines

Show the codeHide the code
#[cfg(windows)]
fn main() -> anyhow::Result<()> {
    use std::{fs::OpenOptions, os::windows::io::AsRawHandle, path::PathBuf};

    use anyhow::{Context, ensure};
    use windows::Win32::{
        Foundation::HANDLE,
        System::{
            Diagnostics::Debug::{
                MiniDumpNormal, MiniDumpWithThreadInfo, MiniDumpWithUnloadedModules,
                MiniDumpWriteDump,
            },
            Threading::{GetCurrentProcess, GetCurrentProcessId},
        },
    };

    let output = std::env::args_os()
        .nth(1)
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("gha-self.dmp"));
    ensure!(
        !output.exists(),
        "refusing to overwrite {}",
        output.display()
    );
    let file = OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(&output)
        .with_context(|| format!("could not create {}", output.display()))?;
    let file_handle = HANDLE(file.as_raw_handle());

    // SAFETY: these calls take no pointers and return identifiers for this process.
    let (process, process_id) = unsafe { (GetCurrentProcess(), GetCurrentProcessId()) };
    let dump_type = MiniDumpNormal | MiniDumpWithThreadInfo | MiniDumpWithUnloadedModules;
    // SAFETY: both handles belong to this process, the file stays open during
    // the call, and no optional exception/user/callback pointer is supplied.
    unsafe {
        MiniDumpWriteDump(
            process,
            process_id,
            file_handle,
            dump_type,
            None,
            None,
            None,
        )?;
    }
    drop(file);

    println!("Wrote {}", output.display());
    println!("The dump contains this lab process, not another process.");
    println!("Open it with: windbgx -z {}", output.display());
    Ok(())
}

#[cfg(not(windows))]
fn main() {
    eprintln!("This self-dump lab must run on Windows.");
}
#[cfg(windows)]
fn main() -> anyhow::Result<()> {
use std::{
fs::OpenOptions,
os::windows::io::AsRawHandle,
path::PathBuf,
};
use anyhow::{Context, ensure};
use windows::Win32::{
Foundation::HANDLE,
System::{
Diagnostics::Debug::{
MiniDumpNormal,
MiniDumpWithThreadInfo,
MiniDumpWithUnloadedModules,
MiniDumpWriteDump,
},
Threading::{GetCurrentProcess, GetCurrentProcessId},
},
};
let output = std::env::args_os()
.nth(1)
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("gha-self.dmp"));
ensure!(
!output.exists(),
"refusing to overwrite {}",
output.display()
);
let file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&output)
.with_context(|| format!("could not create {}", output.display()))?;
let file_handle = HANDLE(file.as_raw_handle());
// SAFETY: these calls take no pointers and return identifiers for this process.
let (process, process_id) = unsafe {
(GetCurrentProcess(), GetCurrentProcessId())
};
let dump_type = MiniDumpNormal
| MiniDumpWithThreadInfo
| MiniDumpWithUnloadedModules;
// SAFETY: both handles belong to this process, the file stays open during
// the call, and no optional exception/user/callback pointer is supplied.
unsafe {
MiniDumpWriteDump(
process,
process_id,
file_handle,
dump_type,
None,
None,
None,
)?;
}
drop(file);
println!("Wrote {}", output.display());
println!("The dump contains this lab process, not another process.");
println!("Open it with: windbgx -z {}", output.display());
Ok(())
}
#[cfg(not(windows))]
fn main() {
eprintln!("This self-dump lab must run on Windows.");
}

File owns a Windows file handle. AsRawHandle borrows the numeric handle so the Windows API can use it. The File must stay alive until MiniDumpWriteDump returns.

That is why drop(file) appears after the Windows call. Closing it first would leave file_handle as a stale number.

Windows VM

The first command builds the small dumper in the Windows VM. The second gives it an output path; create_new(true) makes a repeat run fail safely rather than replace the first capture. Open the resulting file in the next step.

Terminal window
cd windows-labs
cargo build --release --target i686-pc-windows-msvc `
--bin self_minidump
.\target\i686-pc-windows-msvc\release\self_minidump.exe `
.\gha-self-first.dmp

The program should print the saved path. Its complete buildable source is self_minidump.rs.

Install WinDbg from Microsoft, then open the file from the UI or run:

Terminal window
windbgx -z .\gha-self-first.dmp

These first commands answer different questions:

!analyze -v summarize an exception if the dump contains one
lm list loaded modules and their address ranges
~ list threads
~* k show a stack trace for every thread
r show registers for the selected thread

The self-dump is captured during a normal function call, so it does not contain a crash exception. That is useful: you can learn module, thread, register, and stack navigation without deliberately causing undefined behavior.

A stack trace is a chain of function frames. The top frame is where the selected thread was when captured. Frames below it are callers that led there.

topinside MiniDumpWriteDump
self_minidump::main
Rust runtime start-up
C runtime start-up
bottomWindows thread start-up in kernel32 and ntdll
where the thread was at capture
callers, each waiting for the frame above it to return
A simplified k listing for the thread that wrote the self-dump, newest call on top. Read from the bottom up, it replays the calls in the order they happened: Windows starts the thread, the C and Rust runtimes start up, main runs, and main calls MiniDumpWriteDump. A real listing splits each box into several frames.

Names may be missing when matching symbols are unavailable. In that case, WinDbg may show a module plus an offset:

self_minidump+0x1234

The offset is still useful when paired with the exact build hash and module base: it names the byte 0x1234 past the start of self_minidump.exe as loaded, and in one exact build that is one specific instruction.

Do not corrupt a running game’s memory just to obtain a dump. Instead, make a tiny separate test program that crashes on purpose, enable Windows Error Reporting local dumps for that one executable in a disposable VM, and inspect the result.

Not every Rust failure is a crash that Windows Error Reporting can see. Returning an error from main prints it and exits with code 1. A panic under Rust’s default unwinding strategy is caught by the runtime, which prints the message and exits with code 101. Both are ordinary exits, so there is nothing to dump. A panic in a program built with panic = "abort" is different: it ends in a fail-fast exception, code 0xC0000409, which Windows Error Reporting does capture, and !analyze then reports that exception rather than an access violation.

  • A small dump may omit heap data needed to inspect a particular object.
  • A dump records one instant, not the earlier event timeline.
  • Unresolved symbols make stacks harder to name.
  • Optimized code may inline functions or keep values only in registers.
  • A module path and timestamp help, but a trusted hash is stronger build identity.

Start small. Capture more data only when a specific debugging question requires it, and keep diagnostic files with the exact build record and test logs they describe.

Chapter 12 uses that same build and module evidence to ask which DLL actually loaded, which API contract was resolved, and where a process-level view ends. It then follows requests into device drivers and distinguishes process virtual addresses from the physical-memory mappings underneath them.

References: MiniDumpWriteDump, Minidump files, and WinDbg overview.

A snapshot captures a calculation at one moment. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

memory = 75; register = 80
choose the capture moment
if after store: memory = register
copy selected memory and register state
save this captured state
explain what this snapshot cannot establish

Result: dump memory=75, register=80. A memory-only view would omit the temporary result; this snapshot alone cannot reconstruct the preceding event history.

Final values: moment = before; live_memory = 75; live_register = 80; saved_memory = 75; saved_register = 80; saved = true.

Read every step of the lesson’s example
  1. Step 1, line 1. Start with the selected inputs. Follow the highlighted operation and the changing state below.

    moment = before.

  2. Step 2, line 1. The calculation’s temporary result has not yet been stored.

    moment = before; live_memory = 75; live_register = 80.

  3. Step 3, line 2. Capture at the selected boundary.

    moment = before; live_memory = 75; live_register = 80.

  4. Step 4, line 3. The store has not executed when capture begins.

    moment = before; live_memory = 75; live_register = 80.

  5. Step 5, line 4. Copy the selected state into the diagnostic file.

    moment = before; live_memory = 75; live_register = 80; saved_memory = 75; saved_register = 80.

  6. Step 6, line 5. The saved values remain available after this model process exits.

    moment = before; live_memory = 75; live_register = 80; saved_memory = 75; saved_register = 80; saved = true.

  7. Step 7, line 6. Result: dump memory=75, register=80. A memory-only view would omit the temporary result; this snapshot alone cannot reconstruct the preceding event history.

    moment = before; live_memory = 75; live_register = 80; saved_memory = 75; saved_register = 80; saved = true.

Knowledge check

Interpret a crash dump

What is a minidump?

Finished Lesson 14.3?

Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.

Chapter 14: 10 lessons

This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.