From 4ef15a1dbe75b69c3deaf8a3afa0da8d5f3a52f3 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 25 Aug 2026 07:42:12 +0000 Subject: [PATCH] =?UTF-8?q?re:=20route=201=20is=20viable=20=E2=80=94=20XOb?= =?UTF-8?q?ject::Wait=20keeps=20`this`=20in=20%rbx=20and=20.eh=5Fframe=20r?= =?UTF-8?q?estores=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/re/BACKLOG.md | 11 +++++++ docs/re/mission-freeze-resume-spin.md | 42 +++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 543921a..1e6907b 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -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**. diff --git a/docs/re/mission-freeze-resume-spin.md b/docs/re/mission-freeze-resume-spin.md index a142471..d512972 100644 --- a/docs/re/mission-freeze-resume-spin.md +++ b/docs/re/mission-freeze-resume-spin.md @@ -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 : + 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.