The probe printed the ring's length only. StarOS's EXP-003 images put lines in the ring that REV_TRACE has no room for (the MEM2 monitor, the measurements), so under Dolphin nothing showed them. After the record's summary, --sbr1-record now prints the ring's text between "SBR1 RING BEGIN" and "SBR1 RING END" on stderr, as StarOS's Broadway loader prints it over the Gecko on the console: at most the ring's 4032 bytes, a byte outside printable ASCII and newline, and the backslash, written \xHH. The formatting is in ProbeRing.h, tested by StarletProbeRingTest. Co-Authored-By: Claude Opus 5.5 <[email protected]>
324 lines
13 KiB
C++
324 lines
13 KiB
C++
// Copyright 2026 Dolphin Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include <algorithm>
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <optional>
|
|
#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"
|
|
#include "ProbeBudget.h"
|
|
#include "ProbeRing.h"
|
|
#include "StarOSPanicCheck.h"
|
|
|
|
namespace
|
|
{
|
|
using namespace StarletBootstrapProbe;
|
|
|
|
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;
|
|
|
|
// Slices run once a StarOS panic path looks finished, to see that it stays so.
|
|
constexpr int PANIC_SETTLE_SLICES = 2;
|
|
|
|
// 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, SBR1Mode sbr1,
|
|
ExpectedOutcome expected, u64 cycles)
|
|
{
|
|
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;
|
|
auto& mem1 = system.GetMemory();
|
|
const auto read_mem1 = [&mem1](u32 address) { return mem1.Read_U32(address); };
|
|
const auto panic_unmet = [&] {
|
|
return UnmetPanicCondition(memory.Read32(info.panic_state), memory.Read32(info.panic_code),
|
|
sbr1, read_mem1);
|
|
};
|
|
// StarOS's panic path writes no trace line. Print the trace published so far, for the
|
|
// scenario's ordered checks, and the panic words.
|
|
const auto report_panic = [&] {
|
|
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 " << memory.Read32(info.panic_state) << " code "
|
|
<< memory.Read32(info.panic_code) << '\n';
|
|
};
|
|
int panic_settled = 0;
|
|
for (u64 slice = 0; slice < cycles / SLICE_CYCLES; ++slice)
|
|
{
|
|
core.SetIRQLine(system.GetWiiIPC().IsStarletIRQAsserted());
|
|
core.SetFIQLine(system.GetWiiIPC().IsStarletFIQAsserted());
|
|
core.RunCycles(SLICE_CYCLES);
|
|
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 sets its guard word first, then its code, then publishes the SBR1
|
|
// record when there is one, and halts. An expected panic passes once all of that holds
|
|
// (UnmetPanicCondition) and still holds PANIC_SETTLE_SLICES slices later.
|
|
if (info.panic_state != 0 && memory.Read32(info.panic_state) != 0)
|
|
{
|
|
if (expected != ExpectedOutcome::Panic)
|
|
{
|
|
report_panic();
|
|
return false;
|
|
}
|
|
if (!panic_unmet().empty())
|
|
{
|
|
panic_settled = 0;
|
|
continue;
|
|
}
|
|
if (++panic_settled <= PANIC_SETTLE_SLICES)
|
|
continue;
|
|
report_panic();
|
|
return true;
|
|
}
|
|
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;
|
|
}
|
|
if (info.panic_state != 0 && memory.Read32(info.panic_state) != 0)
|
|
{
|
|
report_panic();
|
|
const std::string unmet = panic_unmet();
|
|
std::cerr << "StarOS panic path unfinished after " << cycles
|
|
<< " ARM cycles: " << (unmet.empty() ? "it ended inside the settle slices" : unmet)
|
|
<< '\n';
|
|
return false;
|
|
}
|
|
std::cerr << "No terminal bootstrap marker after " << cycles << " 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 > 8)
|
|
{
|
|
std::cerr << "Usage: StarletBootstrapProbe <ELF> [--jit] [--mmu-faults] "
|
|
"[--sbr1-record|--sbr1-absent] "
|
|
"[--expect-panic|--expect-fatal-data|--expect-undefined-fault|--expect-phase2] "
|
|
"[--cycles <n>]\n";
|
|
return 2;
|
|
}
|
|
bool jit = false;
|
|
bool mmu_faults = false;
|
|
SBR1Mode sbr1 = SBR1Mode::None;
|
|
ExpectedOutcome expected = ExpectedOutcome::Idle;
|
|
bool outcome_selected = false;
|
|
std::optional<u64> cycles;
|
|
for (int index = 2; index < argc; ++index)
|
|
{
|
|
const std::string_view option = argv[index];
|
|
if (option == "--cycles")
|
|
{
|
|
// A whole number of slices, at most MAX_CYCLES (ProbeBudget.h); given once.
|
|
if (cycles || index + 1 == argc || !(cycles = ParseCycles(argv[index + 1])))
|
|
{
|
|
std::cerr << "--cycles takes one decimal multiple of " << SLICE_CYCLES << ", at most "
|
|
<< MAX_CYCLES << ", once\n";
|
|
return 2;
|
|
}
|
|
++index;
|
|
}
|
|
else 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 + SBR1_EXI_OWNER);
|
|
memory.Write_U32(SBR1_RELAY_JUMPING, SBR1_BASE + SBR1_RELAY_STATUS);
|
|
memory.Write_U32(SBR1_MAGIC, SBR1_BASE);
|
|
}
|
|
const bool passed =
|
|
Run(argv[1], jit, mmu_faults, sbr1, expected, cycles.value_or(DEFAULT_CYCLES));
|
|
if (sbr1 != SBR1Mode::None)
|
|
{
|
|
// The record's summary after the run; with --sbr1-absent every word must still be zero (an
|
|
// expected panic fails otherwise).
|
|
std::cerr << std::hex << "SBR1 magic 0x" << memory.Read_U32(SBR1_BASE) << std::dec
|
|
<< " state " << memory.Read_U32(SBR1_BASE + SBR1_STARLET_STATE) << " reason "
|
|
<< memory.Read_U32(SBR1_BASE + SBR1_FATAL_REASON) << " owner "
|
|
<< memory.Read_U32(SBR1_BASE + SBR1_EXI_OWNER) << " fault "
|
|
<< memory.Read_U32(SBR1_BASE + SBR1_FAULT_CODE) << " ring "
|
|
<< memory.Read_U32(SBR1_BASE + SBR1_RING_LENGTH) << " dropped "
|
|
<< memory.Read_U32(SBR1_BASE + SBR1_RING_DROPPED);
|
|
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';
|
|
if (sbr1 == SBR1Mode::Record)
|
|
{
|
|
// The ring's text, as StarOS's Broadway loader prints it over the Gecko on the console;
|
|
// END starts its own line even when the text does not end with one.
|
|
const std::string ring =
|
|
RingText([&memory](u32 offset) { return memory.Read_U8(SBR1_BASE + offset); },
|
|
memory.Read_U32(SBR1_BASE + SBR1_RING_LENGTH));
|
|
std::cerr << "SBR1 RING BEGIN\n"
|
|
<< ring << (ring.empty() || ring.back() == '\n' ? "" : "\n") << "SBR1 RING END\n";
|
|
}
|
|
}
|
|
system.GetMemory().Shutdown();
|
|
return passed ? 0 : 1;
|
|
}
|