Files
dolphin/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp
T
Yaya48andClaude Opus 5.5 15da28ad7b StarletBootstrapProbe: --sbr1-record and --sbr1-absent
StarOS's SBR1 boot record (MEM1 0x00200000) is created by its Broadway
loader before the image runs; the probe never had one, so the paths
that publish into it ran only on the console. --sbr1-record writes the
record as the loader and the ARM relay leave it (header, relay at its
last step, EXI channel 1 given to the Starlet, magic last); after the run
both options print the record's magic, state, reason, EXI owner, fault
code, ring length and dropped count, and how many of its 1024 words are
nonzero (0 is expected with --sbr1-absent). Without either option the
probe behaves as before.

With the StarOS M1 images (interpreter and JIT): idle publishes state 3
and hands EXI back (owner 1, ring 258 bytes, no Gecko emulated);
fatal-data state 4 reason 3; undefined-entry state 6, fault 1, ring
empty, as on the console (gates HW2-HW4).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 22:40:28 +02:00

272 lines
11 KiB
C++

// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#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;
// StarOS's SBR1 boot record in MEM1 (StarOS crates/boot-contract, report.rs).
enum class SBR1Mode
{
None,
// --sbr1-record: create the record as the loader does, report it after the run.
Record,
// --sbr1-absent: no record; report the area after the run.
Absent,
};
constexpr u32 SBR1_BASE = 0x00200000;
constexpr u32 SBR1_MAGIC = 0x53425231;
constexpr u32 SBR1_VERSION = 1;
constexpr u32 SBR1_SIZE = 0x1000;
constexpr u32 SBR1_EXI_STARLET = 2;
constexpr u32 SBR1_RELAY_JUMPING = 4;
// 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, bool mmu_faults, 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);
// StarOS MMU-001: translation, domain, permission and alignment aborts (off by default).
core.SetMMUFaultsEnabled(mmu_faults);
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;
}
// StarOS's panic path writes no trace line: it sets its guard word, then its code, and halts.
// The trace published so far is printed for the scenario's ordered checks.
if (info.panic_state != 0 && memory.Read32(info.panic_state) != 0)
{
const u32 panic_state = memory.Read32(info.panic_state);
const u32 panic_code = memory.Read32(info.panic_code);
const u32 length = memory.Read32(info.trace) == TRACE_MAGIC ?
std::min(memory.Read32(info.trace + 4), TRACE_CAPACITY) :
0;
for (u32 index = 0; index < length; ++index)
std::cout << static_cast<char>(memory.Read8(info.trace + 8 + index));
std::cout << "PC=0x" << std::hex << core.GetRegister(15) << std::dec
<< " instructions=" << core.GetExecutedInstructions() << '\n';
std::cerr << "Starlet panic state " << panic_state << " code " << panic_code << '\n';
return expected == ExpectedOutcome::Panic && panic_state == 1;
}
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 > 6)
{
std::cerr << "Usage: StarletBootstrapProbe <ELF> [--jit] [--mmu-faults] "
"[--sbr1-record|--sbr1-absent] "
"[--expect-panic|--expect-fatal-data|--expect-undefined-fault|--expect-phase2]\n";
return 2;
}
bool jit = false;
bool mmu_faults = false;
SBR1Mode sbr1 = SBR1Mode::None;
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 == "--mmu-faults")
mmu_faults = true;
else if (option == "--sbr1-record" || option == "--sbr1-absent")
{
if (sbr1 != SBR1Mode::None)
{
std::cerr << "Select only one SBR1 mode\n";
return 2;
}
sbr1 = option == "--sbr1-record" ? SBR1Mode::Record : SBR1Mode::Absent;
}
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();
auto& memory = system.GetMemory();
if (sbr1 == SBR1Mode::Record)
{
// As the Broadway loader and the ARM relay leave StarOS's SBR1 boot record before the image
// runs (StarOS docs/contracts/wii-dev-boot.md): header, the relay at its last step, EXI
// channel 1 given to the Starlet, the magic written last.
memory.Write_U32(SBR1_VERSION, SBR1_BASE + 0x04);
memory.Write_U32(SBR1_SIZE, SBR1_BASE + 0x08);
memory.Write_U32(SBR1_EXI_STARLET, SBR1_BASE + 0x0c);
memory.Write_U32(SBR1_RELAY_JUMPING, SBR1_BASE + 0x10);
memory.Write_U32(SBR1_MAGIC, SBR1_BASE);
}
const bool passed = Run(argv[1], jit, mmu_faults, expected);
if (sbr1 != SBR1Mode::None)
{
// The record's summary after the run; with --sbr1-absent every word must still be zero.
std::cerr << std::hex << "SBR1 magic 0x" << memory.Read_U32(SBR1_BASE) << std::dec
<< " state " << memory.Read_U32(SBR1_BASE + 0x18) << " reason "
<< memory.Read_U32(SBR1_BASE + 0x1c) << " owner "
<< memory.Read_U32(SBR1_BASE + 0x0c) << " fault "
<< memory.Read_U32(SBR1_BASE + 0x20) << " ring "
<< memory.Read_U32(SBR1_BASE + 0x2c) << " dropped "
<< memory.Read_U32(SBR1_BASE + 0x30);
u32 written = 0;
for (u32 offset = 0; offset < SBR1_SIZE; offset += 4)
written += memory.Read_U32(SBR1_BASE + offset) != 0;
std::cerr << " nonzero_words " << written << '\n';
}
system.GetMemory().Shutdown();
return passed ? 0 : 1;
}