mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-23 05:00:32 +02:00
346 lines
18 KiB
Markdown
346 lines
18 KiB
Markdown
# `source/networking/telnet_console.obj` — reconstruction
|
|
|
|
**Status: CLOSED — all 17 of 17 symbols byte-exact under the strict comparator
|
|
(5/5 functions, 1344/1344 `.text` bytes, all 11 string literals, `.bss`).
|
|
Was 3/5 functions, 624/1344 bytes when §5 below was written.**
|
|
**Lane:** `claude/libcmt-stream` · **Object status:** `Matching`
|
|
|
|
**Toolchain:** XDK 3911 `CL.Exe` 13.00.9254.1 · **Flags:** the game set, `/O2 /Oy- /DDEBUG /Dxbox`
|
|
**Start state:** the source was a pure skeleton — symbol-map header and empty
|
|
section comments, no code. Every symbol was missing.
|
|
|
|
| symbol | target | ours | exact |
|
|
|---|--:|--:|:--:|
|
|
| `_telnet_console_dispose` | 80 B / 7 rel | 80 B / 7 rel | **yes** |
|
|
| `_telnet_console_print` | 192 B / 18 rel | 192 B / 18 rel | **yes** |
|
|
| `_telnet_console_process` | 352 B / 33 rel | 352 B / 33 rel | **yes** |
|
|
| `_bss_00456d00` | 140 B / 0 rel | 140 B / 0 rel | **yes** |
|
|
| all 11 string literals | — | — | **yes** |
|
|
| `_telnet_console_initialize` | 224 B / 18 rel | 224 B / 18 rel | **yes** (§6) |
|
|
| `_code_001203c0` | 496 B / 30 rel | 496 B / 30 rel | **yes** (§6) |
|
|
|
|
---
|
|
|
|
## 1. The globals block
|
|
|
|
Byte-exact, which confirms the layout. Recovered by reading the **relocation
|
|
addends** — every reference into the block resolves to one of four offsets
|
|
(0x0, 0x4, 0x8, 0x88):
|
|
|
|
```c
|
|
struct telnet_client
|
|
{
|
|
struct transport_endpoint *endpoint; /* +0x04 */
|
|
char buffer[128]; /* +0x08 */
|
|
};
|
|
|
|
struct telnet_console_globals
|
|
{
|
|
struct transport_endpoint *listening_endpoint; /* +0x00 */
|
|
struct telnet_client clients[1]; /* +0x04 */
|
|
boolean initialized; /* +0x88 */
|
|
};
|
|
```
|
|
|
|
4 + 132 + 4 = 140. **`MAXIMUM_TELNET_CLIENTS` is 1**, proven by the scan loop in
|
|
`telnet_console_process`: it strides `add eax,0x84` (132 = one client) from
|
|
`globals+4` with the bound `globals+0x88`.
|
|
|
|
Defined `= {0}` so the symbol lands in `.bss` owned by this TU rather than
|
|
becoming COMMON — the `thread_win32` precedent.
|
|
|
|
## 2. Facts recovered from the disassembly
|
|
|
|
* **Port 23** (`mov word ptr [ebp-6],0x17`), `address_length` 4.
|
|
* **`create_transport_endpoint(0x12)`** — the endpoint type constant.
|
|
* **`_code_001203c0` takes its client pointer in ESI.** VC7 gives statics with
|
|
fully-known call sites a custom calling convention; the call site does
|
|
`push count; push buffer; mov esi, OFFSET globals+4; call`. Written as an
|
|
ordinary third parameter, the compiler reproduces this on its own.
|
|
* **The character switch** uses a byte index table at `+0x1e0` and a 4-entry jump
|
|
table at `+0x1d0`. Decoding both gives the cases exactly: `c=4` disconnect,
|
|
`c=8` backspace, `c=10`/`c=13` execute via `hs_compile_and_evaluate`, other
|
|
values in 4..13 skip. Printable (`isalnum`/`ispunct`/space) appends.
|
|
* **`read_endpoint` into a 32-byte stack buffer**, with the returned count
|
|
`push`ed *before* the branch so it serves as the argument to either
|
|
`code_001203c0` or `transport_error_to_string`.
|
|
|
|
## 3. Source-shape findings
|
|
|
|
Five real corrections, each verified by the gate:
|
|
|
|
1. **`print` tests its write result once, at a shared join.** January writes
|
|
`"\r\n"`, then the string, then optionally the pending input buffer, and tests
|
|
**once** at a join reached both by fall-through and by the empty-buffer branch.
|
|
A short-circuit `a>0 && b>0 && (!buf[0] || c>0)` adds a branch and shifts every
|
|
later offset. Using a `result` local and testing at the join made it exact.
|
|
2. **`process` indexes the globals directly, it does not cache a `client`
|
|
pointer.** The three missing relocations were all references to
|
|
`bss_00456d00+0x4/+0x8/+0x88`. Rewriting the second half to use
|
|
`bss_00456d00.clients[0]` throughout recovered them.
|
|
3. **`process`'s greeting branch is written failure-first.** January's layout is
|
|
`jg <assign>` falling through into the delete; writing `if (write>0) {assign}
|
|
else {delete}` produces the opposite layout. Inverting to
|
|
`if (write<=0) {delete} else {assign}` made the function exact.
|
|
4. **`initialize` uses `if (endpoint) {...} else {error}`, not an early return.**
|
|
The positive form places all three error blocks out of line at the end, in
|
|
reverse order, with the bind and listen paths tail-merging into a shared
|
|
`delete` + NULL + return (`jmp 0x97`). The early-return form emits them inline.
|
|
5. **The switch case order is `10/13`, `8`, `4`.** MSVC lays case bodies out in
|
|
source order, and January's are execute → backspace → goodbye. Writing them in
|
|
ascending value order reverses the layout: this alone took `_code_001203c0`
|
|
from 219 to 120 differing instructions.
|
|
|
|
Also: **the `transport_address` is zeroed inline in an inner scope.**
|
|
`csmemset(&address, …)` emits a real call (+2 relocations, 208 B); declaring
|
|
`= {0}` at function scope emits the zeroing before the globals `csmemset`. An
|
|
inner block opened after the endpoint check puts it where January has it.
|
|
|
|
## 4. `transport.h` — union member order
|
|
|
|
`struct transport_address_data`'s union originally listed `byte bytes[16]` first.
|
|
With that order, `struct transport_address address = {0}` makes MSVC initialise
|
|
the union's *first member* — so it emits a byte store for `bytes[0]` followed by a
|
|
misaligned blob fill (`[ebp-0x17]`, `[ebp-0x13]`, …). January's fill is six clean
|
|
4-aligned dword stores.
|
|
|
|
Reordering the union to put `unsigned long long_words[4]` first restores the
|
|
aligned fill and matches January. The union order is not observable except
|
|
through codegen. The admission lane forced clean rebuilds of both current exact
|
|
consumers: all 5 `transport_address.obj` functions and all 100
|
|
`ui_widget_event_handler_functions.obj` functions remained strict-exact.
|
|
|
|
## 5. The residual as it was diagnosed — SUPERSEDED BY §6
|
|
|
|
**This section's class verdict was wrong.** Both functions were adjudicated
|
|
here as register-allocation ties; both closed from source (§6). The measured
|
|
negatives below remain valid negatives and are kept. Two specific claims are
|
|
corrected: (a) the two functions were *not* "identical modulo register naming"
|
|
— `_code_001203c0` had two `push 0` where January has `push eax` (a 2-byte
|
|
code-size difference hidden by section alignment padding, which also displaced
|
|
the jump table), and (b) the register assignment was not a tie — it was
|
|
downstream of a live range the source failed to create, exactly the house-rules
|
|
§6 pattern "read the live ranges, never chase the registers".
|
|
|
|
### `_code_001203c0`
|
|
|
|
January puts `index` in EDI and `size` in EBX; we do the reverse. A mechanical
|
|
EBX↔EDI rename of our disassembly aligns **132 of 172** instructions, and the
|
|
residue shows it is *not* a clean swap: at the execute case both builds use EDI
|
|
for the buffer pointer (`lea edi,[esi+4]`). January therefore **reuses EDI for
|
|
both `index` and the buffer pointer**, spilling `index` and reloading `size` from
|
|
`[ebp+0xc]` each iteration; we keep the two in separate registers. Same
|
|
instruction sequence, different live-range packing.
|
|
|
|
Verified inert against it:
|
|
|
|
| attempt | ndiff |
|
|
|---|--:|
|
|
| baseline | 120 |
|
|
| `index` declared before `result` | 120 |
|
|
| `length` at function scope | 120 |
|
|
| `character` and `length` both at function scope | 120 |
|
|
| named `client_buffer` local in the append path | 119 |
|
|
| cached `limit` local for the bound | 119 |
|
|
| `buffer[index]` directly, no pointer variable | 119 |
|
|
| `size>index` | 119 |
|
|
| `index<size && result` (result tested second) | 275 |
|
|
| `while` form | 198 |
|
|
|
|
The one-instruction wins are metric noise, not progress: `size>index` emits
|
|
`cmp ebx,edi; jle` where January has `cmp edi,ebx; jge`, so `index<size` is the
|
|
faithful form and is what the source keeps.
|
|
|
|
### `_telnet_console_initialize`
|
|
|
|
January materialises `0` into **two** registers — ECX for the three address
|
|
dwords, EDX for `[ebp-8]` and `[ebp-4]` — because ECX is consumed by
|
|
`lea ecx,[ebp-0x18]` for the `&address` push. We keep one zero register and use
|
|
EDX for the `lea`, so the three error-path reloads then rotate EDX/EAX/ECX
|
|
against our EAX/ECX/EDX. The extra `xor edx,edx` is exactly the two bytes our
|
|
version spends on trailing padding — both functions are 224 bytes.
|
|
|
|
Verified inert or worse: field assignment order (`port` first), explicit
|
|
`address_type = 0` (133), `address_type = 0` before the other fields (133),
|
|
declaring `address` at function scope (115).
|
|
|
|
### Class (as adjudicated then — retracted in §6)
|
|
|
|
Same as the `woutput` residual — a register-allocation / live-range packing
|
|
decision with no source or flag lever found. Not credited; the object stays
|
|
`NonMatching`.
|
|
|
|
## 6. Close-out — both "register ties" fell to source levers (2026-08-10)
|
|
|
|
Diagnosed by a relocation-masked lockstep walk of both residuals against the
|
|
target (register- and slot-wildcarded tiers), which separated the genuine
|
|
divergences from the permutation noise the earlier ndiff metric mixed together.
|
|
|
|
### `_code_001203c0`: January passes the write result to the error report
|
|
|
|
At **both** write-failure sites January has `push eax` where we had `push 0`:
|
|
the original passes the failed `write_endpoint` result to
|
|
`transport_error_to_string`, not a literal `(short)0` — the same idiom
|
|
`telnet_console_process` already proved with its `(short)count`. Spelled
|
|
|
|
```c
|
|
length = write_endpoint(client->endpoint, character, 1);
|
|
if (length<=0)
|
|
{
|
|
error(2, "failed to write to telnet client ('%s')",
|
|
transport_error_to_string((short)length));
|
|
...
|
|
}
|
|
```
|
|
|
|
(and likewise at the overflow-message site, reusing the `length` local), the
|
|
function became **byte-exact in one step**. The named result's live range now
|
|
crosses into the error arm, and that alone flipped the entire EBX/EDI
|
|
assignment and the prologue push schedule into January's shape — the "120
|
|
differing instructions" were all downstream of these two calls. The tell in
|
|
the disassembly was 1-byte `push eax` (0x50) vs 2-byte `push 0` (0x6A 00)
|
|
with every later branch target off by one, and section size still 496 because
|
|
alignment padding absorbed the difference. **Size-exactness can hide a real
|
|
code-size delta; diff the padding tail too.**
|
|
|
|
(The earlier §5 claim that January "reuses EDI for both `index` and the buffer
|
|
pointer" was an artifact of the mechanical rename: January converts `index`
|
|
to the character pointer in place (`add edi, eax`) exactly as we do, and the
|
|
append-path buffer pointer lands in EBX. The execute path's
|
|
`lea edi,[esi+4]` matched byte-for-byte on both sides while meaning different
|
|
things — jan's EDI held the dead character pointer, ours the dead size copy.)
|
|
|
|
### `_telnet_console_initialize`: `{{0}}`, not `{0}`
|
|
|
|
January's zero-fill of the `transport_address` local splits **exactly at the
|
|
member boundary**: `[-0x18]` immediate + `[-0x14..-0xc]` from ECX cover the
|
|
16-byte `struct transport_address_data address` member, then
|
|
`lea ecx,[ebp-0x18]` consumes ECX, then `[-8]`/`[-4]` (the trailing
|
|
`address_length`/`port`/`address_type` scalars) come from a freshly
|
|
materialised EDX. Two zero sources for one initializer means two fill
|
|
operations in the IR. **VC7 groups a `= {…}` zero-fill by brace level**:
|
|
|
|
- `= {0}` → the `0` initialises the deepest first scalar
|
|
(`long_words[0]`, the immediate store) and the *rest of the whole struct*
|
|
is one fill — one zero register (ours, wrong);
|
|
- `= {{0}}` → member 0's fill and the implicit zeroing of the remaining
|
|
members are *separate* fills — the allocator can hand the address `lea` the
|
|
first fill's dead register and materialise a second zero for the tail
|
|
(January, exact).
|
|
|
|
One character of source. The three error-path register rotations §5 describes
|
|
were pure cascade from this and closed with it.
|
|
|
|
### Verification
|
|
|
|
Whole unit rebuilt via ninja (CL path patched per the known checkout issue)
|
|
and every one of the 17 target sections verified with the strict comparator
|
|
(`section_infos_equal`): **17/17 EXACT**, 5/5 functions, 1344/1344 `.text`
|
|
bytes. `configure.py progress` validation (parked/semantic/ownership gates)
|
|
passes. Status flipped `NonMatching` → `Matching`.
|
|
|
|
The §5 compiler A/B result (all three local VC7 suites byte-identical) stands
|
|
— correctly, as it turns out: the residual was never compiler variance.
|
|
|
|
### House-rule audit
|
|
|
|
Every parameter remains one-per-line, both no-argument `void` parameter lists
|
|
use the project form, and every void function ends in an explicit `return;`.
|
|
The source contains no assembly, volatile codegen forcing, alignment trick,
|
|
compiler-flag override, or raw typed `tag_get`/`object_get` cast. The early
|
|
returns in `telnet_console_print`, `telnet_console_process`, and
|
|
`code_001203c0` are measured control-flow requirements of the exact January
|
|
sections; rewriting them into one exit changes the emitted branches and is the
|
|
documented exception to the preferred single-return rule.
|
|
|
|
> Note: `ninja` cannot launch `xbox/bin/vc7/CL.Exe` in this checkout
|
|
> (`CreateProcess failed`); pre-existing, reproduces in `campaign-integration`.
|
|
> Verification used CL directly with the exact cflags ninja generates.
|
|
|
|
## 7. Current-campaign independent admission audit (2026-08-13)
|
|
|
|
The completed reconstruction was transplanted onto campaign tip `738deff8`
|
|
and rebuilt from source with XDK 3911 `CL.Exe` 13.00.9254.1 and the unchanged
|
|
`/O2 /Oy- /DDEBUG /Dxbox` flags. This audit did not trust the earlier report:
|
|
it enumerated every external, target-owned COFF section and compared each one
|
|
again with `tools/coff_compare.py::section_infos_equal`.
|
|
|
|
All **17/17 target-owned sections** are strict-equal: the five functions
|
|
(1344 padded code bytes, 106 relocations), all eleven string COMDATs (473
|
|
bytes, no relocations), and `_bss_00456d00` (140 bytes, no relocations). The
|
|
function measurements from this rebuild are:
|
|
|
|
| function | padded bytes | relocations | normalized SHA-256 |
|
|
|---|---:|---:|---|
|
|
| `_telnet_console_initialize` | 224 | 18 | `2b614d5f0126ba4234219a23adcb5e98ec7e39b2feab42b900006feab1ed1748` |
|
|
| `_telnet_console_dispose` | 80 | 7 | `bb37c5b76e11f4f7badf0fbdf0de442d5af90930871d0645d3412b483e279a2a` |
|
|
| `_telnet_console_print` | 192 | 18 | `5509b44b5d0c4ffbcda8b091d14df2ed82d3dd1748b0e7a6757edd28b4e93657` |
|
|
| `_code_001203c0` | 496 | 30 | `320e28baa4ff4a0f80d2a37ce9c2107d66c72e2bf5c321c8f205d813d25fc398` |
|
|
| `_telnet_console_process` | 352 | 33 | `6697a49e8c92a112af1c2dce74fa24f03bcbf0c7c6c014844f69c4ae25eb6715` |
|
|
|
|
The ordinary objdiff reporter still scores `_code_001203c0` at 89.14286%
|
|
because its switch-table/local-label ownership is encoded differently. The
|
|
hardened comparator proves all 496 padded bytes and all 30 resolved relocation
|
|
destinations equal, so the current fail-closed semantic ledger records that
|
|
specific false negative. `tools.audit_semantic_matches` independently accepts
|
|
it from `semantic-coff` evidence; after that entry, `ninja progress` credits
|
|
the complete object instead of revoking it.
|
|
|
|
The only shared-header change reorders the three equivalent views in
|
|
`transport_address_data` so the dword view is the aggregate's first member.
|
|
The complete current-tree include census found exactly one other C consumer,
|
|
`transport_address.c`. It was force-rebuilt, and all **42/42** of its owned
|
|
sections remained strict-equal (five functions, all string COMDATs, and its
|
|
256-byte BSS). This closes the change's measured regression radius.
|
|
|
|
Plainly: the code, its strings, its zero-initialized globals, and every call or
|
|
data reference all describe the same January object. The one progress-report
|
|
disagreement is a label-spelling artifact, not a byte or destination mismatch.
|
|
|
|
### Current house/Berth audit
|
|
|
|
- every parameter is on its own line, and every no-argument `(void)` is on its
|
|
own line;
|
|
- every void function has an explicit final `return;`;
|
|
- the few early returns are the measured exceptions required to reproduce the
|
|
original branch layout and are documented above;
|
|
- no inline assembly, `volatile`, `__forceinline`, undefined behavior,
|
|
byte-forcing, alignment pragma, flag override, raw object/tag cast, or magic
|
|
field-offset access was introduced;
|
|
- recovered numeric values are named protocol/domain constants, and recovered
|
|
storage is represented by readable structures rather than offset arithmetic.
|
|
|
|
### Final current-campaign gates
|
|
|
|
- `ninja halobetacache_build`: passed after rebuilding all 465 base objects.
|
|
- `ninja progress`: credited `source/networking/telnet_console` as the 257th
|
|
complete Halo unit; halobetacache is 20.26% object-matched (257/468), with
|
|
358614/1770166 code bytes and 3190/7574 functions credited.
|
|
- `tools.regression_gate snapshot/check --unit
|
|
source/networking/telnet_console`: passed with five `still_exact`, no changed
|
|
nonexact functions, no failures, and no warnings.
|
|
- `python -m unittest discover -s tools -p 'test_*.py' -v`: 173/173 passed.
|
|
|
|
## Compiler A/B — all three local VC7 suites produce identical bytes
|
|
|
|
Prompted by the `units.obj` compiler-provenance work
|
|
(`units-campaign-current-20260802/docs/object_matching_logs/units_obj_compiler_provenance.md`
|
|
and `units_obj_compiler_qfe_hunt.md`), the local worktrees were re-censused for
|
|
distinct compiler binaries. **Three** distinct `CL.Exe` images exist, not one:
|
|
|
|
| suite | copies | `CL.Exe` SHA-256 (16) | note |
|
|
|---|--:|---|---|
|
|
| 13.00.9254.1 | 830 | `483e00c47bb08d69` | project standard |
|
|
| 13.00.9210 | 4 | `38955691c987c64d` | genuinely different `C1`/`C2` back end |
|
|
| vsnet-beta2-rc4-9254 | 1 | `cd6b42c2e4f14140` | reports 9254 but is a distinct image |
|
|
|
|
This object was rebuilt with all three complete suites (each with its matching
|
|
`C1.dll` / `C1xx.dll` / `C2.dll`), same flags, same source. **All three produce
|
|
byte-identical output**, with the identical residual.
|
|
|
|
That is a stronger park than "only one compiler is available": the residual is
|
|
now shown not to be compiler-version variance at all. It also corrects an
|
|
earlier claim in this log that only a single CL binary existed locally — that
|
|
was wrong, and the conclusion it supported (a QFE-level codegen difference)
|
|
is not supported by this test.
|