Files
dolphin/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp
T
Yaya48andClaude Opus 5.5 f9193539fb StarletBootstrapProbe: back MEM1 and MEM2 as on a retail Wii
The probe never initialised Dolphin's MEM1 and MEM2, so any Starlet
access there was an access to unmapped memory: StarOS reads its SBR1
record header at 0x00200000 on every boot, which raised two alerts
(Unknown Pointer, then Invalid range in CopyFromEmu) and read 0. The
probe now initialises both, zeroed, at their retail sizes (24 and
64 MiB), and shuts them down after the run. The read still returns 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 17:49:56 +02:00

196 lines
7.1 KiB
C++

// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
#include "Common/MsgHandler.h"
#include "Core/Core.h"
#include "Core/HW/Memmap.h"
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/Starlet/ARMCore.h"
#include "Core/IOS/Starlet/BootstrapELF.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/System.h"
namespace
{
enum class ExpectedOutcome
{
Idle,
Panic,
FatalData,
UndefinedFault,
Phase2
};
// Exit status of a run stopped by a Dolphin alert (1: failed expectation, 2: usage).
constexpr int ALERT_EXIT_STATUS = 3;
// Dolphin's default handler shows an alert in a modal MessageBox on Windows, which blocks the
// probe until someone closes it. Report the alert on stderr and fail the run at once instead.
bool FailOnAlert(const char* caption, const char* text, bool, Common::MsgType)
{
std::cout.flush();
std::cerr << "Dolphin alert (" << caption << "): " << text << std::endl;
std::_Exit(ALERT_EXIT_STATUS);
}
bool Run(const std::string& path, bool jit, ExpectedOutcome expected)
{
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file || file.tellg() <= 0 || file.tellg() > 16 * 1024 * 1024)
{
std::cerr << "Expected a readable Starlet ELF of at most 16 MiB: " << path << '\n';
return false;
}
const size_t size = static_cast<size_t>(file.tellg());
std::vector<u8> image(size);
file.seekg(0);
if (!file.read(reinterpret_cast<char*>(image.data()), size))
{
std::cerr << "Could not read Starlet ELF: " << path << '\n';
return false;
}
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
system.GetWiiIPC().Reset();
IOS::LLE::StarletMemory memory(system);
memory.PrepareBootstrap();
IOS::LLE::BootstrapELFInfo info;
std::string error;
if (!IOS::LLE::LoadBootstrapELF(image, memory, &info, &error))
{
std::cerr << error << '\n';
return false;
}
IOS::LLE::ARMCore core(memory);
core.SetJitEnabled(jit);
core.Reset(info.entry);
core.SetBigEndian(true);
constexpr u32 TRACE_MAGIC = 0x52455631;
constexpr u32 TRACE_CAPACITY = 1024;
for (int slice = 0; slice < 512; ++slice)
{
core.SetIRQLine(system.GetWiiIPC().IsStarletIRQAsserted());
core.SetFIQLine(system.GetWiiIPC().IsStarletFIQAsserted());
core.RunCycles(4096);
const u32 fault_code = memory.Read32(info.fault_code);
if (fault_code != 0)
{
std::cerr << "Starlet exception " << fault_code << " LR=" << std::hex
<< memory.Read32(info.fault_lr) << " SPSR=" << memory.Read32(info.fault_spsr)
<< " PC=" << core.GetRegister(15) << std::dec << '\n';
return expected == ExpectedOutcome::UndefinedFault && fault_code == 1 &&
memory.Read32(info.fault_lr) != 0 && memory.Read32(info.fault_spsr) != 0;
}
if (memory.Read32(info.trace) != TRACE_MAGIC)
continue;
const u32 length = memory.Read32(info.trace + 4);
if (length > TRACE_CAPACITY)
{
std::cerr << "Invalid REV_TRACE length: " << length << '\n';
return false;
}
std::string trace(length, '\0');
for (u32 index = 0; index < length; ++index)
trace[index] = static_cast<char>(memory.Read8(info.trace + 8 + index));
if (trace.find("REV:BOOT:IDLE\n") == std::string::npos &&
trace.find("REV:PANIC") == std::string::npos &&
trace.find("REV:FATAL") == std::string::npos)
{
continue;
}
std::cout << trace;
std::cout << "PC=0x" << std::hex << core.GetRegister(15) << std::dec
<< " instructions=" << core.GetExecutedInstructions() << '\n';
const bool early_startup_ok = trace.find("REV:BOOT:ENTRY\n") != std::string::npos &&
trace.find("REV:BOOT:BSS_OK\n") != std::string::npos;
if (expected == ExpectedOutcome::FatalData)
{
return early_startup_ok && trace.find("REV:FATAL:DATA\n") != std::string::npos &&
trace.find("REV:BOOT:DATA_OK\n") == std::string::npos;
}
const bool startup_ok = early_startup_ok &&
trace.find("REV:BOOT:DATA_OK\n") != std::string::npos &&
trace.find("REV:BOOT:STACK_OK\n") != std::string::npos &&
trace.find("REV:BOOT:RUST\n") != std::string::npos;
if (expected == ExpectedOutcome::Phase2)
{
const std::string_view required[] = {
"REV:P2:START\n", "ARCH PASS\n", "CRITICAL PASS\n",
"TIMER PASS\n", "MONOTONIC PASS\n", "MEM PASS\n",
"IRQ PASS\n", "BARRIER PASS\n", "LOGGING PASS\n",
"REV:P2:PASS\n", "REV:P2:KERNEL_READY\n", "REV:BOOT:IDLE\n"};
bool phase2_ok = true;
for (const std::string_view marker : required)
phase2_ok &= trace.find(marker) != std::string::npos;
return startup_ok && phase2_ok && trace.find(" FAIL\n") == std::string::npos;
}
const bool outcome_ok =
expected == ExpectedOutcome::Panic ?
trace.find("REV:PANIC") != std::string::npos :
expected == ExpectedOutcome::Idle && trace.find("REV:BOOT:IDLE\n") != std::string::npos;
return startup_ok && outcome_ok;
}
std::cerr << "No terminal bootstrap marker after 2097152 ARM cycles, PC=0x" << std::hex
<< core.GetRegister(15) << std::dec << '\n';
return false;
}
} // namespace
int main(int argc, char** argv)
{
Common::RegisterMsgAlertHandler(FailOnAlert);
if (argc < 2 || argc > 4)
{
std::cerr << "Usage: StarletBootstrapProbe <ELF> [--jit] "
"[--expect-panic|--expect-fatal-data|--expect-undefined-fault|--expect-phase2]\n";
return 2;
}
bool jit = false;
ExpectedOutcome expected = ExpectedOutcome::Idle;
bool outcome_selected = false;
for (int index = 2; index < argc; ++index)
{
const std::string_view option = argv[index];
if (option == "--jit")
jit = true;
else if (option == "--expect-panic" || option == "--expect-fatal-data" ||
option == "--expect-undefined-fault" || option == "--expect-phase2")
{
if (outcome_selected)
{
std::cerr << "Select only one expected outcome\n";
return 2;
}
outcome_selected = true;
expected = option == "--expect-panic" ? ExpectedOutcome::Panic :
option == "--expect-fatal-data" ? ExpectedOutcome::FatalData :
option == "--expect-undefined-fault" ? ExpectedOutcome::UndefinedFault :
ExpectedOutcome::Phase2;
}
else
{
std::cerr << "Unknown option: " << option << '\n';
return 2;
}
}
// The Starlet shares MEM1 and MEM2 with Broadway. Back them as on a retail Wii (24 and 64 MiB,
// zeroed): without them, any Starlet access there, such as StarOS reading its SBR1 record
// header at 0x00200000, is an access to unmapped memory and raises an alert.
auto& system = Core::System::GetInstance();
system.SetIsWii(true);
system.GetMemory().Init();
const bool passed = Run(argv[1], jit, expected);
system.GetMemory().Shutdown();
return passed ? 0 : 1;
}