/** ****************************************************************************** * 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 #include #include #include #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 type; xe::be name_ptr; xe::be thread_id; xe::be flags; } X_THREADNAME_INFO; static_assert_size(X_THREADNAME_INFO, 0x10); void HandleSetThreadName(pointer_t 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(&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(thread_info->name_ptr)); std::replace_if( name.begin(), name.end(), [](auto c) { return c < 32 || c > 127; }, '?'); object_ref 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 mdisp; xe::be pdisp; xe::be vdisp; } x_PMD; typedef struct { xe::be properties; xe::be type_ptr; x_PMD this_displacement; xe::be size_or_offset; xe::be copy_function_ptr; } x_s__CatchableType; typedef struct { xe::be number_catchable_types; xe::be catchable_type_ptrs[1]; } x_s__CatchableTypeArray; typedef struct { xe::be attributes; xe::be unwind_ptr; xe::be forward_compat_ptr; xe::be 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(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 ""; } auto rd32 = [](uint32_t va) -> uint32_t { auto p = kernel_memory()->TranslateVirtual*>(va); return p ? static_cast(*p) : 0; }; const uint32_t vtable = rd32(thrown_ptr); if (!vtable) { return ""; } const uint32_t col = rd32(vtable - 4); if (!col) { return ""; } const uint32_t type_desc = rd32(col + 0x0C); if (!type_desc) { return ""; } 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 (map is rebuilt from this, keyed by hash-pair) // OBJ+0x48 = std::deque: +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(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*>(va); return p ? static_cast(*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 map_keys; const uint32_t head = rd(OBJ + 0x34); const uint32_t root = head ? rd(head + 4) : 0; std::vector 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(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(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(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(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(f); if (nsp <= sp || nsp - sp > 0x1000000) break; auto lp = kernel_memory()->TranslateVirtual(nsp - 8); uint32_t pc = lp ? xe::load_and_swap(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 snap_keys; const uint32_t shead = rd(snap + 4); const uint32_t sroot = shead ? rd(shead + 4) : 0; std::vector 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(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(rd(addr)) << 32) | rd(addr + 4); if (!snap_keys.count(key)) { miss += fmt::format(" [{}]{:08X}:{:08X}(in_live_map={})", i, static_cast(key >> 32), static_cast(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(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* g_eh_catch_index = nullptr; static uint32_t EhRd(uint32_t va) { auto p = kernel_memory()->TranslateVirtual*>(va); return p ? static_cast(*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(); // .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(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(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 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 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(EhRd(ct + 8)); } } uint32_t sp = static_cast(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(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(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(EhRd(te)); int try_high = static_cast(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*>(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(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(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 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(ctx->lr)); DumpCacheManagerOnThrow(ctx); std::string regs; for (int i = 3; i <= 31; ++i) { regs += fmt::format(" r{}={:08X}", i, static_cast(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(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(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(lrp); if (lr >= 0x82150000 && lr < 0x8284A000) { frames += fmt::format(" {:08X}", lr); } } sp = next_sp; } XELOGE("GUEST-THROW guest stack:{}", frames); } void HandleCppException(pointer_t 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*>(thrown); auto throw_info_ptr = record->exception_information[2]; auto throw_info = kernel_memory()->TranslateVirtual(throw_info_ptr); auto catchable_types = kernel_memory()->TranslateVirtual( throw_info->catchable_type_array_ptr); // xe::debugging::Break(); XELOGE("Guest attempted to throw a C++ exception!"); } void RtlRaiseException_entry(pointer_t 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(code), static_cast(param1), static_cast(param2), static_cast(param3), static_cast(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);