With no handler registered, Dolphin shows an alert in a modal MessageBox on Windows, and the probe waits until someone closes it. A run that hits an alert then looks like a hang (StarOS EXP-001). The probe now writes the alert to stderr and exits at once with status 3. Co-Authored-By: Claude Opus 5.5 <[email protected]>
186 lines
6.6 KiB
C++
186 lines
6.6 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/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;
|
|
}
|
|
}
|
|
return Run(argv[1], jit, expected) ? 0 : 1;
|
|
}
|