mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
The review of the exit sequence found that svc.c case 0x07 logged "guest ExitProcess at ...; exiting" before rt_exit_begin. When a close had already begun, the guest thread that the guard then parks still claimed to exit, and past the log cap the line fell outside the end's reserve, which rt_exit_begin opens. It also found that host.h and gfx_wsys.h still said gfx_window_closed does not return, though it returns for its own thread coming back, and that rt_exit_begin's comment spoke of the window thread handling a message inside the sequence, which since the close runs on a thread of its own happens only in the fallback. svc.c logs the line after rt_exit_begin returns 1. The two header comments say gfx_window_closed returns only for its own thread coming back (gfx_present.c rt_exit_begin), and rt_exit_begin's comment says the window thread runs the sequence itself only when gfx_window_win.c exit_start could make no thread. tests/exit_order_check.py checks statically that 0x07's rt_log comes after rt_exit_begin; it fails on the code before this change and passes after it. exit_order_check and the compile check of every runtime file (a header changed) pass, with no new -Wall warnings in svc.c and gfx_present.c. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
257 lines
11 KiB
C
257 lines
11 KiB
C
// Contains code Copyright (c) 2026 MacNativeGames contributors (MIT; see THIRD_PARTY_NOTICES.md).
|
|
// The SVC entry of the lifted code: preemption checkpoint, trace, then the thread and
|
|
// synchronization SVCs (kernel.c), IPC (ipc.c), memory (memory.c), Break, OutputDebugString, GetInfo and ExitProcess. The 21 SVCs
|
|
// NX2navi has but never reaches on its boot path stop with a named fatal, the caller and the guest backtrace.
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "fs.h"
|
|
#include "host.h"
|
|
#include "kernel.h"
|
|
#include "vk.h"
|
|
#include "platform/platform.h"
|
|
|
|
#define R_OK 0u
|
|
#define ST_FREE 0u
|
|
#define ST_STACK 0xBu
|
|
#define PERM_RW 3u
|
|
#define PROGRAM_ID 0x0400486024dec000ull // the game's title id (GetInfo 18); fs_host.c NX2NAVI_APPLICATION_ID is the same
|
|
|
|
static uint64_t g_svcs;
|
|
void (*volatile svc_test_exit_hook)(unsigned code);
|
|
|
|
RtStats rt_stats(void) {
|
|
return (RtStats){__atomic_load_n(&g_svcs, __ATOMIC_RELAXED), kern_threads_created(), hostfn_calls()};
|
|
}
|
|
|
|
static int readable(uint64_t a, uint64_t n) { return mem_host_readable(a, n); }
|
|
|
|
void kern_backtrace(const Cpu *c) {
|
|
char d[160];
|
|
rt_logf(" lr %s\n", ldr_describe(c->x[30], d, sizeof d));
|
|
uint64_t fp = c->x[29];
|
|
for (int n = 0; n < 32 && fp && !(fp & 7) && readable(fp, 16); n++) {
|
|
uint64_t next = LD64(fp), lr = LD64(fp + 8);
|
|
rt_logf(" guest frame %2d: %s\n", n, ldr_describe(lr, d, sizeof d));
|
|
if (next <= fp || next - fp > (64u << 20)) break;
|
|
fp = next;
|
|
}
|
|
}
|
|
|
|
static const struct { uint32_t imm; const char *name; } unimplemented[] = {
|
|
{0x13, "MapSharedMemory"}, {0x14, "UnmapSharedMemory"},
|
|
{0x23, "SendAsyncRequestWithUserBuffer"}, {0x28, "ReturnFromException"}, {0x2A, "FlushEntireDataCache"},
|
|
{0x2F, "GetLastThreadInfo"}, {0x32, "SetThreadActivity"}, {0x33, "GetThreadContext3"},
|
|
{0x36, "SynchronizePreemptionState"}, {0x40, "CreateSession"}, {0x41, "AcceptSession"},
|
|
{0x43, "ReplyAndReceive"}, {0x44, "ReplyAndReceiveWithUserBuffer"}, {0x48, "MapPhysicalMemoryUnsafe"},
|
|
{0x49, "UnmapPhysicalMemoryUnsafe"}, {0x4B, "CreateCodeMemory"}, {0x50, "CreateSharedMemory"},
|
|
{0x51, "MapTransferMemory"}, {0x52, "UnmapTransferMemory"}, {0x70, "CreatePort"}, {0x72, "ConnectToPort"},
|
|
};
|
|
|
|
static __attribute__((noreturn)) void svc_fatal(Cpu *c, uint32_t imm, uint64_t pc, const char *why) {
|
|
const char *name = "unknown";
|
|
for (unsigned i = 0; i < sizeof unimplemented / sizeof *unimplemented; i++)
|
|
if (unimplemented[i].imm == imm) name = unimplemented[i].name;
|
|
char at[160];
|
|
fflush(stdout);
|
|
rt_logf("FATAL: svc #%#x %s at %s: %s\n x0 %#llx x1 %#llx x2 %#llx x3 %#llx\n", imm, name,
|
|
ldr_describe(pc, at, sizeof at), why, (unsigned long long)c->x[0], (unsigned long long)c->x[1],
|
|
(unsigned long long)c->x[2], (unsigned long long)c->x[3]);
|
|
kern_backtrace(c);
|
|
rt_fatal("svc #%#x %s is not implemented", imm, name);
|
|
}
|
|
|
|
// QueryMemory sees the guest main thread's stack, which memory.c does not know: Stack inside it, free ranges
|
|
// around it clipped (the way memory.c clips moved stacks).
|
|
static void main_stack_query(uint64_t a, RtMemoryInfo *r) {
|
|
uint64_t mb, ms;
|
|
kern_main_stack(&mb, &ms);
|
|
if (!ms) return;
|
|
uint64_t me = mb + ms;
|
|
if (a >= mb && a < me) {
|
|
*r = (RtMemoryInfo){.base = mb, .size = ms, .state = ST_STACK, .perm = PERM_RW};
|
|
return;
|
|
}
|
|
if (r->state != ST_FREE) return;
|
|
uint64_t lo = r->base, hi = r->base + r->size;
|
|
if (me <= a && me > lo) lo = me;
|
|
if (mb > a && mb < hi) hi = mb;
|
|
r->base = lo;
|
|
r->size = hi - lo;
|
|
}
|
|
|
|
static int on_main_stack(uint64_t a, uint64_t n) {
|
|
uint64_t mb, ms;
|
|
kern_main_stack(&mb, &ms);
|
|
return ms && a < mb + ms && mb < a + n;
|
|
}
|
|
|
|
static uint64_t user_exception_context(void) {
|
|
static void *page;
|
|
void *p = __atomic_load_n(&page, __ATOMIC_ACQUIRE);
|
|
if (!p) {
|
|
void *mine = mem_kernel_alloc(0x1000, 0x1000), *expected = NULL;
|
|
if (__atomic_compare_exchange_n(&page, &expected, mine, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) p = mine;
|
|
else {
|
|
mem_kernel_free(mine, 0x1000);
|
|
p = expected;
|
|
}
|
|
}
|
|
return (uint64_t)(uintptr_t)p;
|
|
}
|
|
|
|
// GetInfo(w1 id, w2 handle, x3 sub id -> x1)
|
|
static void get_info(Cpu *c, uint64_t pc) {
|
|
uint32_t id = (uint32_t)c->x[1];
|
|
uint64_t v = 0;
|
|
if (mem_info(id, &v)) {
|
|
c->x[0] = R_OK;
|
|
c->x[1] = v;
|
|
return;
|
|
}
|
|
switch (id) {
|
|
case 0: v = 0x3f; break; // core mask: cores 0-5 (Switch 2 application cores; see kernel.c NCORE)
|
|
case 1: v = 0x0ffffffff0000000ull; break; // priority mask
|
|
case 8: v = 0; break; // debugger attached
|
|
case 10: v = 0; break; // idle tick count
|
|
case 11: // random entropy
|
|
if (!plat_random(&v, sizeof v)) rt_fatal("GetInfo 11: no random bytes from the system");
|
|
break;
|
|
case 18: v = PROGRAM_ID; break;
|
|
case 20: v = user_exception_context(); break; // the SDK keeps e.g. the dying message at +0x1C0
|
|
case 23: v = 1; break;
|
|
case 29: // region the SDK records at nnosInitialize (address, then
|
|
case 30: v = 0; break;
|
|
case 33: v = 0x1000; break; // VAMM control region size: mapped at 0x300000000 and made into a unit heap
|
|
default: {
|
|
char why[64];
|
|
snprintf(why, sizeof why, "GetInfo id %u (handle %#x, sub id %#llx)", id, (uint32_t)c->x[2],
|
|
(unsigned long long)c->x[3]);
|
|
svc_fatal(c, 0x29, pc, why);
|
|
}
|
|
}
|
|
c->x[0] = R_OK;
|
|
c->x[1] = v;
|
|
}
|
|
|
|
static void svc_body(Cpu *c, uint32_t imm, uint64_t pc);
|
|
|
|
void rt_svc(Cpu *c, uint32_t imm, uint64_t pc) {
|
|
kern_checkpoint();
|
|
if (__builtin_expect(!kern_host_track || !c->thread, 1)) {
|
|
svc_body(c, imm, pc);
|
|
return;
|
|
}
|
|
void *t = c->thread; // kern_host_track: the SVC counted in the thread's host depth (hostfn.c hostfn_tracked)
|
|
int *d = kern_host_depth(t);
|
|
__atomic_store_n(d, *d + 1, __ATOMIC_RELAXED);
|
|
svc_body(c, imm, pc);
|
|
int n = *d - 1;
|
|
__atomic_store_n(d, n, __ATOMIC_RELAXED);
|
|
if (!n && kern_preempt_pending(t)) kern_host_left(t);
|
|
}
|
|
|
|
static void svc_body(Cpu *c, uint32_t imm, uint64_t pc) {
|
|
__atomic_fetch_add(&g_svcs, 1, __ATOMIC_RELAXED);
|
|
if (rt_trace & RT_TRACE_SVC) {
|
|
char at[160];
|
|
rt_log("svc %#x at %s x0=%#llx x1=%#llx x2=%#llx x3=%#llx", imm, ldr_describe(pc, at, sizeof at),
|
|
(unsigned long long)c->x[0], (unsigned long long)c->x[1], (unsigned long long)c->x[2],
|
|
(unsigned long long)c->x[3]);
|
|
}
|
|
if (kern_svc(c, imm)) return;
|
|
uint64_t *x = c->x;
|
|
switch (imm) {
|
|
case 0x15: case 0x1F: case 0x21: case 0x22:
|
|
if (ipc_svc(c, imm)) return;
|
|
svc_fatal(c, imm, pc, "not handled by ipc.c");
|
|
case 0x01: { // SetHeapSize(x1 size -> x1 address)
|
|
uint64_t a = 0;
|
|
uint32_t r = mem_set_heap_size(x[1], &a);
|
|
x[0] = r;
|
|
if (!r) x[1] = a;
|
|
return;
|
|
}
|
|
case 0x02: x[0] = mem_set_permission(x[0], x[1], (uint32_t)x[2]); return;
|
|
case 0x03: x[0] = mem_set_attribute(x[0], x[1], (uint32_t)x[2], (uint32_t)x[3]); return;
|
|
case 0x04: // MapMemory(x0 dst, x1 src, x2 size)
|
|
x[0] = on_main_stack(x[0], x[2]) ? KERN_R_INVALID_STATE_MEM : mem_map(x[0], x[1], x[2]);
|
|
return;
|
|
case 0x05: x[0] = mem_unmap(x[0], x[1], x[2]); return;
|
|
case 0x06: { // QueryMemory(x0 MemoryInfo*, x2 address -> w1 page info)
|
|
RtMemoryInfo mi;
|
|
uint32_t r = mem_query(x[2], &mi);
|
|
main_stack_query(x[2], &mi);
|
|
if (!r && x[0]) memcpy((void *)(uintptr_t)x[0], &mi, sizeof mi);
|
|
x[0] = r;
|
|
x[1] = 0;
|
|
return;
|
|
}
|
|
case 0x2C: x[0] = mem_map_physical(x[0], x[1]); return;
|
|
case 0x2D: x[0] = mem_unmap_physical(x[0], x[1]); return;
|
|
case 0x26: { // Break(x0 reason, x1 info, x2 info size); bit 31 of the reason: notification only
|
|
uint32_t result = 0;
|
|
if (x[1] && x[2] >= 4 && readable(x[1], 4)) memcpy(&result, (const void *)(uintptr_t)x[1], 4);
|
|
char at[160];
|
|
rt_log("guest Break reason %#llx result %#x at %s", (unsigned long long)x[0], result,
|
|
ldr_describe(pc, at, sizeof at));
|
|
if (x[0] & 0x80000000ull) {
|
|
x[0] = R_OK;
|
|
return;
|
|
}
|
|
{ // the SDK keeps the abort text in the user exception context: show any readable run
|
|
const char *ctx = (const char *)(uintptr_t)user_exception_context();
|
|
char run[161];
|
|
int n = 0;
|
|
for (int i = 0; i < 0x400; i++) {
|
|
unsigned char ch = (unsigned char)ctx[i];
|
|
if (ch >= 0x20 && ch < 0x7f) {
|
|
if (n < 160) run[n++] = (char)ch;
|
|
} else if (n >= 4) {
|
|
run[n] = 0;
|
|
rt_log("guest abort text at ctx+%#x: %s", i - n, run);
|
|
n = 0;
|
|
} else {
|
|
n = 0;
|
|
}
|
|
}
|
|
}
|
|
{ // the object the aborting code was working on: its first bytes carry the state machine's byte
|
|
uint64_t obj = c->x[19];
|
|
if (obj && readable(obj, 32)) {
|
|
const unsigned char *p = (const unsigned char *)(uintptr_t)obj;
|
|
rt_log("guest abort object %#llx: %02x %02x %02x %02x %02x %02x %02x %02x "
|
|
"%02x %02x %02x %02x %02x %02x %02x %02x", (unsigned long long)obj,
|
|
p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7],
|
|
p[8], p[9], p[10], p[11], p[12], p[13], p[14], p[15]);
|
|
}
|
|
}
|
|
svc_fatal(c, imm, pc, "Break (the SDK aborted)");
|
|
}
|
|
case 0x27: { // OutputDebugString(x0 text, x1 size)
|
|
uint64_t n = x[1];
|
|
if (n > 65536) svc_fatal(c, imm, pc, "debug string longer than 64 KiB");
|
|
if (n) {
|
|
fflush(stdout);
|
|
rt_log_raw((const char *)(uintptr_t)x[0], n);
|
|
}
|
|
x[0] = R_OK;
|
|
return;
|
|
}
|
|
case 0x29: get_info(c, pc); return;
|
|
case 0x07: { // ExitProcess, as closing the window: the orderly end (gfx_present.c rt_exit_steps), exit code 0
|
|
// once, shared with the window's close: a close already ending the process keeps this thread there, and the
|
|
// exit line is that close's alone; this thread coming back would resume guest code after its ExitProcess, so it
|
|
// ends the process instead
|
|
if (!rt_exit_begin()) plat_exit(0);
|
|
char at[160]; // after the guard, which also lets the line reach the log file past its cap (main.c log_sink)
|
|
rt_log("guest ExitProcess at %s; exiting", ldr_describe(pc, at, sizeof at));
|
|
rt_exit_steps(RT_EXIT_SETTINGS);
|
|
fflush(NULL);
|
|
if (svc_test_exit_hook) return svc_test_exit_hook(0);
|
|
plat_exit(0);
|
|
}
|
|
}
|
|
svc_fatal(c, imm, pc, "not implemented");
|
|
}
|