mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
With fewer than six virtual cores a thread pinned to a core at or above the count runs on core (c mod count), and GetCurrentProcessorNumber told it that virtual core's number. The game keys per-core data by the number without locks, so with four cores a thread pinned to core 5 used core 1's data in turn with core 1's own threads, and the game died within seconds of the first frame in play. The Steam Deck profile sets four cores, so this was every start under that profile (issue #445). The call now answers with one of the thread's own cores that fold onto the virtual core it holds, its ideal core first; with all six cores the answer is as before. The kernel probe gets a test with two threads pinned to each of the six cores: with 6, 4, 3 and 1 virtual cores no thread sees another core's number and no two see the same at once. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
813 lines
36 KiB
C
813 lines
36 KiB
C
// --probe-kernel: the kernel's waits, wake orders, virtual cores, fibers and
|
|
// ExitThread on guest threads whose entries are host functions, then real lifted code on four guest threads: main's
|
|
// PLT stub for nn::os::GetSystemTick, through the lifted sdk function, to rt_ticks.
|
|
//
|
|
// Every probe thread is created with SVC 0x08 and drives the kernel through rt_svc, the entry the lifted code uses.
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "cfg.h"
|
|
#include "host.h"
|
|
#include "kernel.h"
|
|
#include "platform/platform.h"
|
|
|
|
#define STACK 0x10000
|
|
#define JOIN_NS 10000000000ll
|
|
#define TICK_SYMBOL "_ZN2nn2os13GetSystemTickEv"
|
|
#define FIBER_SWITCHES 10000
|
|
#define CORE_WORKERS 8
|
|
#define CORE_RUNS 300
|
|
#define TICK_THREADS 4
|
|
#define TICK_CALLS 1000
|
|
|
|
static int failures;
|
|
#define CHECK(cond, ...) do { if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
|
|
|
|
static uint64_t a_lock_waiter, a_cv_waiter, a_cv_order_waiter, a_arb_waiter, a_event_waiter, a_core_worker,
|
|
a_exit_entry, a_exit_nested, a_fiber_fn, a_fiber_exit_thread, a_fiber_exit_fn, a_tick_worker;
|
|
static uint64_t a_fib_init, a_fib_fin, a_fib_switch, a_fib_cur, tick_stub;
|
|
|
|
// ---- helpers: SVCs through rt_svc, thread start and join
|
|
static uint32_t sv(Cpu *c, uint32_t imm, uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3) {
|
|
c->x[0] = a0;
|
|
c->x[1] = a1;
|
|
c->x[2] = a2;
|
|
c->x[3] = a3;
|
|
rt_svc(c, imm, 0);
|
|
return (uint32_t)c->x[0];
|
|
}
|
|
|
|
static void sleep_ms(Cpu *c, int ms) { sv(c, 0x0B, (uint64_t)ms * 1000000, 0, 0, 0); }
|
|
static __attribute__((noreturn)) void exit_thread(Cpu *c) {
|
|
sv(c, 0x0A, 0, 0, 0, 0);
|
|
rt_fatal("probe-kernel: ExitThread returned");
|
|
}
|
|
static uint64_t ga(const volatile void *p) { return (uint64_t)(uintptr_t)p; }
|
|
|
|
static uint32_t create(Cpu *c, uint64_t entry, void *arg, int prio, int core) {
|
|
uint8_t *stack = mem_kernel_alloc(STACK, 16);
|
|
c->x[1] = entry;
|
|
c->x[2] = ga(arg);
|
|
c->x[3] = ga(stack + STACK);
|
|
c->x[4] = (uint64_t)prio;
|
|
c->x[5] = (uint64_t)(int64_t)core;
|
|
rt_svc(c, 0x08, 0);
|
|
if (c->x[0]) rt_fatal("probe-kernel: CreateThread failed %#llx", (unsigned long long)c->x[0]);
|
|
return (uint32_t)c->x[1];
|
|
}
|
|
|
|
static void start(Cpu *c, uint32_t h) { CHECK(sv(c, 0x09, h, 0, 0, 0) == 0, "StartThread %#x", h); }
|
|
|
|
static uint32_t wait1(Cpu *c, uint32_t h, int64_t ns) {
|
|
uint32_t hs[1] = {h};
|
|
return sv(c, 0x18, 0, ga(hs), 1, (uint64_t)ns);
|
|
}
|
|
|
|
static void join(Cpu *c, uint32_t h) {
|
|
uint32_t r = wait1(c, h, JOIN_NS);
|
|
if (r) rt_fatal("probe-kernel: thread %#x did not end (wait result %#x)", h, r);
|
|
CHECK(sv(c, 0x16, h, 0, 0, 0) == 0, "CloseHandle(thread %#x)", h);
|
|
}
|
|
|
|
// Poll (sleeping, so the virtual core is free) until the thread reaches a state; 5 s at most.
|
|
static int wait_state(Cpu *c, uint32_t h, int want) {
|
|
for (int i = 0; i < 5000; i++) {
|
|
if (kern_thread_state(h) == want) return 1;
|
|
sleep_ms(c, 1);
|
|
}
|
|
CHECK(0, "thread %#x never reached state %d (now %d)", h, want, kern_thread_state(h));
|
|
return 0;
|
|
}
|
|
|
|
static double ms_since(uint64_t t0) { return (double)(kern_now_ns() - t0) / 1e6; }
|
|
|
|
// ---- user-mode lock of the SDK: tag word = owner handle | waiters bit
|
|
static uint32_t ld(volatile uint32_t *a) { return __atomic_load_n(a, __ATOMIC_SEQ_CST); }
|
|
static int cas(volatile uint32_t *a, uint32_t *e, uint32_t v) {
|
|
return __atomic_compare_exchange_n(a, e, v, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST);
|
|
}
|
|
|
|
static void user_lock(Cpu *c, volatile uint32_t *a, uint32_t tag) {
|
|
for (;;) {
|
|
uint32_t v = 0;
|
|
if (cas(a, &v, tag)) return;
|
|
if (!(v & KERN_WAITERS_BIT)) {
|
|
if (!cas(a, &v, v | KERN_WAITERS_BIT)) continue;
|
|
v |= KERN_WAITERS_BIT;
|
|
}
|
|
sv(c, 0x1A, v & ~KERN_WAITERS_BIT, ga(a), tag, 0);
|
|
if ((ld(a) & ~KERN_WAITERS_BIT) == tag) return;
|
|
}
|
|
}
|
|
|
|
static void user_unlock(Cpu *c, volatile uint32_t *a, uint32_t tag) {
|
|
uint32_t v = tag;
|
|
if (cas(a, &v, 0)) return;
|
|
sv(c, 0x1B, ga(a), 0, 0, 0);
|
|
}
|
|
|
|
// ---- lock handoff order
|
|
typedef struct { volatile uint32_t *lock, *key; uint32_t tag, r; int idx, held; } LockJob;
|
|
static volatile uint32_t g_lockw __attribute__((aligned(8))), g_keyw __attribute__((aligned(8)));
|
|
static volatile int32_t g_order_n;
|
|
static int g_order[8];
|
|
|
|
static void record(int idx) { g_order[plat_inc32(&g_order_n) - 1] = idx; }
|
|
|
|
static void lock_waiter(Cpu *c) {
|
|
LockJob *j = arg_p(c, 0);
|
|
user_lock(c, j->lock, j->tag);
|
|
record(j->idx);
|
|
user_unlock(c, j->lock, j->tag);
|
|
exit_thread(c);
|
|
}
|
|
|
|
static int order_is(const int *want, int n, char *buf, size_t size) {
|
|
int ok = g_order_n == n;
|
|
buf[0] = 0;
|
|
for (int i = 0; i < g_order_n && i < 8; i++) {
|
|
snprintf(buf + strlen(buf), size - strlen(buf), "%s%d", i ? " " : "", g_order[i]);
|
|
ok &= i < n && g_order[i] == want[i];
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
static void test_lock(Cpu *c, uint32_t me) {
|
|
static const int prio[] = {40, 30, 40, 30, 35}, want[] = {1, 3, 4, 0, 2};
|
|
LockJob j[5];
|
|
uint32_t h[5];
|
|
g_lockw = me;
|
|
g_order_n = 0;
|
|
for (int i = 0; i < 5; i++) {
|
|
j[i] = (LockJob){.lock = &g_lockw, .idx = i};
|
|
h[i] = create(c, a_lock_waiter, &j[i], prio[i], -2);
|
|
j[i].tag = h[i];
|
|
start(c, h[i]);
|
|
wait_state(c, h[i], KERN_WAIT_LOCK);
|
|
}
|
|
CHECK(ld(&g_lockw) == (me | KERN_WAITERS_BIT), "lock word %#x with 5 waiters", ld(&g_lockw));
|
|
user_unlock(c, &g_lockw, me);
|
|
for (int i = 0; i < 5; i++) join(c, h[i]);
|
|
char got[64];
|
|
int ok = order_is(want, 5, got, sizeof got);
|
|
printf("lock handoff: priorities 40 30 40 30 35 registered in order, woken %s (expected 1 3 4 0 2): %s\n", got,
|
|
ok ? "ok" : "FAIL");
|
|
CHECK(ok, "lock handoff order");
|
|
CHECK(ld(&g_lockw) == 0, "lock word %#x after the last unlock", ld(&g_lockw));
|
|
}
|
|
|
|
// ---- condition variables
|
|
static void cv_waiter(Cpu *c) { // waits forever; woken holding the lock
|
|
LockJob *j = arg_p(c, 0);
|
|
user_lock(c, j->lock, j->tag);
|
|
j->r = sv(c, 0x1C, ga(j->lock), ga(j->key), j->tag, (uint64_t)-1);
|
|
j->held = (ld(j->lock) & ~KERN_WAITERS_BIT) == j->tag;
|
|
record(j->idx);
|
|
user_unlock(c, j->lock, j->tag);
|
|
exit_thread(c);
|
|
}
|
|
|
|
static void test_cv_wake_order(Cpu *c) {
|
|
static const int prio[] = {45, 35, 40}, want[] = {1, 2, 0};
|
|
LockJob j[3];
|
|
uint32_t h[3];
|
|
g_lockw = 0;
|
|
g_keyw = 0;
|
|
g_order_n = 0;
|
|
for (int i = 0; i < 3; i++) {
|
|
j[i] = (LockJob){.lock = &g_lockw, .key = &g_keyw, .idx = i};
|
|
h[i] = create(c, a_cv_waiter, &j[i], prio[i], -2);
|
|
j[i].tag = h[i];
|
|
start(c, h[i]);
|
|
wait_state(c, h[i], KERN_WAIT_CONDVAR);
|
|
}
|
|
CHECK(ld(&g_keyw) == 1, "condvar key %u with 3 waiters", ld(&g_keyw));
|
|
for (int i = 0; i < 3; i++) CHECK(sv(c, 0x1D, ga(&g_keyw), 1, 0, 0) == 0, "SignalProcessWideKey");
|
|
for (int i = 0; i < 3; i++) join(c, h[i]);
|
|
char got[64];
|
|
int ok = order_is(want, 3, got, sizeof got);
|
|
for (int i = 0; i < 3; i++) ok &= j[i].r == 0 && j[i].held;
|
|
printf("condvar wake order: priorities 45 35 40, signalled one at a time, woken %s holding the lock (expected "
|
|
"1 2 0): %s\n", got, ok ? "ok" : "FAIL");
|
|
CHECK(ok, "condvar wake order");
|
|
CHECK(ld(&g_keyw) == 0 && ld(&g_lockw) == 0, "key %u lock %#x after the last waiter", ld(&g_keyw), ld(&g_lockw));
|
|
}
|
|
|
|
// Another thread's user-mode critical section and Signal, run inside the waiter's svc 0x1C right after the lock
|
|
// release: take the lock, read the key (the SDK's Signal skips svc 0x1D on 0), release.
|
|
static volatile int g_seen_key;
|
|
static void cv_signaller(void) {
|
|
uint32_t e = 0;
|
|
if (cas(&g_lockw, &e, 0x7777)) {
|
|
g_seen_key = (int)ld(&g_keyw);
|
|
__atomic_store_n(&g_lockw, 0, __ATOMIC_SEQ_CST);
|
|
}
|
|
kern_test_cv_hook = NULL;
|
|
}
|
|
|
|
static void cv_order_waiter(Cpu *c) {
|
|
LockJob *j = arg_p(c, 0);
|
|
user_lock(c, j->lock, j->tag);
|
|
j->r = sv(c, 0x1C, ga(j->lock), ga(j->key), j->tag, 200000000);
|
|
if (!j->r) user_unlock(c, j->lock, j->tag);
|
|
exit_thread(c);
|
|
}
|
|
|
|
static uint32_t cv_order_case(Cpu *c, int old) {
|
|
LockJob j = {.lock = &g_lockw, .key = &g_keyw};
|
|
g_lockw = 0;
|
|
g_keyw = 0;
|
|
g_seen_key = -1;
|
|
kern_test_cv_old_order = old;
|
|
kern_test_cv_hook = cv_signaller;
|
|
uint32_t h = create(c, a_cv_order_waiter, &j, 44, -2);
|
|
j.tag = h;
|
|
start(c, h);
|
|
while (kern_thread_state(h) != KERN_WAIT_CONDVAR && kern_thread_state(h) != KERN_THREAD_EXITED) sleep_ms(c, 1);
|
|
if (g_seen_key != 0) sv(c, 0x1D, ga(&g_keyw), 1, 0, 0); // what the SDK's Signal does
|
|
join(c, h);
|
|
kern_test_cv_old_order = 0;
|
|
kern_test_cv_hook = NULL;
|
|
return j.r;
|
|
}
|
|
|
|
static void test_cv_key_order(Cpu *c) {
|
|
uint32_t r = cv_order_case(c, 0);
|
|
int seen = g_seen_key;
|
|
printf("condvar key order: key set before the lock release; a signaller between release and wait saw key %d, "
|
|
"waiter result %#x: %s\n", seen, r, seen == 1 && r == 0 ? "ok" : "FAIL");
|
|
CHECK(seen == 1 && r == 0, "condvar key order");
|
|
r = cv_order_case(c, 1);
|
|
seen = g_seen_key;
|
|
int lost = seen == 0 && r == KERN_R_TIMEOUT;
|
|
printf("condvar key order, negative control (the old order, release then set the key): signaller saw key %d, "
|
|
"waiter result %#x: %s\n", seen, r, lost ? "lost wakeup, fails as expected" : "FAIL (the control did not "
|
|
"fail)");
|
|
CHECK(lost, "the old-order negative control did not lose the wakeup");
|
|
}
|
|
|
|
// ---- address arbiter
|
|
typedef struct { int type, value, idx; int64_t timeout; uint32_t r; } ArbJob;
|
|
static volatile uint32_t g_arb __attribute__((aligned(8)));
|
|
|
|
static void arb_waiter(Cpu *c) {
|
|
ArbJob *j = arg_p(c, 0);
|
|
j->r = sv(c, 0x34, ga(&g_arb), (uint64_t)j->type, (uint64_t)(int64_t)j->value, (uint64_t)j->timeout);
|
|
exit_thread(c);
|
|
}
|
|
|
|
static uint32_t arb_spawn(Cpu *c, ArbJob *j) {
|
|
uint32_t h = create(c, a_arb_waiter, j, 44, -2);
|
|
start(c, h);
|
|
wait_state(c, h, KERN_WAIT_ADDRESS);
|
|
return h;
|
|
}
|
|
|
|
static uint32_t sig(Cpu *c, int type, int value, int count) {
|
|
return sv(c, 0x35, ga(&g_arb), (uint64_t)type, (uint64_t)(int64_t)value, (uint64_t)(int64_t)count);
|
|
}
|
|
|
|
static void test_arbiter(Cpu *c) {
|
|
int before = failures;
|
|
uint64_t arb = ga(&g_arb);
|
|
// wait type 0, if less than
|
|
g_arb = 0;
|
|
CHECK(sv(c, 0x34, arb, 0, 0, (uint64_t)-1) == KERN_R_BAD_STATE, "wait type 0 with value not less");
|
|
CHECK(sv(c, 0x34, arb, 0, 1, 0) == KERN_R_TIMEOUT, "wait type 0 with timeout 0");
|
|
ArbJob j0 = {.type = 0, .value = 1, .timeout = -1};
|
|
uint32_t h = arb_spawn(c, &j0);
|
|
CHECK(sig(c, 0, 0, 1) == 0, "signal type 0");
|
|
join(c, h);
|
|
CHECK(j0.r == 0, "wait type 0 woken by signal type 0: %#x", j0.r);
|
|
// wait type 1, decrement if less than; signal type 1, increment if equal
|
|
g_arb = 0;
|
|
ArbJob j1 = {.type = 1, .value = 1, .timeout = -1};
|
|
h = arb_spawn(c, &j1);
|
|
CHECK((int32_t)g_arb == -1, "wait type 1 did not decrement (%d)", (int32_t)g_arb);
|
|
CHECK(sig(c, 1, 0, 1) == KERN_R_BAD_STATE && (int32_t)g_arb == -1 && kern_thread_state(h) == KERN_WAIT_ADDRESS,
|
|
"signal type 1 with a different value");
|
|
CHECK(sig(c, 1, -1, 1) == 0 && g_arb == 0, "signal type 1 did not increment (%d)", (int32_t)g_arb);
|
|
join(c, h);
|
|
CHECK(j1.r == 0, "wait type 1 woken by signal type 1: %#x", j1.r);
|
|
// wait type 2, if equal; signal type 2, modify by the number of waiters if equal
|
|
g_arb = 5;
|
|
CHECK(sv(c, 0x34, arb, 2, 6, (uint64_t)-1) == KERN_R_BAD_STATE, "wait type 2 with a different value");
|
|
ArbJob ja = {.type = 2, .value = 5, .timeout = -1}, jb = ja;
|
|
uint32_t ha = arb_spawn(c, &ja), hb = arb_spawn(c, &jb);
|
|
CHECK(sig(c, 2, 5, 1) == 0 && g_arb == 5, "signal type 2, 2 waiters, count 1: value %d (want 5)", (int32_t)g_arb);
|
|
wait_state(c, ha, KERN_THREAD_EXITED);
|
|
CHECK(kern_thread_state(hb) == KERN_WAIT_ADDRESS, "signal type 2 woke more than one, or not the first");
|
|
CHECK(sig(c, 2, 5, 1) == 0 && g_arb == 4, "signal type 2, 1 waiter, count 1: value %d (want 4)", (int32_t)g_arb);
|
|
join(c, ha);
|
|
join(c, hb);
|
|
CHECK(sig(c, 2, 4, 1) == 0 && g_arb == 5, "signal type 2, no waiter: value %d (want 5)", (int32_t)g_arb);
|
|
CHECK(sig(c, 2, 5, 0) == 0 && g_arb == 6, "signal type 2, no waiter, count 0: value %d (want 6)", (int32_t)g_arb);
|
|
ArbJob jc = {.type = 2, .value = 6, .timeout = -1};
|
|
h = arb_spawn(c, &jc);
|
|
// Waking all (count <= 0) with waiters subtracts 2 (Horizon's KAddressArbiter::SignalAndModifyByWaitingCountIfEqual).
|
|
CHECK(sig(c, 2, 6, -1) == 0 && g_arb == 4, "signal type 2, 1 waiter, count -1: value %d (want 4)", (int32_t)g_arb);
|
|
join(c, h);
|
|
g_arb = 6;
|
|
ArbJob jd = {.type = 2, .value = 6, .timeout = -1}, je = jd;
|
|
uint32_t hd = arb_spawn(c, &jd), he = arb_spawn(c, &je);
|
|
CHECK(sig(c, 2, 6, 0) == 0 && g_arb == 4, "signal type 2, 2 waiters, count 0: value %d (want 4)", (int32_t)g_arb);
|
|
join(c, hd);
|
|
join(c, he);
|
|
CHECK(ja.r == 0 && jb.r == 0 && jc.r == 0 && jd.r == 0 && je.r == 0, "wait type 2 results %#x %#x %#x %#x %#x", ja.r,
|
|
jb.r, jc.r, jd.r, je.r);
|
|
g_arb = 5;
|
|
CHECK(sig(c, 2, 7, 1) == KERN_R_BAD_STATE, "signal type 2 with a different value");
|
|
// timeout, invalid type
|
|
uint64_t t0 = kern_now_ns();
|
|
uint32_t r = sv(c, 0x34, arb, 2, 5, 5000000);
|
|
double ms = ms_since(t0);
|
|
CHECK(r == KERN_R_TIMEOUT && ms >= 5.0, "wait type 2 with a 5 ms timeout: %#x after %.2f ms", r, ms);
|
|
// wait type 3: the whole 64-bit word against x2 (kernel.c case 0x34); an address off 8 bytes is refused
|
|
static volatile uint64_t arb64 = 0x100000005ull;
|
|
uint64_t a64 = ga(&arb64);
|
|
t0 = kern_now_ns();
|
|
r = sv(c, 0x34, a64, 3, 0x100000005ull, 2000000);
|
|
double ms3 = ms_since(t0);
|
|
CHECK(r == KERN_R_TIMEOUT && ms3 >= 2.0, "wait type 3 on an equal word with a 2 ms timeout: %#x after %.2f ms", r, ms3);
|
|
CHECK(sv(c, 0x34, a64, 3, 5, 0) == KERN_R_BAD_STATE, "wait type 3 on a word whose high half differs");
|
|
CHECK(sv(c, 0x34, a64 + 4, 3, 0, 0) == 0xCC01u, "wait type 3 at an address off 8 bytes");
|
|
CHECK(sv(c, 0x34, arb, 4, 0, 0) == KERN_R_INVALID_ENUM && sig(c, 3, 0, 0) == KERN_R_INVALID_ENUM,
|
|
"a wait type past 3 or a signal type past 2");
|
|
printf("address arbiter: wait types 0-3 (less, decrement-less, equal, equal 64-bit), signal types 0-2 (wake, "
|
|
"increment, modify by waiters), 5 ms timeout after %.2f ms: %s\n", ms, failures == before ? "ok" : "FAIL");
|
|
}
|
|
|
|
// ---- events
|
|
// pre: a first wait on h0 with timeout t0 (result r0, index idx0) before the wait on h.
|
|
typedef struct { uint32_t h, r, idx, h0, r0, idx0; int64_t timeout, t0; volatile int delay, go; int pre; } EvJob;
|
|
|
|
static void event_waiter(Cpu *c) {
|
|
EvJob *j = arg_p(c, 0);
|
|
while (j->delay && !j->go) sleep_ms(c, 1);
|
|
if (j->pre) {
|
|
uint32_t h0[1] = {j->h0};
|
|
j->r0 = sv(c, 0x18, 0, ga(h0), 1, (uint64_t)j->t0);
|
|
j->idx0 = (uint32_t)c->x[1];
|
|
}
|
|
uint32_t hs[1] = {j->h};
|
|
j->r = sv(c, 0x18, 0, ga(hs), 1, (uint64_t)j->timeout);
|
|
j->idx = (uint32_t)c->x[1];
|
|
exit_thread(c);
|
|
}
|
|
|
|
static void test_events(Cpu *c) {
|
|
int before = failures;
|
|
CHECK(sv(c, 0x45, 0, 0, 0, 0) == 0, "CreateEvent");
|
|
uint32_t hw = (uint32_t)c->x[1], hr = (uint32_t)c->x[2];
|
|
uint64_t t0 = kern_now_ns();
|
|
uint32_t r = wait1(c, hr, 10000000);
|
|
double ms = ms_since(t0);
|
|
CHECK(r == KERN_R_TIMEOUT && ms >= 10.0, "wait with a 10 ms timeout: %#x after %.2f ms", r, ms);
|
|
CHECK(sv(c, 0x17, hr, 0, 0, 0) == KERN_R_BAD_STATE, "ResetSignal of an unsignalled event");
|
|
|
|
EvJob e = {.h = hr, .timeout = -1};
|
|
uint32_t h = create(c, a_event_waiter, &e, 44, -2);
|
|
start(c, h);
|
|
wait_state(c, h, KERN_WAIT_SYNC);
|
|
CHECK(sv(c, 0x11, hw, 0, 0, 0) == 0, "SignalEvent");
|
|
join(c, h);
|
|
CHECK(e.r == 0 && e.idx == 0, "waiter woken by SignalEvent: %#x index %u", e.r, e.idx);
|
|
|
|
CHECK(sv(c, 0x45, 0, 0, 0, 0) == 0, "CreateEvent");
|
|
uint32_t hw2 = (uint32_t)c->x[1], hr2 = (uint32_t)c->x[2];
|
|
uint32_t two[2] = {hr2, hr};
|
|
CHECK(sv(c, 0x18, 0, ga(two), 2, 0) == 0 && c->x[1] == 1, "wait on two handles, the second signalled");
|
|
CHECK(wait1(c, hw, 0) == KERN_R_BAD_HANDLE, "wait on a writable event handle");
|
|
CHECK(sv(c, 0x12, hr, 0, 0, 0) == 0 && wait1(c, hr, 0) == KERN_R_TIMEOUT, "ClearEvent");
|
|
CHECK(sv(c, 0x11, hr, 0, 0, 0) == KERN_R_BAD_HANDLE, "SignalEvent on the readable end");
|
|
CHECK(sv(c, 0x11, hw, 0, 0, 0) == 0 && sv(c, 0x17, hr, 0, 0, 0) == 0 && sv(c, 0x17, hr, 0, 0, 0) == KERN_R_BAD_STATE,
|
|
"ResetSignal");
|
|
|
|
EvJob f = {.h = hr, .timeout = -1};
|
|
h = create(c, a_event_waiter, &f, 44, -2);
|
|
start(c, h);
|
|
wait_state(c, h, KERN_WAIT_SYNC);
|
|
CHECK(sv(c, 0x19, h, 0, 0, 0) == 0, "CancelSynchronization");
|
|
join(c, h);
|
|
CHECK(f.r == KERN_R_CANCELLED, "cancelled wait: %#x", f.r);
|
|
|
|
EvJob g = {.h = hr, .timeout = -1, .delay = 1};
|
|
h = create(c, a_event_waiter, &g, 44, -2);
|
|
start(c, h);
|
|
CHECK(sv(c, 0x19, h, 0, 0, 0) == 0, "CancelSynchronization before the wait");
|
|
g.go = 1;
|
|
join(c, h);
|
|
CHECK(g.r == KERN_R_CANCELLED, "wait after a cancel: %#x", g.r);
|
|
|
|
// A pending cancel is for the next WaitSynchronization that would block (Horizon's KSynchronizationObject::Wait:
|
|
// signalled objects first, then timeout 0, then the cancel): a poll times out and a wait on a signalled object
|
|
// succeeds, both leaving it pending; the next blocking wait ends cancelled. The blocking waits have a 500 ms
|
|
// timeout, so a cancel used up too early shows as a timeout instead of a hang.
|
|
CHECK(sv(c, 0x11, hw2, 0, 0, 0) == 0, "SignalEvent of the second event");
|
|
EvJob p = {.h = hr, .timeout = 500000000, .delay = 1, .pre = 1, .h0 = hr, .t0 = 0};
|
|
EvJob q = {.h = hr, .timeout = 500000000, .delay = 1, .pre = 1, .h0 = hr2, .t0 = -1};
|
|
EvJob *pq[2] = {&p, &q};
|
|
for (int i = 0; i < 2; i++) {
|
|
h = create(c, a_event_waiter, pq[i], 44, -2);
|
|
start(c, h);
|
|
CHECK(sv(c, 0x19, h, 0, 0, 0) == 0, "CancelSynchronization before the %s", i ? "signalled wait" : "poll");
|
|
pq[i]->go = 1;
|
|
join(c, h);
|
|
}
|
|
CHECK(p.r0 == KERN_R_TIMEOUT, "poll (timeout 0) with a cancel pending: %#x (want timeout)", p.r0);
|
|
CHECK(p.r == KERN_R_CANCELLED, "blocking wait after the poll: %#x (want cancelled)", p.r);
|
|
CHECK(q.r0 == 0 && q.idx0 == 0, "wait on a signalled event with a cancel pending: %#x (want success)", q.r0);
|
|
CHECK(q.r == KERN_R_CANCELLED, "blocking wait after the signalled wait: %#x (want cancelled)", q.r);
|
|
|
|
uint32_t sw = kern_event_new(), sr = kern_event_readable(sw);
|
|
CHECK(wait1(c, sr, 0) == KERN_R_TIMEOUT, "service event before the signal");
|
|
kern_event_signal(sw);
|
|
CHECK(wait1(c, sr, 0) == 0, "service event after kern_event_signal");
|
|
uint32_t handles[] = {sr, sw, hr2, hw2, hr, hw};
|
|
for (unsigned i = 0; i < 6; i++) CHECK(sv(c, 0x16, handles[i], 0, 0, 0) == 0, "CloseHandle %#x", handles[i]);
|
|
CHECK(sv(c, 0x16, hr, 0, 0, 0) == KERN_R_BAD_HANDLE, "CloseHandle twice");
|
|
printf("events: signal, clear, reset, two handles, cancel while waiting and before, a pending cancel kept by a poll "
|
|
"and a signalled wait, service events, 10 ms "
|
|
"timeout after %.2f ms: %s\n", ms, failures == before ? "ok" : "FAIL");
|
|
}
|
|
|
|
// ---- virtual cores: count the threads running guest code at once, and that no two of them share a core number
|
|
#define PROBE_CORES 8 // at least kernel.c NCORE
|
|
static volatile int32_t g_running, g_run_max, g_core_use[PROBE_CORES], g_core_clash;
|
|
|
|
static void core_worker(Cpu *c) {
|
|
for (int i = 0; i < CORE_RUNS; i++) {
|
|
uint32_t core = sv(c, 0x10, 0, 0, 0, 0);
|
|
int known = core < (uint32_t)kern_cores() && core < PROBE_CORES; // a number past the cores is a clash too
|
|
if (!known || plat_inc32(&g_core_use[core]) != 1) plat_inc32(&g_core_clash);
|
|
long n = plat_inc32(&g_running), m;
|
|
while (n > (m = g_run_max) && plat_cas32(&g_run_max, n, m) != m) {}
|
|
uint64_t until = kern_now_ns() + 20000;
|
|
while (kern_now_ns() < until) {}
|
|
plat_dec32(&g_running);
|
|
if (known) plat_dec32(&g_core_use[core]);
|
|
sv(c, 0x0B, i % 8 == 7 ? 50000 : 0, 0, 0, 0);
|
|
}
|
|
exit_thread(c);
|
|
}
|
|
|
|
static void test_cores(Cpu *c) {
|
|
uint32_t h[CORE_WORKERS];
|
|
g_running = g_run_max = g_core_clash = 0;
|
|
kern_core_stats_reset();
|
|
for (int i = 0; i < CORE_WORKERS; i++) {
|
|
h[i] = create(c, a_core_worker, NULL, 44, -2);
|
|
start(c, h[i]);
|
|
}
|
|
for (int i = 0; i < CORE_WORKERS; i++) join(c, h[i]);
|
|
int now, held_max;
|
|
kern_core_stats(&now, &held_max);
|
|
uint32_t cpus = plat_cpu_usable();
|
|
int gate = !cfg_bool("NX2NAVI_FREE_CORES"); // the kernel's own switch
|
|
int ncore = kern_cores(); // as many threads run guest code at once as there are virtual cores, where the host
|
|
// has more CPUs than that
|
|
int ok = g_run_max <= ncore && held_max <= ncore && !g_core_clash && ((int)cpus <= ncore || g_run_max == ncore);
|
|
printf("virtual cores: %d threads x %d runs on %u host CPUs: at most %d running guest code at once, at most %d "
|
|
"core holders (kernel count), %d core-number clashes: %s\n", CORE_WORKERS, CORE_RUNS, cpus, g_run_max,
|
|
held_max, g_core_clash, !gate ? "not checked (NX2NAVI_FREE_CORES is set)" : ok ? "ok" : "FAIL");
|
|
CHECK(ok || !gate, "virtual cores");
|
|
}
|
|
|
|
// ---- pinned threads: each sees the number of the core it is pinned to, also where NX2NAVI_CORES folds that core
|
|
// onto another one (kernel.c core_seen), and no two that run guest code at once see the same number. Run the probe
|
|
// with NX2NAVI_CORES=4 (the Steam Deck profile's value) to check the folded case; with all cores it holds trivially.
|
|
#define PIN_CORES 6 // kernel.c NCORE
|
|
static volatile int32_t g_pin_wrong, g_pin_clash, g_pin_use[PROBE_CORES];
|
|
static uint64_t a_pin_worker;
|
|
|
|
static void pin_worker(Cpu *c) {
|
|
uint32_t own = *(uint32_t *)arg_p(c, 0);
|
|
for (int i = 0; i < CORE_RUNS; i++) {
|
|
uint32_t core = sv(c, 0x10, 0, 0, 0, 0);
|
|
if (core != own) plat_inc32(&g_pin_wrong);
|
|
int known = core < PROBE_CORES;
|
|
if (!known || plat_inc32(&g_pin_use[core]) != 1) plat_inc32(&g_pin_clash);
|
|
uint64_t until = kern_now_ns() + 20000;
|
|
while (kern_now_ns() < until) {}
|
|
if (known) plat_dec32(&g_pin_use[core]);
|
|
sv(c, 0x0B, i % 8 == 7 ? 50000 : 0, 0, 0, 0);
|
|
}
|
|
exit_thread(c);
|
|
}
|
|
|
|
static void test_pinned_cores(Cpu *c) {
|
|
static uint32_t own[2 * PIN_CORES];
|
|
uint32_t h[2 * PIN_CORES];
|
|
g_pin_wrong = g_pin_clash = 0;
|
|
for (int i = 0; i < 2 * PIN_CORES; i++) { // two threads per core, so each core's own threads take turns too
|
|
own[i] = (uint32_t)(i % PIN_CORES);
|
|
h[i] = create(c, a_pin_worker, &own[i], 44, i % PIN_CORES);
|
|
start(c, h[i]);
|
|
}
|
|
for (int i = 0; i < 2 * PIN_CORES; i++) join(c, h[i]);
|
|
int gate = !cfg_bool("NX2NAVI_FREE_CORES");
|
|
int ok = !g_pin_wrong && !g_pin_clash;
|
|
printf("pinned cores: %d threads on %d cores (%d virtual cores run threads) x %d runs: %d saw another core's "
|
|
"number, %d core-number clashes: %s\n", 2 * PIN_CORES, PIN_CORES, kern_cores(), CORE_RUNS, g_pin_wrong,
|
|
g_pin_clash, !gate ? "not checked (NX2NAVI_FREE_CORES is set)" : ok ? "ok" : "FAIL");
|
|
CHECK(ok || !gate, "pinned cores");
|
|
}
|
|
|
|
// ---- fibers: two fibers switch to each other; each checks its callee-saved registers, sp and tpidr survive
|
|
typedef struct { uint64_t self, other; int id; } FiberJob;
|
|
static volatile int32_t g_switches, g_runs[3];
|
|
static volatile int g_last_fiber;
|
|
static long g_fiber_bad, g_fiber_turns;
|
|
|
|
static void set_regs(Cpu *c, uint64_t seed, uint64_t *save, __uint128_t *vsave) {
|
|
for (int r = 19; r <= 28; r++) save[r - 19] = c->x[r] = seed * 0x9E3779B97F4A7C15ull + (uint64_t)r;
|
|
for (int r = 8; r <= 15; r++)
|
|
vsave[r - 8] = c->v[r] = ((__uint128_t)(seed ^ 0xABCDull) << 64) | (seed * 31u + (uint64_t)r);
|
|
}
|
|
|
|
static int same_regs(const Cpu *c, const uint64_t *save, const __uint128_t *vsave) {
|
|
for (int r = 19; r <= 28; r++)
|
|
if (c->x[r] != save[r - 19]) return 0;
|
|
for (int r = 8; r <= 15; r++)
|
|
if (c->v[r] != vsave[r - 8]) return 0;
|
|
return 1;
|
|
}
|
|
|
|
static void fiber_fn(Cpu *c) {
|
|
FiberJob *j = arg_p(c, 0);
|
|
uint64_t save[10];
|
|
__uint128_t vsave[8];
|
|
for (;;) {
|
|
long n = g_switches;
|
|
if (n >= FIBER_SWITCHES) {
|
|
ret_x(c, 0); // back to the thread
|
|
return;
|
|
}
|
|
g_switches = n + 1;
|
|
g_runs[j->id]++;
|
|
set_regs(c, (uint64_t)n * 2 + (uint64_t)j->id, save, vsave);
|
|
uint64_t sp = c->sp, tp = c->tpidr, tlr = c->tpidrro;
|
|
g_last_fiber = j->id;
|
|
rt_call(c, a_fib_switch, 1, j->other);
|
|
if (g_last_fiber != 3 - j->id) g_fiber_turns++; // the other fiber did not run in between
|
|
if (!same_regs(c, save, vsave) || c->sp != sp || c->tpidr != tp || c->tpidrro != tlr) g_fiber_bad++;
|
|
if (rt_call(c, a_fib_cur, 0) != j->self) g_fiber_bad++;
|
|
}
|
|
}
|
|
|
|
static void test_fibers(Cpu *c) {
|
|
uint64_t fa = ga(mem_kernel_alloc(0x100, 16)), fb = ga(mem_kernel_alloc(0x100, 16));
|
|
uint64_t sa = ga(mem_kernel_alloc(STACK, 16)), sb = ga(mem_kernel_alloc(STACK, 16));
|
|
FiberJob ja = {fa, fb, 1}, jb = {fb, fa, 2};
|
|
rt_call(c, a_fib_init, 6, fa, a_fiber_fn, ga(&ja), sa, (uint64_t)STACK, (uint64_t)0);
|
|
rt_call(c, a_fib_init, 6, fb, a_fiber_fn, ga(&jb), sb, (uint64_t)STACK, (uint64_t)0);
|
|
g_switches = g_runs[1] = g_runs[2] = 0;
|
|
g_fiber_bad = g_fiber_turns = 0;
|
|
uint64_t save[10];
|
|
__uint128_t vsave[8];
|
|
set_regs(c, 0x5555, save, vsave);
|
|
uint64_t sp = c->sp, tp = c->tpidr;
|
|
uint64_t t0 = kern_now_ns();
|
|
rt_call(c, a_fib_switch, 1, fa);
|
|
double ms = ms_since(t0);
|
|
int base_ok = same_regs(c, save, vsave) && c->sp == sp && c->tpidr == tp && rt_call(c, a_fib_cur, 0) == 0;
|
|
rt_call(c, a_fib_fin, 1, fa);
|
|
rt_call(c, a_fib_fin, 1, fb);
|
|
int ok = g_switches == FIBER_SWITCHES && g_runs[1] == FIBER_SWITCHES / 2 && g_runs[2] == FIBER_SWITCHES / 2 &&
|
|
!g_fiber_turns && !g_fiber_bad && base_ok;
|
|
printf("fibers: %d switches (fiber 1 %d, fiber 2 %d, %ld out of turn) in %.1f ms (%.3f us each), %ld register "
|
|
"mismatches, thread context %s: %s\n", g_switches, g_runs[1], g_runs[2], g_fiber_turns, ms,
|
|
ms * 1000.0 / (double)(g_switches ? g_switches : 1), g_fiber_bad, base_ok ? "preserved" : "CHANGED",
|
|
ok ? "ok" : "FAIL");
|
|
CHECK(ok, "fiber ping-pong");
|
|
}
|
|
|
|
// ---- ExitThread unwinding: from three nested host calls deep, and from a fiber
|
|
typedef struct { int stage, depth, bad; uint64_t fiber, stack; } ExitJob;
|
|
|
|
static void exit_nested(Cpu *c) {
|
|
ExitJob *j = arg_p(c, 0);
|
|
if (++j->depth < 3) {
|
|
rt_call(c, a_exit_nested, 1, ga(j));
|
|
j->bad = 1;
|
|
return;
|
|
}
|
|
j->stage = 2;
|
|
sv(c, 0x0A, 0, 0, 0, 0);
|
|
j->bad = 2;
|
|
}
|
|
|
|
static void exit_entry(Cpu *c) {
|
|
ExitJob *j = arg_p(c, 0);
|
|
j->stage = 1;
|
|
rt_call(c, a_exit_nested, 1, ga(j));
|
|
j->bad = 3;
|
|
exit_thread(c);
|
|
}
|
|
|
|
static void fiber_exit_fn(Cpu *c) {
|
|
ExitJob *j = arg_p(c, 0);
|
|
j->stage = 3;
|
|
sv(c, 0x0A, 0, 0, 0, 0);
|
|
j->bad = 4;
|
|
}
|
|
|
|
static void fiber_exit_thread(Cpu *c) {
|
|
ExitJob *j = arg_p(c, 0);
|
|
rt_call(c, a_fib_init, 6, j->fiber, a_fiber_exit_fn, ga(j), j->stack, (uint64_t)STACK, (uint64_t)0);
|
|
rt_call(c, a_fib_switch, 1, j->fiber);
|
|
j->bad = 5;
|
|
exit_thread(c);
|
|
}
|
|
|
|
static void test_exit(Cpu *c) {
|
|
ExitJob a = {0};
|
|
uint32_t h = create(c, a_exit_entry, &a, 44, -2);
|
|
start(c, h);
|
|
join(c, h);
|
|
CHECK(wait1(c, h, 0) == KERN_R_BAD_HANDLE, "wait on a closed thread handle");
|
|
ExitJob b = {.fiber = ga(mem_kernel_alloc(0x100, 16)), .stack = ga(mem_kernel_alloc(STACK, 16))};
|
|
h = create(c, a_fiber_exit_thread, &b, 44, -2);
|
|
start(c, h);
|
|
join(c, h);
|
|
rt_call(c, a_fib_fin, 1, b.fiber);
|
|
int ok = a.stage == 2 && a.depth == 3 && !a.bad && b.stage == 3 && !b.bad;
|
|
printf("ExitThread: unwound from %d nested host calls (stage %d) and from a fiber (stage %d), nothing ran after "
|
|
"it: %s\n", a.depth, a.stage, b.stage, ok ? "ok" : "FAIL");
|
|
CHECK(ok, "ExitThread unwinding (bad %d %d)", a.bad, b.bad);
|
|
}
|
|
|
|
// ---- lifted code: main's PLT stub for nn::os::GetSystemTick -> lifted sdk -> rt_ticks
|
|
typedef struct { uint64_t first, last; long bad; } TickJob;
|
|
|
|
static void tick_worker(Cpu *c) {
|
|
TickJob *j = arg_p(c, 0);
|
|
uint64_t prev = 0;
|
|
for (int i = 0; i < TICK_CALLS; i++) {
|
|
uint64_t t = rt_call(c, tick_stub, 0);
|
|
if (!t || t < prev) j->bad++;
|
|
if (!i) j->first = t;
|
|
prev = t;
|
|
}
|
|
j->last = prev;
|
|
exit_thread(c);
|
|
}
|
|
|
|
static void test_lifted_ticks(Cpu *c) {
|
|
TickJob j[TICK_THREADS] = {0};
|
|
uint32_t h[TICK_THREADS];
|
|
uint64_t calls0 = hostfn_calls(), before = rt_ticks();
|
|
for (int i = 0; i < TICK_THREADS; i++) h[i] = create(c, a_tick_worker, &j[i], 44, -2);
|
|
for (int i = 0; i < TICK_THREADS; i++) start(c, h[i]);
|
|
for (int i = 0; i < TICK_THREADS; i++) join(c, h[i]);
|
|
uint64_t after = rt_ticks(), calls = hostfn_calls() - calls0;
|
|
char stub[128], def[128];
|
|
printf("lifted GetSystemTick: %d guest threads x %d calls of %s -> %s:\n", TICK_THREADS, TICK_CALLS,
|
|
ldr_describe(tick_stub, stub, sizeof stub), ldr_describe(ldr_symbol(TICK_SYMBOL), def, sizeof def));
|
|
int ok = 1;
|
|
for (int i = 0; i < TICK_THREADS; i++) {
|
|
int in = j[i].first >= before && j[i].last <= after && j[i].last > j[i].first;
|
|
printf(" thread %d: ticks %llu .. %llu, %ld out of order, inside the host's rt_ticks window: %s\n", i,
|
|
(unsigned long long)j[i].first, (unsigned long long)j[i].last, j[i].bad, in ? "yes" : "NO");
|
|
ok &= !j[i].bad && in;
|
|
}
|
|
// Host functions entered by dispatch meanwhile: only the four thread entries, so no call reached a host override.
|
|
int lifted = calls == TICK_THREADS;
|
|
printf(" monotonic per thread: %s; host-function calls meanwhile %llu (the %d thread entries): %s\n",
|
|
ok ? "ok" : "FAIL", (unsigned long long)calls, TICK_THREADS, lifted ? "all ticks came from lifted code"
|
|
: "FAIL");
|
|
CHECK(ok && lifted, "lifted GetSystemTick");
|
|
}
|
|
|
|
// svc 0x07 ExitProcess: the path up to the final ExitProcess (logged, pipeline cache saved, logs flushed) runs with the
|
|
// test hook in its place, which must see exit code 0; the SVC then returns to the probe.
|
|
static volatile int32_t exit_calls;
|
|
static volatile unsigned exit_code = 99;
|
|
static void exit_hook(unsigned code) { exit_code = code, exit_calls++; }
|
|
static void test_exit_process(Cpu *c) {
|
|
svc_test_exit_hook = exit_hook;
|
|
sv(c, 0x07, 0, 0, 0, 0);
|
|
svc_test_exit_hook = NULL;
|
|
CHECK(exit_calls == 1 && exit_code == 0, "ExitProcess reached the exit %d times with code %u", exit_calls, exit_code);
|
|
printf("ExitProcess: exit path reached once with code %u\n", exit_code);
|
|
}
|
|
|
|
static void probe_main(Cpu *c) {
|
|
Cpu *again;
|
|
uint32_t me = kern_main_thread(&again);
|
|
uint64_t mb, ms;
|
|
kern_main_stack(&mb, &ms);
|
|
printf("main thread %#x: prio 44 core 0, guest stack %#llx+%#llx, sp %#llx, TLR %#llx (+0x1F8 -> %#llx)\n", me,
|
|
(unsigned long long)mb, (unsigned long long)ms, (unsigned long long)c->sp, (unsigned long long)c->tpidrro,
|
|
(unsigned long long)LD64(c->tpidrro + 0x1F8));
|
|
CHECK(again == c && c->sp == mb + ms && LD64(c->tpidrro + 0x1F8) == c->tpidrro + 0x200, "main thread set-up");
|
|
CHECK(sv(c, 0x25, 0, KERN_H_CURRENT_THREAD, 0, 0) == 0 && c->x[1] == 0x51, "GetThreadId(current) %#llx",
|
|
(unsigned long long)c->x[1]);
|
|
test_lock(c, me);
|
|
test_cv_wake_order(c);
|
|
test_cv_key_order(c);
|
|
test_arbiter(c);
|
|
test_events(c);
|
|
test_cores(c);
|
|
test_pinned_cores(c);
|
|
test_fibers(c);
|
|
test_exit(c);
|
|
test_lifted_ticks(c);
|
|
test_exit_process(c);
|
|
}
|
|
|
|
// ---- platform layer (platform/win32.c): a thread handing values over a condition variable, and plat_sleep_ns(1 ms)
|
|
// overshoot (median of 21 under 0.5 ms; frame pacing depends on it).
|
|
static PlatMutex g_pm = PLAT_MUTEX_INIT;
|
|
static PlatCond g_pc = PLAT_COND_INIT;
|
|
static int g_pv;
|
|
|
|
static void plat_worker(void *arg) {
|
|
plat_mutex_lock(&g_pm);
|
|
g_pv = (int)(intptr_t)arg;
|
|
plat_cond_broadcast(&g_pc);
|
|
while (g_pv != 2) plat_cond_wait(&g_pc, &g_pm);
|
|
g_pv = 3;
|
|
plat_mutex_unlock(&g_pm);
|
|
}
|
|
|
|
static int cmp_u64(const void *a, const void *b) {
|
|
uint64_t x = *(const uint64_t *)a, y = *(const uint64_t *)b;
|
|
return x < y ? -1 : x > y;
|
|
}
|
|
|
|
static void test_platform(void) {
|
|
PlatThread *t = plat_thread_new(plat_worker, (void *)(intptr_t)1, "probe-kernel plat");
|
|
CHECK(t, "plat_thread_new");
|
|
if (t) {
|
|
plat_mutex_lock(&g_pm);
|
|
int timed_out = 0;
|
|
while (g_pv != 1 && !timed_out) timed_out = !plat_cond_wait_ns(&g_pc, &g_pm, 5000000000ull);
|
|
CHECK(g_pv == 1, "plat thread did not start (%d)", g_pv);
|
|
g_pv = 2;
|
|
plat_cond_broadcast(&g_pc);
|
|
plat_mutex_unlock(&g_pm);
|
|
plat_thread_join(t);
|
|
CHECK(g_pv == 3, "plat thread did not finish (%d)", g_pv);
|
|
}
|
|
PlatMutex m = PLAT_MUTEX_INIT;
|
|
PlatCond c = PLAT_COND_INIT;
|
|
plat_mutex_lock(&m);
|
|
uint64_t t0 = plat_now_ns();
|
|
CHECK(plat_cond_wait_ns(&c, &m, 2000000) == 0 && plat_now_ns() - t0 >= 1000000, "plat_cond_wait_ns timeout");
|
|
plat_mutex_unlock(&m);
|
|
CHECK(plat_mutex_trylock(&m), "plat_mutex_trylock");
|
|
plat_mutex_unlock(&m);
|
|
|
|
enum { N = 21 };
|
|
uint64_t over[N];
|
|
for (int i = 0; i < N; i++) {
|
|
uint64_t a = plat_now_ns();
|
|
plat_sleep_ns(1000000);
|
|
uint64_t d = plat_now_ns() - a;
|
|
CHECK(d >= 1000000, "plat_sleep_ns(1 ms) returned after %llu ns", (unsigned long long)d);
|
|
over[i] = d > 1000000 ? d - 1000000 : 0;
|
|
}
|
|
qsort(over, N, sizeof *over, cmp_u64);
|
|
printf(" plat_sleep_ns(1 ms) overshoot: median %.3f ms, max %.3f ms\n", over[N / 2] / 1e6, over[N - 1] / 1e6);
|
|
CHECK(over[N / 2] < 500000, "plat_sleep_ns(1 ms) median overshoot %.3f ms (want < 0.5)", over[N / 2] / 1e6);
|
|
}
|
|
|
|
int probe_kernel(void) {
|
|
a_lock_waiter = hostfn_register("probe-kernel lock waiter", lock_waiter);
|
|
a_cv_waiter = hostfn_register("probe-kernel condvar waiter", cv_waiter);
|
|
a_cv_order_waiter = hostfn_register("probe-kernel condvar order waiter", cv_order_waiter);
|
|
a_arb_waiter = hostfn_register("probe-kernel arbiter waiter", arb_waiter);
|
|
a_event_waiter = hostfn_register("probe-kernel event waiter", event_waiter);
|
|
a_core_worker = hostfn_register("probe-kernel core worker", core_worker);
|
|
a_pin_worker = hostfn_register("probe-kernel pin worker", pin_worker);
|
|
a_exit_entry = hostfn_register("probe-kernel exit entry", exit_entry);
|
|
a_exit_nested = hostfn_register("probe-kernel exit nested", exit_nested);
|
|
a_fiber_fn = hostfn_register("probe-kernel fiber", fiber_fn);
|
|
a_fiber_exit_thread = hostfn_register("probe-kernel fiber exit thread", fiber_exit_thread);
|
|
a_fiber_exit_fn = hostfn_register("probe-kernel fiber exit", fiber_exit_fn);
|
|
a_tick_worker = hostfn_register("probe-kernel tick worker", tick_worker);
|
|
a_fib_init = hostfn_register(KERN_SYM_INITIALIZE_FIBER, kern_fiber_initialize); // the loader's addresses
|
|
a_fib_fin = hostfn_register(KERN_SYM_FINALIZE_FIBER, kern_fiber_finalize);
|
|
a_fib_switch = hostfn_register(KERN_SYM_SWITCH_TO_FIBER, kern_fiber_switch);
|
|
a_fib_cur = hostfn_register(KERN_SYM_GET_CURRENT_FIBER, kern_fiber_current);
|
|
ldr_load(rt_config()->exefs);
|
|
tick_stub = ldr_plt_stub("main", TICK_SYMBOL);
|
|
if (!tick_stub || !ldr_symbol(TICK_SYMBOL)) {
|
|
printf("probe-kernel: FAILED (no PLT stub for %s in main)\n", TICK_SYMBOL);
|
|
return 1;
|
|
}
|
|
test_platform();
|
|
kern_run_on_guest_main(probe_main);
|
|
RtStats s = rt_stats();
|
|
printf("stats: %llu SVCs, %llu threads, %llu host calls\n", (unsigned long long)s.svcs,
|
|
(unsigned long long)s.threads, (unsigned long long)s.host_calls);
|
|
if (failures) {
|
|
printf("probe-kernel: FAILED (%d failures)\n", failures);
|
|
return 1;
|
|
}
|
|
printf("probe-kernel: passed\n");
|
|
return 0;
|
|
}
|