#include "system_bridge.h" #include #include #include #include #include #include #include #include #if defined(_WIN32) #ifndef NOMINMAX #define NOMINMAX #endif #include #endif #include "abi_bridge.h" #include "memory.h" #include "ppc_runtime.h" #include "recomp_mod_loader.h" #include "runtime_config.h" #include "runtime_log.h" #include "runtime_product.h" #include "timebase_contract.h" // Global flag to suppress SEH reporting during static constructor execution bool g_suppressSehReporting = false; thread_local MkwJmpBuf* g_sehJumpTarget = nullptr; thread_local uint32_t g_sehLastExceptionCode = 0; thread_local uintptr_t g_sehLastExceptionAddress = 0; thread_local uintptr_t g_sehLastAccessedAddress = 0; thread_local uint32_t g_sehLastAccessType = 0; // Forward declaration for data section initialization generated by the translator. extern "C" void InitializeDataSections(); namespace { std::string Hex32(uint32_t value) { std::ostringstream oss; oss << std::hex << std::uppercase << std::setw(8) << std::setfill('0') << value; return oss.str(); } std::optional PrintableU32(uint32_t value) { std::string out; out.reserve(4); for (int shift = 24; shift >= 0; shift -= 8) { const auto ch = static_cast((value >> shift) & 0xFFu); if (!std::isprint(ch)) { return std::nullopt; } out.push_back(static_cast(ch)); } return out; } std::optional FindNearestTranslatedFunction(uint32_t address) { auto best = TranslatedFunctionRegistry::FindNearestByAddress(address); // Crash-only heuristic: large gaps are usually data/FourCC values, not code offsets. if (best && address - best->address < 0x10000u) { return best; } return std::nullopt; } // generated/guest_symbol_table.cpp (or the stub when it is absent): the // project's function-map names, sorted by address, for crash symbolization. extern "C" { extern const uint32_t kGuestMapSymbolCount; extern const uint32_t kGuestMapSymbolAddresses[]; extern const char* const kGuestMapSymbolNames[]; } // Floor lookup into the function-map name table with the same gap cap as the // registry heuristic above. Returns nullptr when no named symbol is close. const char* GuestMapSymbolFloor(uint32_t address, uint32_t* symbolStart) { if (kGuestMapSymbolCount == 0) { return nullptr; } uint32_t lo = 0; uint32_t hi = kGuestMapSymbolCount; while (lo < hi) { const uint32_t mid = lo + (hi - lo) / 2; if (kGuestMapSymbolAddresses[mid] <= address) { lo = mid + 1; } else { hi = mid; } } if (lo == 0) { return nullptr; } const uint32_t index = lo - 1; if (address - kGuestMapSymbolAddresses[index] >= 0x10000u) { return nullptr; } if (symbolStart) { *symbolStart = kGuestMapSymbolAddresses[index]; } return kGuestMapSymbolNames[index]; } std::optional ReadGuest32NoThrow(uint32_t address) { uint32_t value = 0; if (MemoryInline::TryReadMappedScalar(address, value)) { return value; } return std::nullopt; } std::optional ReadGuest8NoThrow(uint32_t address) { uint8_t value = 0; if (MemoryInline::TryReadMappedScalar(address, value)) { return value; } return std::nullopt; } std::optional GuestCStringPreview(uint32_t address) { std::string out; out.reserve(48); for (uint32_t i = 0; i < 48; ++i) { const auto byte = ReadGuest8NoThrow(address + i); if (!byte) { return std::nullopt; } if (*byte == 0) { return out.size() >= 3 ? std::optional(out) : std::nullopt; } if (!std::isprint(*byte)) { return std::nullopt; } out.push_back(static_cast(*byte)); } out += "..."; return out; } void PrintGuestValue(std::ostream& os, std::string_view label, uint32_t value) { os << " "; if (!label.empty()) { os << label << "="; } os << "0x" << Hex32(value); // Prefer the function-map name (human symbol) over the registry's // func_XXXXXXXX label; when both resolve, the closer floor wins so a map // symbol from a preceding function cannot shadow an exact registry hit. uint32_t mapStart = 0; const char* mapName = GuestMapSymbolFloor(value, &mapStart); if (const auto* exact = TranslatedFunctionRegistry::FindByAddressPtr(value)) { if (mapName && mapStart >= exact->address) { os << "<" << mapName; if (value != mapStart) { os << "+0x" << std::hex << std::uppercase << (value - mapStart) << std::dec; } os << ">"; } else { os << "<" << exact->name << ">"; } } else if (auto nearest = FindNearestTranslatedFunction(value)) { if (mapName && mapStart >= nearest->address) { os << "<" << mapName; if (value != mapStart) { os << "+0x" << std::hex << std::uppercase << (value - mapStart) << std::dec; } os << ">"; } else { os << "<" << nearest->name << "+0x" << std::hex << std::uppercase << (value - nearest->address) << std::dec << ">"; } } else if (mapName) { os << "<" << mapName << "+0x" << std::hex << std::uppercase << (value - mapStart) << std::dec << ">"; } if (auto text = PrintableU32(value)) { os << "('" << *text << "')"; } else if (auto guestString = GuestCStringPreview(value)) { os << "(\"" << *guestString << "\")"; } } void PrintFrameTailWords(std::ostream& os, uint32_t previousSp) { os << " tail:"; for (int32_t offset = -0x40; offset <= 0x04; offset += 4) { const uint32_t address = previousSp + static_cast(offset); const auto value = ReadGuest32NoThrow(address); if (!value) { continue; } os << " " << (offset < 0 ? "-" : "+") << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << static_cast(std::abs(offset)) << "=0x" << Hex32(*value); } os << std::dec << std::setfill(' '); } void DumpGuestBackchain(std::ostream& os, const CpuContext* cpu) { uint32_t sp = cpu->gpr[1]; if (sp == 0) { return; } os << "[runtime] Stack backchain:" << std::endl; for (int frame = 0; frame < 12; ++frame) { const auto previousSp = ReadGuest32NoThrow(sp); if (!previousSp || *previousSp == 0) { os << "[runtime] #" << frame << " sp=0x" << Hex32(sp) << " prev=(unmapped)" << std::endl; break; } const uint32_t frameSize = *previousSp - sp; os << "[runtime] #" << frame << " sp=0x" << Hex32(sp) << " prev=0x" << Hex32(*previousSp) << " size=0x" << std::hex << std::uppercase << frameSize << std::dec; if (frameSize <= 0x20000u) { if (const auto savedLr = ReadGuest32NoThrow(*previousSp + 4u)) { PrintGuestValue(os, "savedLR", *savedLr); } PrintFrameTailWords(os, *previousSp); } os << std::endl; if (*previousSp <= sp || frameSize > 0x20000u) { break; } sp = *previousSp; } } void DumpGuestStackHints(std::ostream& os, const CpuContext* cpu) { const uint32_t sp = cpu->gpr[1]; if (sp == 0) { return; } os << "[runtime] Stack words:"; for (uint32_t offset = 0; offset < 0x40; offset += 4) { const auto value = ReadGuest32NoThrow(sp + offset); if (!value) { break; } os << " +" << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << offset << "=0x" << Hex32(*value); } os << std::dec << std::setfill(' ') << std::endl; int printed = 0; for (uint32_t offset = 0; offset < 0x800 && printed < 32; offset += 4) { const auto value = ReadGuest32NoThrow(sp + offset); if (!value) { continue; } if (!TranslatedFunctionRegistry::FindByAddressPtr(*value) && !FindNearestTranslatedFunction(*value)) { continue; } if (printed == 0) { os << "[runtime] Stack code refs:"; } os << " +" << std::hex << std::uppercase << offset << std::dec << ":"; PrintGuestValue(os, "", *value); ++printed; } if (printed != 0) { os << std::endl; } } void DumpGuestCrashHints(std::ostream& os, const CpuContext* cpu) { if (!cpu) { return; } const uint32_t active = RecompMod::CurrentTranslatedExecutionAddress(); if (active != 0) { os << "[runtime] Active translated target: "; PrintGuestValue(os, "guest", active); os << std::endl; } os << "[runtime] Guest refs:"; PrintGuestValue(os, "pc", cpu->pc); PrintGuestValue(os, "lr", cpu->lr); PrintGuestValue(os, "ctr", cpu->ctr); PrintGuestValue(os, "r12", cpu->gpr[12]); PrintGuestValue(os, "r3", cpu->gpr[3]); PrintGuestValue(os, "r4", cpu->gpr[4]); PrintGuestValue(os, "r5", cpu->gpr[5]); PrintGuestValue(os, "r15", cpu->gpr[15]); PrintGuestValue(os, "r16", cpu->gpr[16]); PrintGuestValue(os, "r18", cpu->gpr[18]); PrintGuestValue(os, "r26", cpu->gpr[26]); PrintGuestValue(os, "r27", cpu->gpr[27]); os << std::endl; if (active != 0 && active != cpu->pc) { os << "[runtime] Note: active translated target differs from ctx->pc; ctx->pc may be stale " "without PC tracing." << std::endl; } DumpGuestStackHints(os, cpu); DumpGuestBackchain(os, cpu); } } // namespace void SystemBridge::Initialize() { const auto memoryConfig = Memory::Config::WiiDefaults(); Memory::Init(memoryConfig); SeedLowMemDefaults(memoryConfig); // Initialize from the embedded DOL/REL data sections. This provides the // initial memory state for .data, .rodata, .sdata, etc. RT_LOG(RT_TAG_RUNTIME) << "Initializing data sections from embedded DOL/REL data" << std::endl; InitializeDataSections(); // Constructors run before the main entry point, but they still require the // PowerPC ABI environment. Keep using the persistent context here: the old // AbiCallContext path implicitly selected and seeded it for every ctor. InitializePersistentCpuContext(); CpuContext& cpu = GetPersistentCpuContext(); cpu.gpr[1] = 0x81700000u; CpuContextScope scope(&cpu); g_suppressSehReporting = true; // Run main DOL static constructors first // These set up vtables and other critical infrastructure RT_LOG(RT_TAG_RUNTIME) << "Running static constructors for main DOL..." << std::endl; const uint32_t dolCtorStart = 0x80244DE0; const uint32_t dolCtorEnd = 0x80244EA0; int dolCount = 0; for (uint32_t addr = dolCtorStart; addr < dolCtorEnd; addr += 4) { uint32_t funcAddr = 0; try { funcAddr = Memory::Read32(addr); } catch (...) { continue; } if (funcAddr == 0 || funcAddr == 0xFFFFFFFF) continue; if (TranslatedFunctionRegistry::FindByAddressPtr(funcAddr)) { MkwJmpBuf jumpBuf; g_sehJumpTarget = &jumpBuf; if (MKW_SETJMP(jumpBuf) == 0) { cpu.gpr[1] = 0x81700000u; InvokeIndirectCpu(funcAddr, &cpu); dolCount++; } else { RT_LOG(RT_TAG_RUNTIME) << "SEH Exception caught invoking DOL ctor 0x" << std::hex << funcAddr << " code=0x" << g_sehLastExceptionCode << " at=0x" << g_sehLastExceptionAddress; if (g_sehLastExceptionCode == 0xC0000005u && g_sehLastAccessedAddress != 0) { std::cerr << " " << (g_sehLastAccessType ? "write" : "read") << "=0x" << g_sehLastAccessedAddress; } std::cerr << std::dec << " - skipping." << std::endl; } g_sehJumpTarget = nullptr; } } RT_LOG(RT_TAG_RUNTIME) << "Executed " << dolCount << " main DOL static constructors." << std::endl; // StaticR.rel ctors must be run manually since we aren't using OSLink RT_LOG(RT_TAG_RUNTIME) << "Running static constructors for StaticR.rel..." << std::endl; // Define range for StaticR.rel .ctors const uint32_t ctorStart = 0x8088f400; const uint32_t ctorEnd = 0x8088f704; int count = 0; for (uint32_t addr = ctorStart; addr < ctorEnd; addr += 4) { uint32_t funcAddr = 0; try { funcAddr = Memory::Read32(addr); } catch (...) { continue; } if (funcAddr == 0 || funcAddr == 0xFFFFFFFF) continue; if (TranslatedFunctionRegistry::FindByAddressPtr(funcAddr)) { MkwJmpBuf jumpBuf; g_sehJumpTarget = &jumpBuf; if (MKW_SETJMP(jumpBuf) == 0) { cpu.gpr[1] = 0x81700000u; InvokeIndirectCpu(funcAddr, &cpu); count++; } else { RT_LOG(RT_TAG_RUNTIME) << "SEH Exception caught invoking ctor 0x" << std::hex << funcAddr << " code=0x" << g_sehLastExceptionCode << " at=0x" << g_sehLastExceptionAddress; if (g_sehLastExceptionCode == 0xC0000005u && g_sehLastAccessedAddress != 0) { std::cerr << " " << (g_sehLastAccessType ? "write" : "read") << "=0x" << g_sehLastAccessedAddress; } std::cerr << std::dec << " - skipping." << std::endl; } g_sehJumpTarget = nullptr; } } g_suppressSehReporting = false; RT_LOG(RT_TAG_RUNTIME) << "Executed " << count << " static constructors." << std::endl; } void SystemBridge::WriteGuestMemorySnapshot(std::ostream& os, const std::filesystem::path& mem1Path) { constexpr uint32_t kMem1Base = 0x80000000u; constexpr uint32_t kMem1Size = 0x01800000u; if (Memory::Contains(kMem1Base, kMem1Size)) { std::ofstream dump(mem1Path, std::ios::binary | std::ios::trunc); dump.write(reinterpret_cast(Memory::GetPointer(kMem1Base, kMem1Size)), kMem1Size); os << "[runtime] MEM1 snapshot written to " << RuntimeConfigFile::PathToUtf8(mem1Path) << (dump.good() ? "" : " (write failed)") << std::endl; } constexpr uint32_t kMem2Base = 0x90000000u; for (uint32_t mem2Size : {0x08000000u, 0x04000000u}) { if (!Memory::Contains(kMem2Base, mem2Size)) { continue; } std::filesystem::path mem2Path = mem1Path; mem2Path += ".mem2"; std::ofstream dump(mem2Path, std::ios::binary | std::ios::trunc); dump.write(reinterpret_cast(Memory::GetPointer(kMem2Base, mem2Size)), mem2Size); os << "[runtime] MEM2 snapshot written to " << RuntimeConfigFile::PathToUtf8(mem2Path) << (dump.good() ? "" : " (write failed)") << std::endl; break; } } void SystemBridge::DumpCrashHeuristics(std::ostream& os, const CpuContext* cpu, const uint32_t* missingGuestTarget) { if (!cpu) { return; } if (missingGuestTarget && *missingGuestTarget == 0) { os << "[runtime] The guest jumped to address 0 - almost always a virtual call " "through a bad object, or a callback pointer that was never set." << std::endl; if (cpu->gpr[3] == 0) { os << "[runtime] r3 (the C++ 'this' pointer) is NULL at the call. A very common " "cause is an unchecked guest heap allocation failure: every " "\"heap (...)->alloc(size(...))\" OSReport line above is a FAILED " "allocation (the number after 'free' is the bytes actually left in " "that heap). If those lines are present, the heap was exhausted " "before this crash." << std::endl; } } else if (missingGuestTarget) { os << "[runtime] The guest jumped to 0x" << std::hex << std::uppercase << *missingGuestTarget << std::dec << ", which is not a translated function entry. If the value looks like " "readable text or a data pointer, a function pointer or vtable was " "overwritten (guest memory stomp)." << std::endl; } } // MEM2 space reserved at startup for the runtime FST. extern "C" uint32_t g_dvdFstReservedBase = 0; extern "C" uint32_t g_dvdFstReservedSize = 0; void SystemBridge::SeedLowMemDefaults(const Memory::Config& config) { const auto* mem1Region = FindRegionConfig(config, "MEM1"); const auto* mem2Region = FindRegionConfig(config, "MEM2"); const uint32_t mem1Size = mem1Region ? static_cast(mem1Region->sizeBytes) : 0; const uint32_t mem2Size = mem2Region ? static_cast(mem2Region->sizeBytes) : 0; struct SeedEntry { uint32_t address; uint32_t value; const char* label; bool force = false; }; std::vector entries; constexpr uint32_t kMem1ArenaLoDefault = 0x80399180u; uint32_t mem1ArenaHiDefault = 0x817f0520u; constexpr uint32_t kBusClockHz = static_cast(TimeBaseContract::kBusClockHz); constexpr uint32_t kCpuClockHz = kBusClockHz * 3u; // Mario Kart Wii boots under IOS36. These are the IOS kernel values that // Dolphin exposes in low memory for IOS36 (VersionInfo.cpp). SDK helpers // read them through the 0xC0000000 MEM1 alias. constexpr uint32_t kIos36Version = 0x00240E18u; constexpr uint32_t kIos36Date = 0x00030110u; constexpr uint32_t kMem2LoFloor = 0x90000800u; // Reserve enough guest memory for the runtime-built DVD FST. constexpr uint32_t kDvdFstReserveSize = 0x200000u; // IOS36 reserves 128 KiB for IPC immediately below a 128 KiB IOS-owned // block. Dolphin's RAM override preserves both sizes and moves them to the // top of the expanded MEM2 mapping. constexpr uint32_t kIPCArenaSize = 0x20000u; constexpr uint32_t kIosReservedSize = 0x20000u; // The boot code exposes the current disc ID in low memory before DVDInit. // Retro Rewind reads the region byte directly from here while building its // Retro-WFC payload URL, and OSGetAppGamename reads the app code mirrors // at 0x80003180/0x80003194 while building NAS auth fields. entries.push_back({0x80000000u, 0x524D4350u, "Disc game code"}); // RMCP entries.push_back({0x80000004u, 0x30310100u, "Disc maker/id"}); // 01 + disc 1 entries.push_back({0x80003180u, 0x524D4350u, "OS app game code"}); // RMCP entries.push_back({0x80003194u, 0x524D4350u, "OS app gamename"}); // RMCP if (RuntimeProduct::IsRetroRewind()) { entries.push_back({0x800017D8u, 0x00000001u, "Retro Rewind recomp runtime marker", true}); } for (const auto& reservation : RecompMod::MemoryReservations()) { if (reservation.start >= kMem1ArenaLoDefault && reservation.start < mem1ArenaHiDefault && reservation.end <= 0x81800000u) { mem1ArenaHiDefault = reservation.start; RT_LOG(RT_TAG_MOD) << "Reserved MEM1 range 0x" << std::hex << std::uppercase << reservation.start << "-0x" << reservation.end << " for " << reservation.name << "; MEM1 arena hi now 0x" << mem1ArenaHiDefault << std::dec << std::nouppercase << std::endl; } } // Seed OS clock values used by SDK timing helpers (OSBusClock/OSClock). entries.push_back({0x800000F8u, kBusClockHz, "__OSBusClock"}); entries.push_back({0x800000FCu, kCpuClockHz, "__OSClock"}); entries.push_back({0x80003140u, kIos36Version, "IOS36 version"}); entries.push_back({0x80003144u, kIos36Date, "IOS36 date"}); if (mem1Size != 0) { const uint32_t mem1End = Memory::kMem1CachedBase + mem1Size; entries.push_back({0x80000028u, mem1Size, "Legacy MEM1 size", true}); entries.push_back({0x80000030u, kMem1ArenaLoDefault, "Legacy MEM1 arena lo", true}); entries.push_back({0x80000034u, mem1ArenaHiDefault, "Legacy MEM1 arena hi", true}); entries.push_back({0x800000F0u, mem1Size, "Legacy simulated MEM1 size", true}); entries.push_back({0x80003100u, mem1Size, "Physical MEM1 size", true}); entries.push_back({0x80003104u, mem1Size, "Simulated MEM1 size", true}); entries.push_back({0x80003108u, mem1End, "MEM1 end", true}); entries.push_back({0x8000310Cu, kMem1ArenaLoDefault, "MEM1 arena lo", true}); entries.push_back({0x80003110u, mem1ArenaHiDefault, "MEM1 arena hi", true}); } if (mem2Size != 0) { const uint32_t physicalMem2End = Memory::kMem2CachedBase + mem2Size; const uint32_t iosReservedLo = physicalMem2End - kIosReservedSize; const uint32_t mem2End = iosReservedLo; const uint32_t ipcBufHi = mem2End; const uint32_t ipcBufLo = ipcBufHi - kIPCArenaSize; // Reserve FST memory before guest code can use this part of the arena. g_dvdFstReservedSize = kDvdFstReserveSize; g_dvdFstReservedBase = ipcBufLo - kDvdFstReserveSize; const uint32_t mem2ArenaHi = g_dvdFstReservedBase; entries.push_back({0x80003118u, mem2Size, "Physical MEM2 size", true}); entries.push_back({0x8000311Cu, mem2Size, "Simulated MEM2 size", true}); entries.push_back({0x80003120u, mem2End, "MEM2 end", true}); entries.push_back({0x80003124u, kMem2LoFloor, "MEM2 arena lo", true}); entries.push_back({0x80003128u, mem2ArenaHi, "MEM2 arena hi", true}); entries.push_back({0x80003130u, ipcBufLo, "IPC Buffer lo", true}); entries.push_back({0x80003134u, ipcBufHi, "IPC Buffer hi", true}); entries.push_back({0x80003138u, 0x00000002u, "Hollywood revision", true}); entries.push_back({0x80003148u, iosReservedLo, "IOS reserved lo", true}); entries.push_back({0x8000314Cu, physicalMem2End, "IOS reserved hi", true}); } bool patchedAny = false; size_t seededCount = 0; for (const auto& entry : entries) { if (entry.value == 0) continue; // Only patch if the address is mapped if (!Memory::Contains(entry.address, sizeof(uint32_t))) continue; // Only patch if currently zero (don't overwrite if OS already initialized) if (!entry.force && Memory::Read32(entry.address) != 0) continue; Memory::Write32(entry.address, entry.value); patchedAny = true; ++seededCount; } if (seededCount != 0) { RT_LOG(RT_TAG_RUNTIME) << "Seeded " << seededCount << " low-memory default(s)" << std::endl; } if (patchedAny && mem2Size != 0) { const uint32_t patchedLo = Memory::Read32(0x80003124u); const uint32_t patchedHi = Memory::Read32(0x80003128u); const uint32_t patchedSize = Memory::Read32(0x8000311Cu); RT_LOG(RT_TAG_RUNTIME) << "MEM2 defaults active -> size=" << (patchedSize / (1024 * 1024)) << " MB, arena 0x" << std::hex << std::uppercase << patchedLo << " - 0x" << patchedHi << std::dec << std::nouppercase << std::endl; } } const Memory::RegionConfig* SystemBridge::FindRegionConfig(const Memory::Config& config, std::string_view name) { for (const auto& region : config.regions) { if (region.name == name) { return ®ion; } } return nullptr; } void SystemBridge::DumpCpuState(const CpuContext* cpu) { DumpCpuState(std::cerr, cpu); } void SystemBridge::DumpCpuState(std::ostream& os, const CpuContext* cpu) { if (!cpu) { os << "[runtime] CPU context unavailable; registers could not be dumped." << std::endl; return; } DumpGuestCrashHints(os, cpu); os << "[runtime] PC=0x" << Hex32(cpu->pc) << " LR=0x" << Hex32(cpu->lr) << " CTR=0x" << Hex32(cpu->ctr) << " CR=0x" << Hex32(cpu->cr) << " XER=0x" << Hex32(cpu->xer) << std::endl; os << "[runtime] SRR0=0x" << Hex32(cpu->srr0) << " SRR1=0x" << Hex32(cpu->srr1) << std::endl; os << "[runtime] SP(r1)=0x" << Hex32(cpu->gpr[1]) << " R2=0x" << Hex32(cpu->gpr[2]) << " R13=0x" << Hex32(cpu->gpr[13]) << std::endl; for (int base = 0; base < 32; base += 8) { os << "[runtime] GPR" << std::setw(2) << std::setfill('0') << base << "-" << std::setw(2) << std::setfill('0') << (base + 7) << ": "; for (int i = 0; i < 8; ++i) { const int reg = base + i; os << "r" << std::setw(2) << std::setfill('0') << reg << "=0x" << Hex32(cpu->gpr[reg]) << (i == 7 ? "" : " "); } os << std::setfill(' ') << std::endl; } os << std::setfill(' '); }