Files
dolphin/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp
T
Yaya48andClaude Opus 5.5 612cab7b30 StarletBootstrapProbe: report StarOS's panic words
StarOS's panic path (ADR-0014 R6) writes no trace line: it sets the guard
word __panic_state, stores __panic_code and halts. LoadBootstrapELF now
returns both symbols when an image has them (optional, but both or
neither, as words of the data segment), and the probe stops a run whose
guard is set: it prints the trace published so far and
"Starlet panic state <n> code <n>", and passes --expect-panic when the
state is 1. Images without these symbols (StarletOS) load and run as
before; a REV:PANIC trace line still satisfies --expect-panic.

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

218 lines
8.4 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;
// 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 > 5)
{
std::cerr << "Usage: StarletBootstrapProbe <ELF> [--jit] [--mmu-faults] "
"[--expect-panic|--expect-fatal-data|--expect-undefined-fault|--expect-phase2]\n";
return 2;
}
bool jit = false;
bool mmu_faults = 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 == "--mmu-faults")
mmu_faults = 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, mmu_faults, expected);
system.GetMemory().Shutdown();
return passed ? 0 : 1;
}