re: route 1 is viable — XObject::Wait keeps this in %rbx and .eh_frame restores it

Pure static analysis, no run spent. XObject::Wait's prologue does mov %rdi,%rbx
at 8fbc9c, so the this pointer lives in a callee-saved register rather than a
stack slot. And the binary carries full unwind information: .eh_frame with
127231 FDEs, which survives in Release builds because C++ exceptions need it,
including an FDE covering 8fbc90 to 8fbde2 that tracks rbx explicitly.

Together those mean that from a thread frozen deep in pthread_cond_wait, moving
to the XObject::Wait frame and reading rbx yields the XObject being waited on --
gdb reconstructs callee-saved registers during the unwind from .eh_frame alone,
with no DWARF involved. Reading the first quadword at that pointer gives the
vtable, and vtable symbols are in the symtab, so the object's concrete type is
identifiable too.

This revises the previous entry, which listed route 1 as per-frame archaeology
that must be redone whenever the binary changes, and route 2, a RelWithDebInfo
rebuild, as what would make the question easy. Route 1 is neither expensive nor
fragile: two gdb commands per thread, no rebuild, and the oracle stays
byte-identical to the binary every other measurement in this corpus was taken
against.

Not yet executed on a frozen run, which is the next step and is now a small one.
This commit is contained in:
Sylpheed RE agent
2026-08-25 07:42:12 +00:00
parent 37ba2a9169
commit b5d1f91743
2 changed files with 53 additions and 0 deletions

View File

@@ -708,6 +708,17 @@ search cannot find a *schedule*.
archaeology; (2) a `RelWithDebInfo` build via `build-canary` — makes this and all
future freeze questions easy, at a full compile and a binary differing from the
one every other measurement used.
***(2026-08-25) Route 1 is viable and cheap — `this` is in `%rbx`.**
Static analysis, no run spent: `XObject::Wait`'s prologue does `mov %rdi,%rbx`
at `8fbc9c`, so `this` sits in a **callee-saved** register rather than a stack
slot. And the binary carries **`.eh_frame` with 127 231 FDEs** (Release builds
keep it for C++ exceptions), including one covering `8fbc90..8fbde2` that
tracks `rbx` explicitly. ⇒ from a frozen thread, **`frame 3` + `info registers
rbx` gives the `XObject*` being waited on**, and `x/gx $rbx` → vtable symbol
(in symtab) gives its concrete type — **no DWARF and no rebuild needed**.
**Revises the previous entry**, which called route 1 "per-frame archaeology"
and route 2 (RelWithDebInfo rebuild) the way to make it easy. **Next: execute on
a frozen run** — two gdb commands per thread.
* ~~🚧 BLOCKER: t=210/240 unreachable in one turn~~ — **superseded, see above**;
it rested on an untested assumption that a turn is one shell call. 595 s shell cap ~220 s boot (a ~190 s title movie that cannot be
tapped through) ~25 s startup = **~350 s observation ≈ 193 game-seconds**.

View File

@@ -728,3 +728,45 @@ Route 1 is cheaper and keeps the oracle identical; route 2 is what makes this an
every future freeze question easy. Neither is attempted here — recorded so the
next pass picks with the costs visible instead of discovering the missing DWARF
mid-run.
## ✅ 2026-08-25 — route 1 is viable: `this` lives in `%rbx`, and `.eh_frame` can restore it
The prologue-guided route turns out to need no archaeology at all, and the
groundwork is pure static analysis — no run spent finding it out.
**`XObject::Wait` keeps `this` in a callee-saved register**, not a stack slot:
```
8fbc90 <xe::kernel::XObject::Wait(unsigned int, unsigned int, unsigned int, unsigned long*)>:
8fbc90 push %rbp / push %r15 / push %r14 / push %rbx / push %rax
8fbc97 mov %r8,%r15
8fbc9a mov %ecx,%ebp
8fbc9c mov %rdi,%rbx <-- `this`
```
**And the binary has full unwind information**, which is what makes that
recoverable from a deep frame. `.eh_frame` is present with **127 231 FDEs** — it
survives in Release builds because C++ exceptions need it — and the FDE covering
`Wait` tracks `rbx` explicitly:
```
FDE pc=00000000008fbc90..00000000008fbde2
LOC CFA rbx rbp r14 r15 ra
8fbc90 rsp+8 u u u u c-8
8fbc91 rsp+16 u u u u c-8
```
So from a frozen thread parked in `pthread_cond_wait`, `frame 3` (the
`XObject::Wait` frame) plus `info registers rbx` yields the **`XObject*` being
waited on** — gdb reconstructs callee-saved registers during the unwind from
`.eh_frame` alone. Reading `x/gx $rbx` then gives the vtable pointer, and vtable
symbols *are* in the symtab (`_ZTVN2xe6kernel6XEventE` and friends), so the
object's concrete type is identifiable without any debug info.
**Revises the previous entry**, which listed route 1 as "per-frame archaeology"
and route 2 (a `RelWithDebInfo` rebuild) as the way to make this easy. Route 1 is
neither expensive nor fragile: two gdb commands per thread, no rebuild, and the
oracle stays byte-identical to the binary every other measurement used.
Not yet executed on a frozen run — that is the next step, and it is now a small
one.