236 lines
No EOL
15 KiB
Markdown
236 lines
No EOL
15 KiB
Markdown
# Usecode Debugger Break-State Layout
|
|
|
|
## Purpose
|
|
|
|
This note captures the current class-lift-relevant evidence for the dormant seg1408 debugger-state object.
|
|
|
|
The retail binary still appears to leave this family orphaned at runtime, but the object model itself is strong enough to justify explicit class authoring in Ghidra.
|
|
|
|
Current working family name:
|
|
|
|
- `UsecodeDebuggerBreakState`
|
|
|
|
## Current Best Class-Level Read
|
|
|
|
`UsecodeDebuggerBreakState` is a retained debugger object that owns:
|
|
|
|
- a small breakpoint table
|
|
- current interpreted-line state
|
|
- single-step / break-armed flags
|
|
- a callstack entry stack
|
|
- a small vtable-backed break/notify surface used by the interpreter callback lane
|
|
|
|
The compiled interpreter still calls into this object when the global debugger-state pointer is non-null, even though the retail binary no longer seems to instantiate it during normal play.
|
|
|
|
## Strongest Evidence Anchors
|
|
|
|
### Constructor
|
|
|
|
#### `1408:0000` `Create`
|
|
|
|
Current verified behavior:
|
|
|
|
- allocates `0x2f2` bytes when `this == null`
|
|
- writes retail vtable offset `0x65ab` at object `+0x00`
|
|
- fills the breakpoint-entry region starting at `+0x04` with `0xffff`
|
|
- clears `+0x02`, `+0x75`, and `+0x7a`
|
|
- returns the object far pointer in `DX:AX`
|
|
|
|
This is a real constructor-style path, not just a helper wrapper.
|
|
|
|
### Break gate
|
|
|
|
#### `1408:0053` `MaybeBreakOnCurrentLine`
|
|
|
|
Current verified behavior:
|
|
|
|
- stores the incoming interpreted line minus one at `+0x72`
|
|
- resolves the current unit-name pointer through `CurrentEntryGetUnitName`
|
|
- checks file+line breakpoints through `HasBreakpoint`
|
|
- dispatches through the object vtable when a break condition is met
|
|
|
|
This is the strongest proof that the hidden debugger lane is object-based and that the interpreter-side callback still expects a live debugger object.
|
|
|
|
### Breakpoint table helpers
|
|
|
|
#### `1408:00dd` `BreakpointInsertSorted`
|
|
|
|
Current verified behavior:
|
|
|
|
- enforces a maximum of ten breakpoint entries
|
|
- scans the `0x0b`-byte breakpoint-entry table rooted at `+0x04`
|
|
- compares unit-name strings via the common string helper
|
|
- inserts a new `(unit_name, line_number)` pair into the sorted table
|
|
|
|
#### `1408:01a5` `BreakpointRemove`
|
|
|
|
Current verified behavior:
|
|
|
|
- scans the same `0x0b`-byte breakpoint-entry table for an exact `(unit_name, line_number)` match
|
|
- compares the stored inline name bytes first, then the stored line word at entry `+0x09`
|
|
- compacts the remaining entries downward when a match is found
|
|
- decrements `breakpoint_count`
|
|
|
|
#### `1408:029e` `HasBreakpoint`
|
|
|
|
Current verified behavior:
|
|
|
|
- scans the same breakpoint-entry table
|
|
- compares the requested line number against entry `+0x09`
|
|
- compares the requested unit-name pair against the stored name bytes
|
|
- returns a boolean-style `uint` in `AX`
|
|
|
|
### Callstack helpers
|
|
|
|
#### `1408:02f5` `CallstackPushFrame`
|
|
|
|
Current verified behavior:
|
|
|
|
- computes the current callstack-entry base as `this + 0x7c + callstack_depth * 0x15`
|
|
- copies an inline unit-name buffer into entry `+0x00`
|
|
- enforces a maximum visible unit-name length of eight characters plus terminator
|
|
- stores three trailing far-pointer/state dwords at `+0x09`, `+0x0d`, and `+0x11`
|
|
- increments `callstack_depth`
|
|
|
|
#### `1408:03b0` `CallstackPushEntry`
|
|
|
|
Current verified behavior:
|
|
|
|
- uses `+0x7a` as the current callstack depth
|
|
- acts as a thinner wrapper over `CallstackPushFrame` when only the inline unit-name payload matters
|
|
- increments the depth and asserts when it reaches `0x1e`
|
|
|
|
#### `1408:03f7` `CallstackPopEntry`
|
|
|
|
Current verified behavior:
|
|
|
|
- decrements `+0x7a`
|
|
- asserts if the depth underflows below zero
|
|
|
|
### Step-state helpers
|
|
|
|
#### `1408:0419` `EnableSingleStep`
|
|
|
|
- clears `+0x76/+0x78`
|
|
- sets `+0x75 = 1`
|
|
|
|
#### `1408:0432` `ClearStepState`
|
|
|
|
- clears `+0x74`
|
|
- clears `+0x75`
|
|
|
|
### Current-entry name accessor
|
|
|
|
#### `1408:0444` `CurrentEntryGetUnitName`
|
|
|
|
Current verified behavior:
|
|
|
|
- returns null when `callstack_depth <= 0`
|
|
- otherwise returns a far pointer to the current callstack entry's inline unit-name buffer
|
|
|
|
## Recovered Entry Schemas
|
|
|
|
The live debugger-state model is now strong enough to split the old table blobs into concrete fixed-size entry records.
|
|
|
|
### `UsecodeDebuggerBreakpointEntry` (`0x0b` bytes)
|
|
|
|
| Offset | Current name | Confidence | Current meaning |
|
|
|---|---|---|---|
|
|
| `+0x00..+0x08` | `unit_name_inline[9]` | High | Inline unit-name buffer, consistent with eight visible characters plus terminator. |
|
|
| `+0x09` | `line_number` | High | Breakpoint line number compared by `BreakpointInsertSorted`, `BreakpointRemove`, and `HasBreakpoint`. |
|
|
|
|
### `UsecodeDebuggerCallstackEntry` (`0x15` bytes)
|
|
|
|
| Offset | Current name | Confidence | Current meaning |
|
|
|---|---|---|---|
|
|
| `+0x00..+0x08` | `unit_name_inline[9]` | High | Inline unit-name buffer for the active frame. |
|
|
| `+0x09` | `source_stream_cursor_farptr` | High | Far pointer to the current debugger/source descriptor stream cursor. `Debugump_13a0_0291` passes this lane into `13a0:045c`, which reads descriptor bytes and inline strings directly from it. This field name is now promoted into the live datatype in `CRUSADER.EXE`, not just the note set. |
|
|
| `+0x0d` | `current_frame_payload_farptr` | High | Far pointer to the current frame payload/evaluation context at `frame_base + 0x04`. `13a0:045c` dereferences this lane while formatting debugger dump/watch text. |
|
|
| `+0x11` | `aux_farptr` | Low | Trailing auxiliary far pointer slot; still zero in the only verified `CallstackPushFrame` caller and still lacks a confirmed current-entry consumer. |
|
|
|
|
## Current Working Layout
|
|
|
|
The live datatype `/Remorse/UsecodeDebuggerBreakState` now exists in-session with the currently safest anchors:
|
|
|
|
| Offset | Current name | Confidence | Current meaning |
|
|
|---|---|---|---|
|
|
| `+0x00` | `vtable_offset` | High | Retail debugger-state vtable offset `0x65ab`. |
|
|
| `+0x02` | `breakpoint_count` | High | Count of `0x0b`-byte breakpoint entries. |
|
|
| `+0x04..+0x71` | `breakpoint_entries[10]` | High | Ten inline `UsecodeDebuggerBreakpointEntry` records. |
|
|
| `+0x72` | `current_line` | High | Current interpreted line minus one. |
|
|
| `+0x74` | `break_armed` | Medium | Break/armed flag cleared by `ClearStepState`. |
|
|
| `+0x75` | `single_step_enabled` | High | Single-step flag set by `EnableSingleStep`. |
|
|
| `+0x76/+0x78` | `step_state_lo/hi` | Medium | Step-state pair cleared by `EnableSingleStep`. |
|
|
| `+0x7a` | `callstack_depth` | High | Current callstack depth. |
|
|
| `+0x7c..+0x2f1` | `callstack_entries[30]` | High | Thirty inline `UsecodeDebuggerCallstackEntry` records. |
|
|
|
|
## Live Ghidra Authoring Status
|
|
|
|
Verified first live class batch landed on 2026-04-06.
|
|
|
|
- Created class owner `Remorse::UsecodeDebuggerBreakState`.
|
|
- Created `/Remorse/UsecodeDebuggerBreakpointEntry` (`0x0b`) and `/Remorse/UsecodeDebuggerCallstackEntry` (`0x15`) in the live data-type manager.
|
|
- Rewrote `/Remorse/UsecodeDebuggerBreakState` in-session so the old blob regions are now explicit `UsecodeDebuggerBreakpointEntry[10]` and `UsecodeDebuggerCallstackEntry[30]` arrays at the recovered offsets.
|
|
- Moved the main seg1408 helpers under the class owner:
|
|
- `1408:0000` -> `Create`
|
|
- `1408:0053` -> `MaybeBreakOnCurrentLine`
|
|
- `1408:00dd` -> `BreakpointInsertSorted`
|
|
- `1408:01a5` -> `BreakpointRemove`
|
|
- `1408:0230` -> `BreakpointFindFirstForUnitAtOrAfterLine`
|
|
- `1408:029e` -> `HasBreakpoint`
|
|
- `1408:02f5` -> `CallstackPushFrame`
|
|
- `1408:03b0` -> `CallstackPushEntry`
|
|
- `1408:03f7` -> `CallstackPopEntry`
|
|
- `1408:0419` -> `EnableSingleStep`
|
|
- `1408:0432` -> `ClearStepState`
|
|
- `1408:0444` -> `CurrentEntryGetUnitName`
|
|
- Promoted the remaining bounded breakpoint-table helper `1408:0230` under the class owner as `BreakpointFindFirstForUnitAtOrAfterLine`; it now reads as the lower-bound search over `breakpoint_entries` for a given `(unit_name, line_number)` query instead of an anonymous seg1408 utility.
|
|
- Resolved the retail debugger vtable root at `1478:65ab` one step further: slot 0 points to `1408:046f` and slot 1 points to `1408:0474`, and both entries are now class-owned as `OnBreakTriggeredNoop` and `VtableSlot1ReturnZero` with explicit comments that the shipped retail implementations are inert stubs.
|
|
- Tightened the live method signatures to explicit object-style forms, including:
|
|
- `UsecodeDebuggerBreakState * __cdecl16far Create(UsecodeDebuggerBreakState * this, dword init_spec)`
|
|
- `void __cdecl16far MaybeBreakOnCurrentLine(UsecodeDebuggerBreakState * this, word current_line)`
|
|
- `byte __cdecl16far BreakpointInsertSorted(UsecodeDebuggerBreakState * this, dword unit_name_farptr, word line_number)`
|
|
- `void __cdecl16far BreakpointRemove(UsecodeDebuggerBreakState * this, dword unit_name_farptr, word line_number)`
|
|
- `int __cdecl16far BreakpointFindFirstForUnitAtOrAfterLine(UsecodeDebuggerBreakState * this, dword unit_name_farptr, word line_number)`
|
|
- `uint __cdecl16far HasBreakpoint(UsecodeDebuggerBreakState * this, dword unit_name_farptr, word line_number)`
|
|
- `void __cdecl16far CallstackPushFrame(UsecodeDebuggerBreakState * this, dword unit_name_farptr, dword source_stream_target_farptr, dword current_frame_payload_farptr, dword aux_farptr)`
|
|
- `byte __cdecl16far CallstackPushEntry(UsecodeDebuggerBreakState * this, dword unit_name_farptr)`
|
|
- `void __cdecl16far CallstackPopEntry(UsecodeDebuggerBreakState * this)`
|
|
- `void __cdecl16far EnableSingleStep(UsecodeDebuggerBreakState * this)`
|
|
- `void __cdecl16far ClearStepState(UsecodeDebuggerBreakState * this)`
|
|
- `dword __cdecl16far CurrentEntryGetUnitName(UsecodeDebuggerBreakState * this)`
|
|
- The only verified `CallstackPushFrame` caller at `1418:051d` is now constrained a little more tightly in-session too: the trailing payload is still best kept as `source_stream_target_farptr`, `current_frame_payload_farptr`, and `aux_farptr`, but the raw stack setup now confirms that the last slot is literal zero in retail, the middle slot is pushed from `frame_base + 0x04`, and the first slot is pushed from the interpreter `+0xd6/+0xd8` source-stream lane plus a local offset.
|
|
- The next seg109 consumer pass closed the main follow-up from that batch. `13a0:0291 Debugump_13a0_0291` now has a decompiler comment showing that it resolves the current callstack entry as `this + 0x67 + depth * 0x15`, passes entry `+0x09` as the descriptor/source-stream cursor, and passes entry `+0x0d` as the frame payload context into `13a0:045c`.
|
|
- `13a0:045c FUN_13a0_045c` now also carries a decompiler comment recording the same split from the consumer side: it reads descriptor bytes and inline strings directly from the `+0x09` lane and dereferences the `+0x0d` lane as the evaluation context while formatting debugger dump/watch text into the shared output buffer at `0x45a6`.
|
|
- The `CallstackPushFrame` comment in seg1408 was updated to reflect that narrower live read: `+0x09` is no longer just a generic source-derived far pointer, but a real source-stream cursor used by the seg109 formatter path.
|
|
- That naming decision is now applied live too. `/Remorse/UsecodeDebuggerCallstackEntry` now exposes `source_stream_cursor_farptr` at offset `+0x09` with a matching field comment in the datatype manager, and `CallstackPushFrame` now uses `source_stream_cursor_farptr` in its parameter list instead of the older `source_stream_target_farptr` wording.
|
|
- The seg109 consumer helpers are now named live as stable free functions rather than left comment-only:
|
|
- `13a0:0291` -> `usecode_debugger_format_expression_to_shared_buffer`
|
|
- `13a0:045c` -> `usecode_debugger_format_descriptor_expression`
|
|
- Those names are still intentionally conservative. They record the verified behavior now visible in both producer-side and consumer-side comments: `13a0:0291` resolves the current debugger callstack entry and writes the formatted result into the shared output buffer, while `13a0:045c` interprets the descriptor/source-stream cursor plus frame payload context rather than merely dumping raw bytes.
|
|
- Added decompiler comments on the breakpoint and callstack helpers so the recovered inline-record layout is visible in-session even before every field is formally typed.
|
|
- Added decompiler comments on the only verified `Interpreter_NextUsecodeOp` caller of `CallstackPushFrame`, which confirms the current live read of the three trailing callstack dwords:
|
|
- `source_stream_target_farptr` is source-stream-derived from the interpreter `+0xd6/+0xd8` lane plus one fetched word
|
|
- `current_frame_payload_farptr` is current-frame-derived from the `frame_base + 0x04` lane
|
|
- `aux_farptr` is still zero in the only verified caller
|
|
- A second caller pass on 2026-04-08 still did not widen that lane: `get_callers(1408:02f5)` still reports only `1418:051d Interpreter_NextUsecodeOp`, and the live `CallstackPushFrame` comment now records that the trailing `aux_farptr` slot remains literal zero in retail while the current seg109 consumers still only read `+0x09` and `+0x0d`.
|
|
|
|
## Current Cautions
|
|
|
|
- The retail instantiation path still appears absent; no normal caller currently reaches `Create` in the unpatched retail binary.
|
|
- The record boundaries inside both tables are now landed in the live datatype, and the first two trailing callstack dwords now have both producer-side and seg109 consumer-side structural evidence plus live datatype names/comments. The remaining uncertainty is no longer whether those lanes are meaningful, but whether they should ever become more subsystem-specific than `source_stream_cursor_farptr` and `current_frame_payload_farptr`.
|
|
- The retail vtable root is no longer an open slot-map question for the first two entries. What remains open is whether any non-retail or UI-side build ever installed non-stub behavior behind those same callback positions.
|
|
- `init_spec` on `Create` and `unit_name_farptr` on the breakpoint/callstack helpers are intentionally neutral names; the live signatures are object-correct, but the payload semantics should stay conservative.
|
|
|
|
## Best Next Moves
|
|
|
|
1. Cross-check the seg1408 class note against the interpreter callback site at `1418:04b5` so the dormant-orphan lifecycle remains explicit in the live notes now that slot 0 is confirmed to land on an inert retail stub.
|
|
2. Decide whether `aux_farptr` should remain neutral or can be promoted after one more caller or consumer pass.
|
|
3. If the debugger family is revisited again, prioritize non-retail callback behavior or instantiation evidence before spending more time on the already-stable retail formatter lane.
|
|
4. If the debugger family stalls there, switch to the next planned class-lift family instead of overworking this orphaned subsystem.
|
|
|
|
## Bottom Line
|
|
|
|
`UsecodeDebuggerBreakState` is now past the “interesting orphan subsystem” stage and into real class territory.
|
|
|
|
The live database now has a bounded debugger-state class with a constructor, breakpoint gate, breakpoint table helpers, callstack helpers, explicit recovered `0x0b` / `0x15` entry schemas, and step-state helpers, even though the retail game still appears to leave that object dormant. |