Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 1m34s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped
Snapshot for handoff. Contains the mission-audio + threading fixes and the
crash-investigation instrumentation (all diagnostic cvars default-OFF).
Fixes (behavioral):
- threading_posix.cc: reap-once guard on PosixCondition<Thread>::post_execution
(double pthread_join at mission teardown -> fault loop -> audio death + freeze).
- xma_decoder.cc: work_event_->Set() in Pause() so the idle XMA worker observes
paused_ and signals pause_fence_ (Pause() deadlock -> permanent audio death).
- audio_system / xma_context_master / xboxkrnl_audio / apu_flags / alsa: mission
audio keepalive + guest_audio_flags + watchdogs.
Instrumentation (additive, default-off): xboxkrnl_debug cache-throw diag +
guest-catch dispatcher, xex_module PE/PDATA/EH scans, kernel_state mem_watch
(NOTE: mem_watch DEFAULTS TRUE -- an always-on host poll thread; prime crash suspect).
NEW ORACLE (see HANDOFF-crash-oracle-2026-07-16.md): stock 6e5b8324f built with
our toolchain + zero custom code = NO crash, NO sound-stop, plays the Ready Room.
=> the Ready-Room out_of_range crash is introduced by THESE changes, not the game
and not the (LTO-broken) build chain. Bisection plan + suspect ranking in the note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
702 lines
28 KiB
C++
702 lines
28 KiB
C++
/**
|
|
******************************************************************************
|
|
* Xenia : Xbox 360 Emulator Research Project *
|
|
******************************************************************************
|
|
* Copyright 2022 Ben Vanik. All rights reserved. *
|
|
* Released under the BSD license - see LICENSE in the root for more details. *
|
|
******************************************************************************
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include <map>
|
|
#include <set>
|
|
#include <vector>
|
|
|
|
#include "xenia/base/cvar.h"
|
|
#include "xenia/base/debugging.h"
|
|
#include "xenia/base/logging.h"
|
|
#include "xenia/cpu/backend/backend.h"
|
|
#include "xenia/cpu/function.h"
|
|
#include "xenia/cpu/processor.h"
|
|
#include "xenia/emulator.h"
|
|
#include "xenia/kernel/kernel_state.h"
|
|
#include "xenia/kernel/util/shim_utils.h"
|
|
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
|
|
#include "xenia/kernel/xthread.h"
|
|
#include "xenia/ui/imgui_dialog.h"
|
|
#include "xenia/ui/imgui_drawer.h"
|
|
#include "xenia/ui/window.h"
|
|
#include "xenia/ui/windowed_app_context.h"
|
|
|
|
DEFINE_bool(
|
|
cache_throw_diag, false,
|
|
"Run the heavy guest-throw diagnostics (LogGuestThrow / cache-manager dump / "
|
|
"catch-frame walk) on every guest C++ throw. OFF by default: in missions the "
|
|
"cache throws constantly and these dumps allocate gigabytes -- they are for "
|
|
"targeted debugging only.",
|
|
"Kernel");
|
|
|
|
DEFINE_bool(
|
|
eh_dispatch, false,
|
|
"Dispatch guest C++ exceptions (0xE06D7363) to their catch handler instead "
|
|
"of letting the throw fall through. NOTE: disproven as a fix for the Project "
|
|
"Sylpheed cache crash (that throw is uncaught by design -- its stack ends at "
|
|
"the thread-entry Execute boundary); kept as inert infra + diagnostics.",
|
|
"Kernel");
|
|
|
|
namespace xe {
|
|
namespace kernel {
|
|
namespace xboxkrnl {
|
|
|
|
void DbgBreakPoint_entry() { xe::debugging::Break(); }
|
|
DECLARE_XBOXKRNL_EXPORT2(DbgBreakPoint, kDebug, kStub, kImportant);
|
|
|
|
// https://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx
|
|
typedef struct {
|
|
xe::be<uint32_t> type;
|
|
xe::be<uint32_t> name_ptr;
|
|
xe::be<uint32_t> thread_id;
|
|
xe::be<uint32_t> flags;
|
|
} X_THREADNAME_INFO;
|
|
static_assert_size(X_THREADNAME_INFO, 0x10);
|
|
|
|
void HandleSetThreadName(pointer_t<X_EXCEPTION_RECORD> record) {
|
|
// SetThreadName. FFS.
|
|
// https://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx
|
|
|
|
// TODO(benvanik): check record->number_parameters to make sure it's a
|
|
// correct size.
|
|
auto thread_info =
|
|
reinterpret_cast<X_THREADNAME_INFO*>(&record->exception_information[0]);
|
|
|
|
assert_true(thread_info->type == 0x1000);
|
|
|
|
if (!thread_info->name_ptr) {
|
|
XELOGD("SetThreadName called with null name_ptr");
|
|
return;
|
|
}
|
|
|
|
// 4D5307D6 (and its demo) has a bug where it ends up passing freed memory for
|
|
// the name, so at the point of SetThreadName it's filled with junk.
|
|
|
|
// TODO(gibbed): cvar for thread name encoding for conversion, some games use
|
|
// SJIS and there's no way to automatically know this.
|
|
auto name = std::string(
|
|
kernel_memory()->TranslateVirtual<const char*>(thread_info->name_ptr));
|
|
std::replace_if(
|
|
name.begin(), name.end(), [](auto c) { return c < 32 || c > 127; }, '?');
|
|
|
|
object_ref<XThread> thread;
|
|
if (thread_info->thread_id == -1) {
|
|
// Current thread.
|
|
thread = retain_object(XThread::GetCurrentThread());
|
|
} else {
|
|
// Lookup thread by ID.
|
|
thread = kernel_state()->GetThreadByID(thread_info->thread_id);
|
|
}
|
|
|
|
if (thread) {
|
|
XELOGD("SetThreadName({}, {})", thread->thread_id(), name);
|
|
thread->set_name(name);
|
|
}
|
|
|
|
// TODO(benvanik): unwinding required here?
|
|
}
|
|
|
|
typedef struct {
|
|
xe::be<int32_t> mdisp;
|
|
xe::be<int32_t> pdisp;
|
|
xe::be<int32_t> vdisp;
|
|
} x_PMD;
|
|
|
|
typedef struct {
|
|
xe::be<uint32_t> properties;
|
|
xe::be<uint32_t> type_ptr;
|
|
x_PMD this_displacement;
|
|
xe::be<int32_t> size_or_offset;
|
|
xe::be<uint32_t> copy_function_ptr;
|
|
} x_s__CatchableType;
|
|
|
|
typedef struct {
|
|
xe::be<int32_t> number_catchable_types;
|
|
xe::be<uint32_t> catchable_type_ptrs[1];
|
|
} x_s__CatchableTypeArray;
|
|
|
|
typedef struct {
|
|
xe::be<uint32_t> attributes;
|
|
xe::be<uint32_t> unwind_ptr;
|
|
xe::be<uint32_t> forward_compat_ptr;
|
|
xe::be<uint32_t> catchable_type_array_ptr;
|
|
} x_s__ThrowInfo;
|
|
|
|
// Read a guest C-string, bounded.
|
|
static std::string GuestCString(uint32_t guest_ptr, size_t max_len = 128) {
|
|
if (!guest_ptr) {
|
|
return {};
|
|
}
|
|
auto p = kernel_memory()->TranslateVirtual<const char*>(guest_ptr);
|
|
if (!p) {
|
|
return {};
|
|
}
|
|
std::string s;
|
|
for (size_t i = 0; i < max_len && p[i]; ++i) {
|
|
s.push_back(p[i]);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
// Name the thrown C++ type via the MSVC RTTI chain hanging off its vtable:
|
|
// object -> vtable ; vtable[-1] -> CompleteObjectLocator ; COL+0xC ->
|
|
// TypeDescriptor ; TypeDescriptor+8 -> mangled name (".?AVout_of_range@std@@")
|
|
static std::string ThrownTypeName(uint32_t thrown_ptr) {
|
|
if (!thrown_ptr) {
|
|
return "<null>";
|
|
}
|
|
auto rd32 = [](uint32_t va) -> uint32_t {
|
|
auto p = kernel_memory()->TranslateVirtual<xe::be<uint32_t>*>(va);
|
|
return p ? static_cast<uint32_t>(*p) : 0;
|
|
};
|
|
const uint32_t vtable = rd32(thrown_ptr);
|
|
if (!vtable) {
|
|
return "<no vtable>";
|
|
}
|
|
const uint32_t col = rd32(vtable - 4);
|
|
if (!col) {
|
|
return "<no RTTI>";
|
|
}
|
|
const uint32_t type_desc = rd32(col + 0x0C);
|
|
if (!type_desc) {
|
|
return "<no type descriptor>";
|
|
}
|
|
return GuestCString(type_desc + 8);
|
|
}
|
|
|
|
// Diagnostics for the swallowed-throw crash: xenia does NOT dispatch guest C++
|
|
// exceptions (see below), so a throw returns and the game crashes in the
|
|
// unreachable code after it. By then the crash dump's registers are already
|
|
// clobbered. Capture the guest state HERE, while the throwing call frame is
|
|
// still intact -- the thrown type, the registers (the failing lookup's key and
|
|
// loop state live in these), and the guest call stack.
|
|
// Dump the asset-cache-manager LRU state at the moment it throws out_of_range,
|
|
// so we can see WHICH access-order deque hash-pair is missing from the entry
|
|
// map (the desync that crashes in sub_8245A098). r25 survives the throw and
|
|
// holds the cache-manager global (guest VA 0x828F4838). Layout (from RE):
|
|
// OBJ+0x30 = std::list<Entry> (map is rebuilt from this, keyed by hash-pair)
|
|
// OBJ+0x48 = std::deque<hashpair(8B)>: +0x4C block-map ptr, +0x50 map_size,
|
|
// +0x54 start off, +0x58 element count. Element i (8-byte, 2/block):
|
|
// bi=((start+i)/2); if bi>=map_size bi-=map_size; addr=blockmap[bi]+(i&1)*8
|
|
static void DumpCacheManagerOnThrow(const ppc_context_t& ctx) {
|
|
const uint32_t OBJ = 0x828F4838u;
|
|
if (static_cast<uint32_t>(ctx->r[25]) != OBJ) {
|
|
return; // not the cache-manager map::at that threw
|
|
}
|
|
auto rd = [](uint32_t va) -> uint32_t {
|
|
auto p = kernel_memory()->TranslateVirtual<xe::be<uint32_t>*>(va);
|
|
return p ? static_cast<uint32_t>(*p) : 0;
|
|
};
|
|
const uint32_t deque_count = rd(OBJ + 0x58);
|
|
const uint32_t list_count = rd(OBJ + 0x38);
|
|
const uint32_t blockmap = rd(OBJ + 0x4C);
|
|
const uint32_t map_size = rd(OBJ + 0x50);
|
|
const uint32_t start = rd(OBJ + 0x54);
|
|
XELOGE(
|
|
"CACHE-DUMP deque_count={} list_count={} blockmap={:08X} map_size={} "
|
|
"start={}",
|
|
deque_count, list_count, blockmap, map_size, start);
|
|
// Raw header words -- resolve the real container layout without guessing.
|
|
auto rawrow = [&](const char* tag, uint32_t base, int words) {
|
|
std::string s;
|
|
for (int i = 0; i < words; ++i)
|
|
s += fmt::format(" +{:X}={:08X}", i * 4, rd(base + i * 4));
|
|
XELOGE("CACHE-RAW {} @{:08X}:{}", tag, base, s);
|
|
};
|
|
rawrow("mgr+0x28", OBJ + 0x28, 16); // map(+0x30) + deque(+0x48) headers
|
|
// deque hash-pairs (access order)
|
|
std::string dq;
|
|
const uint32_t n = deque_count > 512 ? 512 : deque_count; // sanity cap
|
|
for (uint32_t i = 0; i < n; ++i) {
|
|
uint32_t bi = (start + i) / 2;
|
|
if (map_size && bi >= map_size) {
|
|
bi -= map_size;
|
|
}
|
|
const uint32_t block = rd(blockmap + bi * 4);
|
|
const uint32_t addr = block + (i & 1) * 8;
|
|
dq += fmt::format(" [{}]{:08X}:{:08X}", i, rd(addr), rd(addr + 4));
|
|
}
|
|
XELOGE("CACHE-DUMP deque hashpairs:{}", dq);
|
|
|
|
// Walk the entry map at OBJ+0x30 (MSVC _Tree, CONFIRMED by live raw dump:
|
|
// _Left@+0, _Parent@+4, _Right@+8, keyLo@+12, keyHi@+16, _Isnil@+25). Map
|
|
// object header is at OBJ+0x30; head=[OBJ+0x34]; root=head._Parent=[head+4].
|
|
// This is the map sub_8245A098 queries per deque entry.
|
|
std::set<uint64_t> map_keys;
|
|
const uint32_t head = rd(OBJ + 0x34);
|
|
const uint32_t root = head ? rd(head + 4) : 0;
|
|
std::vector<uint32_t> stack;
|
|
if (root && (rd(root + 25) & 0xFF) == 0) {
|
|
stack.push_back(root);
|
|
}
|
|
// Hard guards: a corrupted map (cyclic/garbage node pointers -- common at the
|
|
// moment it throws) would otherwise balloon this walk to gigabytes.
|
|
uint32_t visits = 0;
|
|
while (!stack.empty() && map_keys.size() < 4096 && ++visits < 100000 &&
|
|
stack.size() < 100000) {
|
|
const uint32_t nd = stack.back();
|
|
stack.pop_back();
|
|
const uint64_t key = (static_cast<uint64_t>(rd(nd + 12)) << 32) | rd(nd + 16);
|
|
map_keys.insert(key);
|
|
const uint32_t l = rd(nd + 0), r = rd(nd + 8);
|
|
if (l && (rd(l + 25) & 0xFF) == 0) stack.push_back(l);
|
|
if (r && (rd(r + 25) & 0xFF) == 0) stack.push_back(r);
|
|
}
|
|
// For each deque key, flag whether it exists in the persistent map. The
|
|
// orphan(s) = deque keys absent from the map == the cause of the map::at miss.
|
|
std::string orphans;
|
|
uint32_t orphan_count = 0;
|
|
for (uint32_t i = 0; i < n; ++i) {
|
|
uint32_t bi = (start + i) / 2;
|
|
if (map_size && bi >= map_size) bi -= map_size;
|
|
const uint32_t block = rd(blockmap + bi * 4);
|
|
const uint32_t addr = block + (i & 1) * 8;
|
|
const uint64_t key =
|
|
(static_cast<uint64_t>(rd(addr)) << 32) | rd(addr + 4);
|
|
if (!map_keys.count(key)) {
|
|
orphans += fmt::format(" [{}]{:08X}:{:08X}", i, rd(addr), rd(addr + 4));
|
|
++orphan_count;
|
|
}
|
|
}
|
|
XELOGE("CACHE-DUMP map_keys={} deque_orphans={} (deque keys NOT in map):{}",
|
|
static_cast<uint32_t>(map_keys.size()), orphan_count, orphans);
|
|
|
|
// --- SNAPSHOT vs LIVE: prove the TOCTOU race. The flush (sub_8245A098) locks,
|
|
// copies OBJ+0x30 into a temp map at [flush_r31+104], UNLOCKS (0x8245a150),
|
|
// then iterates the live deque doing map::at on the FROZEN snapshot. A
|
|
// concurrent LOCKED add (8245ABD8/8245AD00) pushes a deque + map entry during
|
|
// the unlocked window -> that key is in the LIVE map but NOT the snapshot ->
|
|
// map::at throws. Find the flush frame (its live PC is in the map::at loop);
|
|
// its snapshot map object is at frame_sp+104 (== r26 at the throw).
|
|
uint32_t flush_sp = 0;
|
|
{
|
|
uint32_t sp = static_cast<uint32_t>(ctx->r[1]);
|
|
for (int depth = 0; depth < 128 && sp; ++depth) {
|
|
auto f = kernel_memory()->TranslateVirtual(sp);
|
|
if (!f) break;
|
|
uint32_t nsp = xe::load_and_swap<uint32_t>(f);
|
|
if (nsp <= sp || nsp - sp > 0x1000000) break;
|
|
auto lp = kernel_memory()->TranslateVirtual(nsp - 8);
|
|
uint32_t pc = lp ? xe::load_and_swap<uint32_t>(lp) : 0;
|
|
if (pc >= 0x8245A154u && pc <= 0x8245A1D0u) { // flush deque/map::at loop
|
|
flush_sp = nsp;
|
|
break;
|
|
}
|
|
sp = nsp;
|
|
}
|
|
}
|
|
if (!flush_sp) {
|
|
XELOGE("CACHE-DUMP flush frame not found on stack (snapshot compare skipped)");
|
|
return;
|
|
}
|
|
rawrow("snapshot@r31+104", flush_sp + 96, 16); // raw temp-map header region
|
|
const uint32_t snap = flush_sp + 104; // temp map object [r31+104]
|
|
std::set<uint64_t> snap_keys;
|
|
const uint32_t shead = rd(snap + 4);
|
|
const uint32_t sroot = shead ? rd(shead + 4) : 0;
|
|
std::vector<uint32_t> st;
|
|
if (sroot && (rd(sroot + 25) & 0xFF) == 0) st.push_back(sroot);
|
|
uint32_t svisits = 0;
|
|
while (!st.empty() && snap_keys.size() < 4096 && ++svisits < 100000 &&
|
|
st.size() < 100000) {
|
|
const uint32_t nd = st.back();
|
|
st.pop_back();
|
|
snap_keys.insert((static_cast<uint64_t>(rd(nd + 12)) << 32) | rd(nd + 16));
|
|
const uint32_t l = rd(nd + 0), r = rd(nd + 8);
|
|
if (l && (rd(l + 25) & 0xFF) == 0) st.push_back(l);
|
|
if (r && (rd(r + 25) & 0xFF) == 0) st.push_back(r);
|
|
}
|
|
std::string miss;
|
|
uint32_t miss_count = 0;
|
|
for (uint32_t i = 0; i < n; ++i) {
|
|
uint32_t bi = (start + i) / 2;
|
|
if (map_size && bi >= map_size) bi -= map_size;
|
|
const uint32_t addr = rd(blockmap + bi * 4) + (i & 1) * 8;
|
|
const uint64_t key = (static_cast<uint64_t>(rd(addr)) << 32) | rd(addr + 4);
|
|
if (!snap_keys.count(key)) {
|
|
miss += fmt::format(" [{}]{:08X}:{:08X}(in_live_map={})", i,
|
|
static_cast<uint32_t>(key >> 32),
|
|
static_cast<uint32_t>(key), map_keys.count(key) ? 1 : 0);
|
|
++miss_count;
|
|
}
|
|
}
|
|
XELOGE(
|
|
"CACHE-DUMP flush_sp={:08X} snapshot_keys={} deque_NOT_in_snapshot={} "
|
|
"(these are what map::at throws on; in_live_map=1 => added during the "
|
|
"flush's UNLOCKED window = the TOCTOU race):{}",
|
|
flush_sp, static_cast<uint32_t>(snap_keys.size()), miss_count, miss);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Fix-A analysis half (read-only): determine which guest stack frame WOULD catch
|
|
// the current C++ throw and where it would resume, by parsing the guest's MSVC
|
|
// C++ EH tables. Zero behaviour change -- logs WOULD-CATCH so the frame-finding
|
|
// can be validated against a live crash before the control-transfer half is
|
|
// wired in. Layout (all BE u32 unless noted):
|
|
// FuncInfo (magic 0x19930522): +12 nTryBlocks, +16 pTryBlockMap,
|
|
// +20 nIPMap, +24 pIP2StateMap
|
|
// IP2StateMapEntry(8B): +0 Ip(VA), +4 State(int)
|
|
// TryBlockMapEntry(20B): +0 tryLow, +4 tryHigh, +8 catchHigh, +12 nCatches,
|
|
// +16 pHandlerArray
|
|
// HandlerType(16B): +4 pType(TypeDescriptor; 0=catch(...)), +8 dispCatchObj,
|
|
// +12 addressOfHandler(funclet)
|
|
// CatchableTypeArray: +0 count, +4 CatchableType*[]; CatchableType: +4 type_ptr
|
|
namespace {
|
|
|
|
struct EhFuncInfo {
|
|
uint32_t funcinfo_va;
|
|
uint32_t func_start; // xenia function containing this FuncInfo's code
|
|
uint32_t first_ip, last_ip; // IP2State map coverage (fallback PC match)
|
|
uint32_t p_ip, n_ip; // IP2State map
|
|
uint32_t p_try, n_try; // try-block map
|
|
};
|
|
|
|
static std::vector<EhFuncInfo>* g_eh_catch_index = nullptr;
|
|
|
|
static uint32_t EhRd(uint32_t va) {
|
|
auto p = kernel_memory()->TranslateVirtual<xe::be<uint32_t>*>(va);
|
|
return p ? static_cast<uint32_t>(*p) : 0;
|
|
}
|
|
|
|
// xenia function-start containing pc, or 0. Non-analyzing lookup: every frame on
|
|
// a live stack has already executed, so its function is known (no side effects).
|
|
static uint32_t EhFuncStart(uint32_t pc) {
|
|
auto* f = kernel_state()->processor()->LookupFunction(pc);
|
|
return f ? f->address() : 0;
|
|
}
|
|
|
|
// Build the catch index once: scan .rdata for FuncInfo(magic) with nTry>=1.
|
|
static void EhBuildIndex() {
|
|
if (g_eh_catch_index) return;
|
|
g_eh_catch_index = new std::vector<EhFuncInfo>();
|
|
// .rdata bounds from the PE section table (PESEC): addr 82000600 size 1142488.
|
|
const uint32_t lo = 0x82000600u, hi = 0x82000600u + 1142488u;
|
|
for (uint32_t a = lo; a + 28 < hi; a += 4) {
|
|
if (EhRd(a) != 0x19930522u) continue;
|
|
uint32_t n_try = EhRd(a + 12), p_try = EhRd(a + 16);
|
|
uint32_t n_ip = EhRd(a + 20), p_ip = EhRd(a + 24);
|
|
if (!n_try || !p_try || n_try > 64 || !n_ip || !p_ip) continue;
|
|
uint32_t first_ip = EhRd(p_ip);
|
|
uint32_t last_ip = EhRd(p_ip + (n_ip - 1) * 8);
|
|
uint32_t fn = EhFuncStart(first_ip); // first IP -> its function
|
|
if (!fn) fn = first_ip; // fall back to the IP itself
|
|
g_eh_catch_index->push_back(
|
|
{a, fn, first_ip, last_ip, p_ip, n_ip, p_try, n_try});
|
|
}
|
|
XELOGE("EH-INDEX built: {} catch-bearing FuncInfos",
|
|
static_cast<uint32_t>(g_eh_catch_index->size()));
|
|
}
|
|
|
|
// State at pc: State of the IP2State entry with the greatest Ip<=pc; -1 if pc
|
|
// precedes the first entry (not inside any protected region).
|
|
static int EhStateAt(const EhFuncInfo& fi, uint32_t pc) {
|
|
int state = -1;
|
|
uint32_t best_ip = 0;
|
|
for (uint32_t i = 0; i < fi.n_ip; ++i) {
|
|
uint32_t ip = EhRd(fi.p_ip + i * 8);
|
|
if (ip <= pc && ip >= best_ip) {
|
|
best_ip = ip;
|
|
state = static_cast<int>(EhRd(fi.p_ip + i * 8 + 4));
|
|
}
|
|
}
|
|
return state;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// Sentinel return address ExecuteRaw() gives a guest function (matches
|
|
// Processor::ExecuteRaw). We force the catching frame's saved-LR slot to this so
|
|
// the catch funclet returns to us regardless of how it restores LR.
|
|
static constexpr uint32_t kFuncletReturnSentinel = 0xBCBCBCBCu;
|
|
|
|
// Fix A. Dispatch the current guest C++ throw to the first stack frame that
|
|
// catches it, then resume the guest as if that frame had caught-and-returned.
|
|
// Returns true if dispatched -- in which case this does NOT return normally: it
|
|
// calls XThread::Reenter, which throws FiberReentryException to unwind every
|
|
// intervening host/JIT frame before resuming the guest at the catch frame's
|
|
// return site. Returns false (falls through to the old crash path) when no
|
|
// handler matches or --eh_dispatch=false.
|
|
bool DispatchGuestCatch(pointer_t<X_EXCEPTION_RECORD> record,
|
|
const ppc_context_t& ctx) {
|
|
EhBuildIndex();
|
|
|
|
const uint32_t thrown_obj = record->exception_information[1];
|
|
|
|
// Every TypeDescriptor the throw can be caught as -> its base-subobject
|
|
// displacement (x_PMD.mdisp), from the throw's CatchableTypeArray.
|
|
std::map<uint32_t, int32_t> catchable;
|
|
uint32_t cta = EhRd(record->exception_information[2] + 12);
|
|
if (cta) {
|
|
uint32_t n = EhRd(cta);
|
|
for (uint32_t i = 0; i < n && i < 32; ++i) {
|
|
uint32_t ct = EhRd(cta + 4 + i * 4);
|
|
if (ct) catchable[EhRd(ct + 4)] = static_cast<int32_t>(EhRd(ct + 8));
|
|
}
|
|
}
|
|
|
|
uint32_t sp = static_cast<uint32_t>(ctx->r[1]);
|
|
for (int depth = 0; depth < 128; ++depth) {
|
|
auto frame = kernel_memory()->TranslateVirtual(sp);
|
|
if (!sp || !frame) {
|
|
XELOGE("EH-WALK stop depth={} sp={:08X} (untranslatable)", depth, sp);
|
|
break;
|
|
}
|
|
uint32_t next_sp = xe::load_and_swap<uint32_t>(frame);
|
|
// The cache flush runs on a fiber/task stack; the back-chain can jump a long
|
|
// way (or to a different stack) when crossing into the dispatcher that has
|
|
// the try/catch. Only require it to go UP; allow large gaps.
|
|
if (next_sp <= sp) {
|
|
XELOGE("EH-WALK stop depth={} sp={:08X} next_sp={:08X} (not ascending)",
|
|
depth, sp, next_sp);
|
|
break;
|
|
}
|
|
if (next_sp - sp > 0x1000000) {
|
|
XELOGE("EH-WALK stop depth={} sp={:08X} next_sp={:08X} (gap>16M)", depth,
|
|
sp, next_sp);
|
|
break;
|
|
}
|
|
auto lrp = kernel_memory()->TranslateVirtual(next_sp - 8);
|
|
uint32_t pc = lrp ? xe::load_and_swap<uint32_t>(lrp) : 0;
|
|
uint32_t frame_sp = next_sp; // SP_F: the catching frame's running SP
|
|
sp = next_sp;
|
|
uint32_t fn = (pc >= 0x82150000 && pc < 0x8284A000) ? EhFuncStart(pc) : 0;
|
|
XELOGE("EH-WALK depth={} SP_F={:08X} pc={:08X} fn={:08X}", depth, frame_sp,
|
|
pc, fn);
|
|
if (pc < 0x82150000 || pc >= 0x8284A000) continue;
|
|
|
|
for (const auto& fi : *g_eh_catch_index) {
|
|
// Match the frame to a catch FuncInfo either by xenia's function-start or
|
|
// (robust to xenia splitting funcs/funclets) by the FuncInfo's IP range.
|
|
bool by_fn = fn && fi.func_start == fn;
|
|
bool by_ip = pc >= fi.first_ip && pc <= fi.last_ip;
|
|
if (!by_fn && !by_ip) continue;
|
|
int state = EhStateAt(fi, pc);
|
|
for (uint32_t t = 0; t < fi.n_try; ++t) {
|
|
uint32_t te = fi.p_try + t * 20;
|
|
int try_low = static_cast<int>(EhRd(te));
|
|
int try_high = static_cast<int>(EhRd(te + 4));
|
|
if (state < try_low || state > try_high) continue;
|
|
uint32_t n_catch = EhRd(te + 12), pH = EhRd(te + 16);
|
|
for (uint32_t c = 0; c < n_catch && c < 32; ++c) {
|
|
uint32_t pType = EhRd(pH + c * 16 + 4);
|
|
auto it = (pType == 0) ? catchable.end() : catchable.find(pType);
|
|
if (pType != 0 && it == catchable.end()) continue;
|
|
int32_t mdisp = (it == catchable.end()) ? 0 : it->second;
|
|
uint32_t dispCatchObj = EhRd(pH + c * 16 + 8);
|
|
uint32_t funclet = EhRd(pH + c * 16 + 12);
|
|
|
|
// Establisher frame = SP at F's entry = the back-chain word saved at
|
|
// F's SP by its `stwu`. The funclet rebuilds its frame ptr as
|
|
// r31 = r12 - N, and __save/restgprlr save/restore F's nonvolatiles +
|
|
// return address at [establisher-8].
|
|
uint32_t establisher = EhRd(frame_sp);
|
|
uint32_t f_return = EhRd(establisher - 8); // F's real return address
|
|
|
|
XELOGE(
|
|
"GUEST-CATCH frame_pc={:08X} fn={:08X} SP_F={:08X} est={:08X} "
|
|
"f_return={:08X} state={} try{}[{}..{}] catch#{} pType={:08X} "
|
|
"obj@est+{:X} funclet={:08X} dispatch={}",
|
|
pc, fn, frame_sp, establisher, f_return, state, t, try_low,
|
|
try_high, c, pType, dispCatchObj, funclet,
|
|
cvars::eh_dispatch ? 1 : 0);
|
|
|
|
if (!cvars::eh_dispatch) return false;
|
|
if (!establisher || !f_return) {
|
|
XELOGE("GUEST-CATCH abort: bad establisher/return frame");
|
|
return false;
|
|
}
|
|
|
|
auto store32 = [](uint32_t va, uint32_t val) {
|
|
auto p = kernel_memory()->TranslateVirtual<xe::be<uint32_t>*>(va);
|
|
if (p) *p = val;
|
|
};
|
|
|
|
// Hand the caught object (adjusted to the caught base) to the catch.
|
|
// catch(...) (dispCatchObj==0) takes no object.
|
|
if (dispCatchObj != 0) {
|
|
store32(establisher + dispCatchObj,
|
|
thrown_obj + static_cast<uint32_t>(mdisp));
|
|
}
|
|
// Force the funclet to return to us no matter how it restores LR.
|
|
store32(establisher - 8, kFuncletReturnSentinel);
|
|
|
|
// Run the game's own catch funclet in F's frame: r12=r1=establisher.
|
|
// It runs the catch body, restores F's inherited nonvolatiles, and
|
|
// returns (r3 = F's return value).
|
|
ctx->r[12] = establisher;
|
|
ctx->r[1] = establisher;
|
|
ctx->processor->ExecuteRaw(ctx->thread_state, funclet);
|
|
|
|
// F has effectively caught-and-returned: resume at F's caller with the
|
|
// funclet's return value. Reenter unwinds all the throw's host/JIT
|
|
// frames first. r1 is already the caller SP (establisher).
|
|
ctx->processor->backend()->PrepareForReentry(ctx.value());
|
|
XELOGE("GUEST-CATCH dispatched -> resuming at f_return={:08X} r3={:08X}",
|
|
f_return, static_cast<uint32_t>(ctx->r[3]));
|
|
XThread::GetCurrentThread()->Reenter(f_return);
|
|
return true; // not reached
|
|
}
|
|
}
|
|
}
|
|
}
|
|
XELOGE("GUEST-CATCH none (no matching handler frame found)");
|
|
return false;
|
|
}
|
|
|
|
void LogGuestThrow(pointer_t<X_EXCEPTION_RECORD> record,
|
|
const ppc_context_t& ctx) {
|
|
const uint32_t thrown_ptr = record->exception_information[1];
|
|
XELOGE("GUEST-THROW type={} object={:08X} lr={:08X}",
|
|
ThrownTypeName(thrown_ptr), thrown_ptr,
|
|
static_cast<uint32_t>(ctx->lr));
|
|
DumpCacheManagerOnThrow(ctx);
|
|
|
|
std::string regs;
|
|
for (int i = 3; i <= 31; ++i) {
|
|
regs += fmt::format(" r{}={:08X}", i, static_cast<uint32_t>(ctx->r[i]));
|
|
}
|
|
XELOGE("GUEST-THROW regs:{}", regs);
|
|
|
|
// Walk the guest stack. Xenon prologues do `mfspr r12,LR; stw r12,-8(r1)`
|
|
// BEFORE `stwu r1,-N(r1)`, so a frame's return address is stored at
|
|
// [caller_sp - 8] = [([sp]) - 8], NOT [sp+4]. Back chain = [sp]. Filter to
|
|
// .text (0x82150000..0x8284A000).
|
|
std::string frames;
|
|
uint32_t sp = static_cast<uint32_t>(ctx->r[1]);
|
|
for (int depth = 0; depth < 64; ++depth) {
|
|
auto frame = kernel_memory()->TranslateVirtual(sp);
|
|
if (!sp || !frame) {
|
|
break;
|
|
}
|
|
const uint32_t next_sp = xe::load_and_swap<uint32_t>(frame);
|
|
if (next_sp <= sp || next_sp - sp > 0x40000) {
|
|
break;
|
|
}
|
|
auto lrp = kernel_memory()->TranslateVirtual(next_sp - 8);
|
|
if (lrp) {
|
|
const uint32_t lr = xe::load_and_swap<uint32_t>(lrp);
|
|
if (lr >= 0x82150000 && lr < 0x8284A000) {
|
|
frames += fmt::format(" {:08X}", lr);
|
|
}
|
|
}
|
|
sp = next_sp;
|
|
}
|
|
XELOGE("GUEST-THROW guest stack:{}", frames);
|
|
}
|
|
|
|
void HandleCppException(pointer_t<X_EXCEPTION_RECORD> record) {
|
|
// C++ exception.
|
|
// https://blogs.msdn.com/b/oldnewthing/archive/2010/07/30/10044061.aspx
|
|
// http://www.drdobbs.com/visual-c-exception-handling-instrumentat/184416600
|
|
// http://www.openrce.org/articles/full_view/21
|
|
|
|
assert_true(record->number_parameters == 3);
|
|
assert_true(record->exception_information[0] == 0x19930520);
|
|
|
|
auto thrown_ptr = record->exception_information[1];
|
|
auto thrown = kernel_memory()->TranslateVirtual(thrown_ptr);
|
|
auto vftable_ptr = *reinterpret_cast<xe::be<uint32_t>*>(thrown);
|
|
|
|
auto throw_info_ptr = record->exception_information[2];
|
|
auto throw_info =
|
|
kernel_memory()->TranslateVirtual<x_s__ThrowInfo*>(throw_info_ptr);
|
|
auto catchable_types =
|
|
kernel_memory()->TranslateVirtual<x_s__CatchableTypeArray*>(
|
|
throw_info->catchable_type_array_ptr);
|
|
|
|
// xe::debugging::Break();
|
|
XELOGE("Guest attempted to throw a C++ exception!");
|
|
}
|
|
|
|
void RtlRaiseException_entry(pointer_t<X_EXCEPTION_RECORD> record,
|
|
const ppc_context_t& ppc_context) {
|
|
switch (record->code) {
|
|
case 0x406D1388: {
|
|
HandleSetThreadName(record);
|
|
return;
|
|
}
|
|
case 0xE06D7363: {
|
|
// Heavy diagnostics are OFF by default: in a mission the cache flush
|
|
// throws constantly, and these dumps (esp. the cache-manager tree walk on
|
|
// a corrupted map) allocate gigabytes per throw. Stock behaviour is just
|
|
// HandleCppException below.
|
|
if (cvars::cache_throw_diag) {
|
|
LogGuestThrow(record, ppc_context);
|
|
}
|
|
// Fix A: dispatch to the catch handler. On success this does not return
|
|
// (it reenters the guest at the catch frame). Disproven for the cache
|
|
// crash and default-off; also skip its (allocating) frame walk unless a
|
|
// diagnostic/dispatch knob is set.
|
|
if ((cvars::eh_dispatch || cvars::cache_throw_diag) &&
|
|
DispatchGuestCatch(record, ppc_context)) {
|
|
return;
|
|
}
|
|
HandleCppException(record);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// TODO(benvanik): unwinding.
|
|
// This is going to suck.
|
|
// xe::debugging::Break();
|
|
|
|
// RtlRaiseException definitely wasn't a noreturn function, we can return
|
|
// safe-ish
|
|
XELOGE("Guest attempted to trigger a breakpoint!");
|
|
}
|
|
DECLARE_XBOXKRNL_EXPORT2(RtlRaiseException, kDebug, kStub, kImportant);
|
|
|
|
void KeBugCheckEx_entry(dword_t code, dword_t param1, dword_t param2,
|
|
dword_t param3, dword_t param4) {
|
|
auto msg =
|
|
fmt::format("*** STOP: 0x{:08X} (0x{:08X}, 0x{:08X}, 0x{:08X}, 0x{:08X})",
|
|
static_cast<uint32_t>(code), static_cast<uint32_t>(param1),
|
|
static_cast<uint32_t>(param2), static_cast<uint32_t>(param3),
|
|
static_cast<uint32_t>(param4));
|
|
XELOGE("{}", msg);
|
|
fflush(stdout);
|
|
|
|
if (xe::debugging::IsDebuggerAttached()) {
|
|
xe::debugging::Break();
|
|
}
|
|
|
|
// Show crash dialog and suspend the guest thread instead of killing the
|
|
// host process.
|
|
auto current_thread = kernel::XThread::GetCurrentThread();
|
|
const auto* emulator = kernel_state()->emulator();
|
|
auto* display_window = emulator->display_window();
|
|
auto* imgui_drawer = emulator->imgui_drawer();
|
|
if (display_window && imgui_drawer) {
|
|
auto dlg_msg = fmt::format(
|
|
"The guest kernel has crashed (KeBugCheck).\n\n{}\n\n"
|
|
"The faulting thread has been suspended.",
|
|
msg);
|
|
display_window->app_context().CallInUIThreadSynchronous(
|
|
[imgui_drawer, &dlg_msg]() {
|
|
xe::ui::ImGuiDialog::ShowMessageBox(imgui_drawer,
|
|
"Guest Kernel Crash", dlg_msg);
|
|
});
|
|
}
|
|
|
|
if (current_thread) {
|
|
current_thread->Suspend(nullptr);
|
|
}
|
|
}
|
|
DECLARE_XBOXKRNL_EXPORT2(KeBugCheckEx, kDebug, kStub, kImportant);
|
|
|
|
void KeBugCheck_entry(dword_t code) { KeBugCheckEx_entry(code, 0, 0, 0, 0); }
|
|
DECLARE_XBOXKRNL_EXPORT2(KeBugCheck, kDebug, kImplemented, kImportant);
|
|
|
|
} // namespace xboxkrnl
|
|
} // namespace kernel
|
|
} // namespace xe
|
|
|
|
DECLARE_XBOXKRNL_EMPTY_REGISTER_EXPORTS(Debug);
|