WinDbg / CDB workflow¶
Remote server setup (works reliably)¶
Just shortcuts (Windows):
Notes:
just windbg-clientuses-bonc, so you mustgafter attaching.- Client output is not persisted by the server; if the client session drops, logs are lost.
- The server logs to
C:\Crimsonland\windbg.log(overwritten on each server start, ASCII/ANSI). - The server should be started by the user. Codex only connects as a client to run commands, and we inspect the server log file to see captured output.
just windbg-tailprints any new log lines since the last read and remembers its position inC:\Crimsonland\windbg.log.pos.- The tail script decodes ASCII by default and switches to UTF-16 if a BOM is present.
Workflows¶
User workflow (server owner)¶
1) Start the server (once, long-lived):
2) Keep it running. The user does not need to connect a client or tail the log.
Agent workflow (short-lived reconnects)¶
1) The user starts the server and keeps it running. The agent never starts a server.
2) On every agent reconnect, catch up first:
3) Then connect and run the agent commands:
4) Resume the game (g) if needed, and disconnect the client (Ctrl+B).
Notes:
- The log is written by the server (
-logo), so it survives client drops but resets on server restart. - The tail script reads only new bytes from
C:\Crimsonland\windbg.log.
Sessions¶
Session 1 (2026-01-18) - credits secret¶
Wishlist¶
Yes, WinDbg is a great way to recover the "missing pointer" when Ghidra's call graph does not show a direct xref. You can catch the function at runtime, inspect who called it, and see where the board pointer lives.
Here is a practical workflow (32-bit game):
1) Attach and break on the function
- Use the module base from lm in case the image is not fixed at 0x00400000.
2) Trigger the suspected minigame When the breakpoint hits:
- k shows the call stack.
- The return address is at [@esp]. You can disassemble the callsite:
3) Inspect the arguments (board pointer + outputs) The signature looks like: FUN_0040f400(int *board, int *out_idx, char *out_dir)
4) If the breakpoint never hits, hunt the pointer table Search memory for the function address and break on reads:
When a read break triggers, check k and disassemble around @eip to see who is using the pointer.
Why this helps
- It gives you the actual callsite (even if indirect).
- It shows the board memory layout and who populates it.
- You can map the caller in name_map.json with real evidence.
If you want, I can translate the WinDbg outputs into specific name-map updates once you capture a hit.
Captured¶
Target: crimsonland.exe+0x40f400 (absolute 0x0040f400)
Breakpoint hit:
- EIP: 0x0040f400
- Return address (caller): 0x0040fe59
Stack / Args (cdecl):
At esp=0x0019f8fc:
- [@esp] return address: 0x0040fe59
- [@esp+4] board pointer: 0x004819ec
- [@esp+8] out_idx pointer: 0x0019f938
- [@esp+0xC] out_dir pointer: 0x0019f91f
Caller snippet (around 0x0040fe59):
0040fe59 83c40c add esp,0Ch
0040fe5c 84c0 test al,al
0040fe5e 75bd jne 0040fe1d
0040fe60 c705f02e4700ffffffff mov dword ptr [00472ef0],0FFFFFFFFh
0040fe6a 891dec244800 mov dword ptr [004824ec],ebx
0040fe70 c705e424480080250000 mov dword ptr [004824e4],25880h
0040fe7a a0101c4800 mov al,byte ptr [00481c10]
0040fe7f a802 test al,2
0040fe81 7553 jne 0040fed6
0040fe85 6830ff4000 push 0040ff30
0040fe8d c605c61b480001 mov byte ptr [00481bc6],1
0040fe94 8815101c4800 mov byte ptr [00481c10],dl
0040fe9a 881dd51b4800 mov byte ptr [00481bd5],bl
0040fea0 881dd41b4800 mov byte ptr [00481bd4],bl
0040fea6 c705d01b48000000803f mov dword ptr [00481bd0],3F800000h
0040feb0 891dcc1b4800 mov dword ptr [00481bcc],ebx
0040feb6 891dc01b4800 mov dword ptr [00481bc0],ebx
0040febc 881dc41b4800 mov byte ptr [00481bc4],bl
0040fec2 881dc51b4800 mov byte ptr [00481bc5],bl
0040fec8 891dc81b4800 mov dword ptr [00481bc8],ebx
Board dump (6x6 ints at 0x004819ec):
Interpretation¶
0x0040f400is confirmed live and receives a 6x6 int board at0x004819ec.- The return address
0x0040fe59is inside the credits secret update loop and gates UI init for globals near0x00481bc0..0x00481bd5. - The globals touched in the snippet align with credits secret state (selection index, timer, score, flags), now mapped in
data_map.json. - Remaining gap: record
*out_idxand*out_dirto confirm orientation encoding.
Session 2 (2026-01-19) - credits secret¶
Wishlist¶
- Log
*out_idxand*out_dirafter calls to0x0040f400(match-3 finder), for both hit and miss cases. - Dump globals each visit to
0x0040f4f0:0x00472ef0,0x004824e4,0x004824e8,0x004824ec,0x00481c10. - Snapshot boards and masks:
0x004819ec(6x6 ints),0x004819f0,0x004819f4,0x00481a04,0x00481a1c. - Break on writes to
0x00472ef0to capture selection/swap flow (EIP + regs + stack). - Grab a call stack the first time
0x0040f4f0runs to confirm the caller chain.
Captured¶
Match-3 finder 0x0040f400
- Hit (miss case):
- Return address:
0x0040fbdd - Args:
- board =
0x004819ec - out_idx_ptr =
0x0019f928 - out_dir_ptr =
0x0019f91f
- board =
*out_idx = 0x00000000,*out_dir = 0x00-
Board sample (6x6 ints;
-3=0xFFFFFFFD): -
Hit (success case):
- Return address:
0x0040fe59 - Args:
- board =
0x004819ec - out_idx_ptr =
0x0019f938 - out_dir_ptr =
0x0019f91f
- board =
*out_idx = 0x00000012,*out_dir = 0x01- Board sample:
Direction encoding (out_dir)
- Static analysis of
credits_secret_match3_findconfirms: out_dir = 0x01for horizontal matches (left-to-right).out_dir = 0x00for vertical matches (top-to-bottom).out_idxis the start index of the 3-tile run in row-major order (leftmost/topmost).
Selection/swap flow
ba w4 0x00472ef0hit at0x0040fe6ainside the credits secret update block.- Callsite is in the same
0x0040fe6a..0x0040fe9aregion that initializes UI globals.
Timer globals
0x004824e4is the main countdown value.- Timer update block:
0x0040f69d..0x0040f6a3(mov [0x004824e4], eax). - One-shot log:
- pre:
0x004824e4 = 0x00000000,0x004824e8 = 0x0000fd3b - post:
0x004824e4 = 0x00002580,0x004824e8 = 0x0001039a
- pre:
- Low-timer trigger:
0x0040f6a3when0x004824e4 < 0x100(observedcur = 0x47). - Timer-expire sfx:
- Breakpoint at
0x0040f6b5(call to0x0043d120,sfx_play(sfx_trooper_die_01)in decompile). - Hit with
0x004824e4 = 0xFFFFFFF9(timer already negative at callsite).
Caller chain
- Per-frame update chain repeats:
... -> crimsonland+0x4732a -> crimsonland+0x46bd7 -> crimsonland+0x1a64d -> crimsonland+0x6b2e -> grim...
Interpretation¶
0x0040f400confirmed:out_idx/out_dirare non-zero on success and zero on miss.0x004824e4is the countdown timer;0x004824e8tracks a paired value in the same update block.- The flashing Reset/Back hover effect correlates with
0x004824e4 < 0x100. out_direncodes orientation only (0 = vertical, 1 = horizontal); no absolute direction.- Still missing: exact “timer zero / dying sound” callsite; likely triggered by a different state flag or audio hook.
Session 3 (2026-01-19) - credits screen / Secret button unlock¶
Goal¶
Capture the exact unlock point for the Secret button on the credits screen and dump the injected secret lines.
Breakpoint¶
One-shot write watch on the unlock flag:
ba w1 crimsonland+0x811c4 ".printf \"[credits] Secret unlock write (DAT_004811c4)\\n\"; k; r; dd crimsonland+0x811c4 L1; dd crimsonland+0x811bc L1; dd crimsonland+0x80980 L40; gc"
Captured¶
- Hit location:
EIP=0x0040dda7(credits_screen_update+0x5a7) - Instruction:
mov edx, dword ptr [crimsonland+0x80984 + eax*8] - Registers:
EAX=0x54(line index) - Unlock flag:
DAT_004811c4 = 1 - Secret base index:
DAT_004811bc = 0x54
Secret line table dump¶
The credits line table lives at 0x00480980 and is a pair array: ptr, flags.
The secret lines begin at index 0x54, so the injected block starts at:
0x00480980 + 0x54*8 = 0x00480c20
Dumped strings (all flags 0x00000004):
0x54— "Inside Dead Let Mighty Blood"0x55— "Do Firepower See Mark Of"0x56— "The Sacrifice Old Center"0x57— "Yourself Ground First For"0x58— "Triangle Cube Last Not Flee"0x59— "0001001110000010101110011"0x5A— "0101001011100010010101100"0x5B— "011111001000111"0x5C— "(4 bits for index) <- OOOPS I meant FIVE!"0x5D— "(4 bits for index)"
Additional dump at 0x00480c70 (index 0x5E onward) was zeroed, indicating no further injected
lines beyond 0x5D.
Interpretation¶
- The Secret button unlock is driven by the credits line scan in
credits_screen_update, not by a separate hidden state. Once all required lines are flagged,DAT_004811c4is set and the secret lines are injected at index0x54.
Session 4 (2026-01-19) - console tilde hotkey¶
Goal¶
Identify the runtime path that toggles the in-game console when pressing ~.
Breakpoints¶
ba w1 crimsonland+0x7eec8 ".printf \"[console] open_flag write\\n\"; k; r; u @eip L12; dd crimsonland+0x7eec8 L1; gc"
ba w4 crimsonland+0x7f4d4 ".printf \"[console] 0x7f4d4 write\\n\"; k; r; u @eip L12; dd crimsonland+0x7f4d4 L1; gc"
bp crimsonland+0x18b0 ".printf \"[console] console_set_open\\n\"; k; r; dd @esp L4; gc"
Captured¶
- Callsite:
0x0040c39acallsconsole_set_open(0x004018b0) when~is pressed. - Call stack:
0x0040c39a -> 0x004018b0 -> DINPUT8!CDIDev_GetDeviceState -> grim... - Writes inside
console_set_open: console_open_flag(0x0047eec8) written at0x004018b7console_input_enabled(0x0047f4d4) written at0x004018bd
Disasm snippet (from console_set_open):
004018b0 8a442404 mov al,byte ptr [esp+4]
004018b7 8b0d3c084800 mov ecx,dword ptr [0048083c]
004018bd a2d4f44700 mov byte ptr [0047f4d4],al
004018c2 8b01 mov eax,dword ptr [ecx]
004018c4 ff504c call dword ptr [eax+4Ch]
004018c7 c20400 ret 4
Interpretation¶
- The tilde hotkey calls
console_set_open, not a direct flag write. - The remaining task is to identify the function containing
0x0040c39aand the specific key check (likelyDIK_GRAVE = 0x29).
Session 5 (2026-01-19) - console hotkey check (DIK_GRAVE)¶
Goal¶
Capture the actual key check that gates the console toggle.
Breakpoint¶
bp /1 crimsonland+0xc39a ".printf \"[console] hotkey callsite 0x0040c39a\\n\"; r; k; u @eip-40 L80; dd @esp L8; gc"
Captured¶
Disassembly confirms the check and toggle sequence:
0040c36d 6a29 push 29h ; DIK_GRAVE
0040c37d ff5248 call dword ptr [edx+48h]
0040c380 84c0 test al,al
0040c382 7416 je 0040c39a
0040c384 8a15c8ee4700 mov dl,[0047eec8] ; console_open_flag
0040c38f 3ad3 cmp dl,bl
0040c391 0f94c0 sete al ; al = (open_flag == 0)
0040c394 50 push eax
0040c395 e81655ffff call 004018b0 ; console_set_open
0040c39a b9a0ee4700 mov ecx,0047eea0 ; console state
0040c39f e89c56ffff call 00401a40 ; console_update
Additional context from the block entry (0x0040c360):
0040c320 6a57 push 57h
0040c322 8818 mov byte ptr [eax],bl
0040c324 8b0d3c084800 mov ecx,dword ptr [0048083c]
0040c32a 8b11 mov edx,dword ptr [ecx]
0040c32c ff5220 call dword ptr [edx+20h]
0040c32f e80c1c0100 call 0041df40
0040c334 84c0 test al,al
0040c336 7513 jne 0040c34b
0040c338 833d347048000a cmp dword ptr [00487034],0Ah
0040c33f 7e0a jle 0040c34b
0040c341 c705347048000a000000 mov dword ptr [00487034],0Ah
0040c34b 381d84af4a00 cmp byte ptr [004aaf84],bl
0040c351 740d je 0040c360
0040c353 e898e20100 call 0042a5f0
0040c358 b001 mov al,1
0040c35a 5e pop esi
0040c35b 5b pop ebx
0040c35c 83c428 add esp,28h
0040c35f c3 ret
0040c360 a144084800 mov eax,dword ptr [00480844]
Additional hotkey in the same block:
Interpretation¶
- The tilde hotkey uses Grim2D key polling (
vtable +0x48) withDIK_GRAVE (0x29). - The toggle is explicit: if
console_open_flagis 0, it passes 1 toconsole_set_open; otherwise 0. - The same function calls
console_updateimmediately after the toggle. - The hotkey block appears inside a larger per-frame input/update function that also checks
DIK_F12 (0x58). - The block is gated by
audio_suspend_flag(0x004aaf84); when non-zero, it callsaudio_resume_all(0x0042a5f0) and returns early.
Session 6 (2026-01-19) - console hotkey function entry¶
Goal¶
Find the true function entry that contains the hotkey block at 0x0040c360.
Breakpoint¶
Captured¶
- Entry hit:
0x0040c1c0 - Stack (unwind warning): call chain passes through Grim (
grim+0x3def,grim+0x6025) and acrimsonlandcaller around0x0042cf41before returning togrim!GRIM__GetInterface.
Prolog:
0040c1c0 8b0d3c084800 mov ecx,dword ptr [0048083c]
0040c1c6 83ec28 sub esp,28h
0040c1c9 8b01 mov eax,dword ptr [ecx]
0040c1cb 53 push ebx
0040c1cc 56 push esi
0040c1cd ff503c call dword ptr [eax+3Ch]
Interpretation¶
- The hotkey block (
0x0040c360) and its callsite (0x0040c39a) live inside a larger per‑frame input/update function that starts at0x0040c1c0.