Skip to main content
This page documents the WinDbg-based debugging workflow for low-level inspection of the original Crimsonland binary.

Overview

WinDbg (Windows Debugger) provides:
  • Breakpoints with conditional expressions
  • Memory dumps of structs and pools
  • Call stacks with return addresses
  • Register inspection during function execution
  • Single-stepping through machine code
Use WinDbg when:
  • Frida hooks are too slow or invasive
  • You need to inspect CPU registers or flags
  • Conditional breakpoints based on complex state
  • One-off deep dives into specific issues

Setup

Remote Server Workflow

Run WinDbg as a persistent server:
# Server (long-lived, logs to file)
cdb -server tcp:port=5555 -logo C:\Crimsonland\windbg.log -g crimsonland.exe
Client (short reconnects):
cdb -remote tcp:server=localhost,port=5555 -bonc
Notes:
  • -bonc = break on next command (must type g to continue)
  • Server log persists; client output is ephemeral
  • Use just windbg-tail to read new log lines

Just Shortcuts

# Windows VM:
just windbg-server  # Start persistent server
just windbg-client  # Connect client
just windbg-tail    # Read new log lines

Basic Commands

Breakpoints

bp 0x00420b90          ; Break at projectile_update
bl                     ; List breakpoints
bc *                   ; Clear all breakpoints
g                      ; Go (continue execution)

Memory Inspection

dd 0x004908d4          ; Display dwords (hex)
df 0x004908d4          ; Display floats
db 0x004908d4 L0x40    ; Display bytes (length 0x40)
du 0x00471230          ; Display Unicode string
da 0x00471230          ; Display ASCII string

Registers

r                      ; Show all registers
r eax                  ; Show EAX
r eax=100              ; Set EAX to 100

Call Stack

k                      ; Stack trace
kb                     ; Stack trace with first 3 params
kn                     ; Stack trace with frame numbers

Inspection Patterns

Dumping a Struct

.printf "Projectile[0]:\n"
db 0x004926b8 L0x40

; Pretty-print specific fields:
.printf "  active: %d\n", by(0x004926b8)
.printf "  pos_x: %.2f\n", poi(0x004926b8+8)
.printf "  pos_y: %.2f\n", poi(0x004926b8+0xc)
.printf "  life_timer: %.2f\n", poi(0x004926b8+0x24)
Output:
Projectile[0]:
  active: 1
  pos_x: 432.50
  pos_y: 300.00
  life_timer: 0.35

Conditional Breakpoint

; Break only when player health drops below 20
bp 0x00425e50 ".if (poi(0x004908d4) < 20) {} .else {gc}"
Explanation:
  • poi(addr) = read pointer/dword at address
  • .if/.else = conditional execution
  • gc = go from conditional (continue without printing)

Watching for Changes

; Break when projectile[5].active changes
ba w1 0x004926b8+5*0x40 "g"
Flags:
  • ba = break on access
  • w1 = write, 1 byte
  • r4 = read, 4 bytes
  • e = execute

Logging Loop Iterations

bp 0x00420c00 ".printf \"tick=%d proj[%d] type=%d\\n\", dwo(0x00480850), @ecx, poi(@ecx+0x20); g"
Output (to log file):
tick=347 proj[0] type=1
tick=347 proj[2] type=6
tick=347 proj[5] type=45

Real-World Examples

Example 1: Fire Bullets Override

Goal: Confirm that Fire Bullets bonus forces projectile type to 0x2d.
; Break at projectile_spawn with params
bp 0x00420440

; On break, inspect:
; - Requested type (on stack)
; - Fire Bullets timer
; - Actual type written

g
; ... breakpoint hit ...

; Show stack params
kb

; Check Fire Bullets timer (player 0)
df 0x00490bcc L1

; Continue and check written type
pt  ; Step to return
dd @ecx+0x20 L1  ; type_id field
Session log:
Breakpoint 0 hit
crimsonland+0x20440:
  ret addr: 0x004136f2
  param[2] (type_id): 0x00000001  ; Requested pistol

Fire Bullets timer: 8.5

After spawn:
  type_id: 0x0000002d  ; Forced to fire projectile

Example 2: Damage Calculation

Goal: Trace damage calculation with perk multipliers.
; Break at creature_apply_damage
bp 0x004207c0 ".printf \"apply_damage: idx=%d dmg=%.2f\\n\", @ecx, dwo(@esp+4); .echo; g"

; Break before health update
bp 0x004207e5 ".printf \"  health: %.2f -> %.2f\\n\", poi(@eax+0x10), poi(@eax+0x10)-dwo(@esp+4); g"
Output:
apply_damage: idx=3 dmg=15.00
  health: 30.00 -> 15.00

apply_damage: idx=3 dmg=15.00
  health: 15.00 -> 0.00

Example 3: RNG Divergence

Goal: Find first RNG call difference between runs.
; Record RNG return values
bp 0x00461746+0x0d ".printf \"rng: %d\\n\", @eax; g"

; Compare two runs:
; Run 1: rng calls [12345, 67890, 11111, ...]
; Run 2: rng calls [12345, 67890, 22222, ...]  <- diverges at call 3
Analysis: Third call differs → find what function made it:
bp 0x00461746 ".if (poi(0x00480850) == 347) {k; g} .else {g}"
Prints stack when tick == 347 (divergence point).

Memory Layouts

Player Health Table

; Player 0 health
df 0x004908d4

; Player 0 position (negative offsets)
df 0x004908d4-0x10  ; pos_x
df 0x004908d4-0x0c  ; pos_y

; Player 1 health (stride 0x360)
df 0x004908d4+0x360

Projectile Pool

; Dump first 5 entries (0x40 bytes each)
.for (r $t0 = 0; @$t0 < 5; r $t0 = @$t0 + 1) {
  .printf "Projectile[%d]:\n", @$t0
  .printf "  active: %d\n", by(0x004926b8 + @$t0 * 0x40)
  .printf "  type: %d\n", poi(0x004926b8 + @$t0 * 0x40 + 0x20)
  .printf "  pos: (%.2f, %.2f)\n", poi(0x004926b8 + @$t0 * 0x40 + 8), poi(0x004926b8 + @$t0 * 0x40 + 0xc)
}

Config Blob

; Screen resolution
dd 0x00480348+0x1bc L2  ; width, height

; Keybinds (player 1)
dd 0x00480348+0x1c8 L13  ; 13 keybind dwords

; Volume settings
df 0x00480348+0x464 L2  ; sfx_volume, music_volume

Scripting

WinDbg Script File

Create capture_state.txt:
.printf "=== State Snapshot ==="
.printf "Tick: %d", dwo(0x00480850)
.printf "Player health: %.2f", poi(0x004908d4)
.printf "Active projectiles: "

.for (r $t0 = 0; @$t0 < 0x60; r $t0 = @$t0 + 1) {
  .if (by(0x004926b8 + @$t0 * 0x40) != 0) {
    .printf "%d ", @$t0
  }
}

.printf ""
Run:
$$<capture_state.txt

Comparison with Frida

FeatureWinDbgFrida
SpeedSlower (pauses execution)Faster (minimal overhead)
PrecisionRegister-levelFunction-level
AutomationScript files, limitedFull JavaScript
PersistenceLog filesJSONL output
Use caseDeep dives, one-offsContinuous capture
Recommendation: Use Frida for bulk capture, WinDbg for targeted investigation.

Common Pitfalls

Float display: Use df or poi() for floats, not dd (shows hex representation).
Pointer syntax: poi(addr) reads 4 bytes as dword/pointer. For single byte, use by(addr).
Stride math: Remember pool access is base + idx * stride + offset, not base + idx + offset.

Frida Capture

Higher-level instrumentation for bulk capture

Struct Recovery

Using memory dumps to validate struct layouts

Differential Testing

Comparing captured states between original and rewrite

Build docs developers (and LLMs) love