Files
totk-pc-recomp/runtime/loader.c
T
shenjackanddeepseek-v4.1-flash-1017 a7b988b417 Name the 1.1.0 update 1.1.0 instead of 1.1.1
The update this port builds for has version value 0x20000, which is Tears of the Kingdom 1.1.0, not 1.1.1: the upstream expected-v1.1.2.json carries 0x40000 and the other expected files step 0x10000 per release, so 0x20000 is 1.1.0 and 0x30000 is 1.1.1. Pull request #220 introduced the wrong label in expected-v1.1.1.json.

Expected hashes, patch sites, boot constants and the RomFS layout are unchanged; only names change:
- tools/data/expected-v1.1.1.json -> expected-v1.1.0.json, display_version 1.1.0
- recomp/patches-v111.txt -> patches-v110.txt
- installer/layout.py SUPPORTED and tools/build_game.py DISPLAY_VERSIONS
- make_romfs_patches.py --version choices and its COMMON table key (the RomFS archive name stays Common.Product.110)
- runtime RT_V111 -> RT_V110, the inits_111 and expect_111 tables, the probe's census folder v110
- README, docs and the installer texts

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 21:08:13 +08:00

738 lines
33 KiB
C

// Adapted from LAS Recomp runtime/loader.c @ da35b83 (MIT, Copyright (c) 2026 Peiikai) and MacNativeGames
// games/totk/runtime/loader.c and scripts/prepare_runtime.py @ b75be8c (MIT, Copyright (c) 2026 MacNativeGames
// contributors).
// NSO loading at the fixed layout bases, dynamic linking with host overrides, the nnMain identity check, sealing,
// and --probe-loader.
//
// The lifted C was generated from the unrelocated words of .text and .rodata and embeds absolute guest addresses, so
// each module must sit at its layout base and relocations may only write the data segment.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "host.h"
#include "layout.h"
#include "platform/platform.h"
// hostfn.c, for the loader only.
uint64_t hostfn_register_observer(const char *name, GuestFn fn, uint64_t original);
enum { DT_NULL = 0, DT_PLTRELSZ = 2, DT_HASH = 4, DT_STRTAB = 5, DT_SYMTAB = 6, DT_RELA = 7, DT_RELASZ = 8,
DT_INIT = 12, DT_FINI = 13, DT_JMPREL = 23, DT_INIT_ARRAY = 25, DT_FINI_ARRAY = 26, DT_INIT_ARRAYSZ = 27,
DT_FINI_ARRAYSZ = 28, DT_RELRSZ = 0x23, DT_RELR = 0x24, DT_GNU_HASH = 0x6ffffef5 };
enum { R_ABS64 = 257, R_GLOB_DAT = 1025, R_JUMP_SLOT = 1026, R_RELATIVE = 1027 };
enum { K_RELATIVE, K_ABS64, K_GLOB_DAT, K_JUMP_SLOT, K_COUNT };
enum { STB_WEAK = 2, STT_FUNC = 2 };
typedef struct { uint32_t name; uint8_t info, other; uint16_t shndx; uint64_t value, size; } Elf64Sym;
typedef struct { uint64_t off, info; int64_t addend; } Elf64Rela;
// How a symbol of a relocation was bound.
enum { RES_UNSET, RES_GUEST, RES_OVERRIDE, RES_OBSERVER, RES_WEAK, RES_STUB };
typedef struct { uint64_t value; uint8_t kind; } Res;
typedef struct {
RtLoaded pub;
uint32_t seg_off[3], seg_size[3]; // text, rodata, data (data without bss)
const Elf64Sym *syms;
const char *strtab;
uint32_t nsyms;
const Elf64Rela *rela, *jmprel;
uint64_t nrela, njmprel;
const uint64_t *relr; // DT_RELR (1.4.3): packed RELATIVE relocations
uint64_t nrelr, relr_count;
uint32_t *hash, hash_mask; // defined symbols by name
Res *res_plain, *res_jump; // per symbol; JUMP_SLOT may bind an observer
uint32_t *funcs, nfuncs; // defined functions sorted by value (ldr_describe)
uint64_t *plt_slot, *plt_stub, nplt; // PLT stubs sorted by GOT slot (ldr_plt_stub), built on first use
// statistics
uint64_t kinds[K_COUNT], sym_import, sym_own;
uint32_t imports, import_to[MOD_COUNT], weak_unresolved, strong_unresolved;
uint32_t init_off;
} Mod;
static Mod mods[MOD_COUNT];
static int nmods;
static uint64_t identity_refs, stubs;
static int *ovr_used, *obs_used; // per linked table entry, plus probe overrides counted separately
static PlatMutex plt_lock = PLAT_MUTEX_INIT;
int ldr_count(void) { return nmods; }
const RtLoaded *ldr_module(int i) { return i >= 0 && i < nmods ? &mods[i].pub : NULL; }
static uint32_t rd32(uint64_t a) { return LD32(a); }
// ---- files
static uint8_t *read_file(const char *path, size_t *len) {
uint64_t size = 0;
uint8_t *p = plat_file_read(path, &size);
if (p) *len = (size_t)size;
return p;
}
static void hex32(const uint8_t d[32], char out[65]) {
for (int i = 0; i < 32; i++) snprintf(out + 2 * i, 3, "%02x", d[i]);
}
uint8_t *ldr_read_verified(const char *dir, const char *file, size_t *len) {
const char *want = NULL;
for (int i = 0; i < rt_exefs_sha256_count; i++)
if (!strcmp(rt_exefs_sha256[i].file, file)) want = rt_exefs_sha256[i].sha256;
if (!want) rt_fatal("loader: no expected SHA-256 for %s in modules.c", file);
char path[1024];
snprintf(path, sizeof path, "%s/%s", dir, file);
uint8_t *p = read_file(path, len);
if (!p) rt_fatal("loader: cannot read %s (extract the game first: tools/extract.py)", path);
uint8_t d[32];
char hex[65];
rt_sha256(p, *len, d);
hex32(d, hex);
if (strcmp(hex, want)) rt_fatal("loader: %s has SHA-256 %s, expected %s", path, hex, want);
return p;
}
// ---- NSO
static size_t lz4_decompress(const uint8_t *src, size_t srclen, uint8_t *dst, size_t dstlen) {
const uint8_t *s = src, *se = src + srclen;
uint8_t *d = dst, *de = dst + dstlen;
while (s < se) {
uint8_t tok = *s++;
size_t lit = tok >> 4;
if (lit == 15) { uint8_t b; do { if (s >= se) return 0; b = *s++; lit += b; } while (b == 255); }
if ((size_t)(de - d) < lit || (size_t)(se - s) < lit) return 0;
memcpy(d, s, lit);
d += lit;
s += lit;
if (s >= se) break;
if (se - s < 2) return 0;
size_t off = s[0] | (s[1] << 8);
s += 2;
size_t ml = tok & 15;
if (ml == 15) { uint8_t b; do { if (s >= se) return 0; b = *s++; ml += b; } while (b == 255); }
ml += 4;
if (!off || off > (size_t)(d - dst) || (size_t)(de - d) < ml) return 0;
uint8_t *m = d - off;
while (ml--) *d++ = *m++;
}
return (size_t)(d - dst);
}
typedef struct { uint32_t file_off, mem_off, size, csize; int compressed; } Seg;
static uint64_t nso_segments(const uint8_t *raw, size_t len, const char *name, Seg seg[3]) {
if (len < 0x100 || memcmp(raw, "NSO0", 4)) rt_fatal("loader: %s is not an NSO", name);
uint32_t flags = *(const uint32_t *)(raw + 0xC);
for (int i = 0; i < 3; i++) {
memcpy(&seg[i], raw + 0x10 + i * 0x10, 12);
seg[i].csize = *(const uint32_t *)(raw + 0x60 + i * 4);
seg[i].compressed = (flags >> i) & 1;
if ((uint64_t)seg[i].file_off + (seg[i].compressed ? seg[i].csize : seg[i].size) > len)
rt_fatal("loader: %s segment %d lies outside the file", name, i);
}
uint32_t bss = *(const uint32_t *)(raw + 0x3C);
return ((uint64_t)seg[2].mem_off + seg[2].size + bss + 0xFFF) & ~0xFFFull;
}
static void nso_segment(const uint8_t *raw, const Seg *s, uint8_t *dst, const char *name, int i) {
if (!s->compressed) {
memcpy(dst, raw + s->file_off, s->size);
} else if (lz4_decompress(raw + s->file_off, s->csize, dst, s->size) != s->size) {
rt_fatal("loader: %s segment %d: LZ4 data is corrupt", name, i);
}
}
// ---- symbols
static const char *sym_name(const Mod *m, uint32_t i) { return m->strtab + m->syms[i].name; }
static int sym_defined(const Mod *m, uint32_t i) { return m->syms[i].shndx != 0; }
static uint32_t fnv(const char *s) {
uint32_t h = 2166136261u;
for (; *s; s++) h = (h ^ (uint8_t)*s) * 16777619u;
return h;
}
static void make_hash(Mod *m) {
uint32_t cap = 16;
while (cap < m->nsyms * 2) cap <<= 1;
m->hash = calloc(cap, sizeof *m->hash);
if (!m->hash) rt_fatal("loader: symbol hash allocation");
m->hash_mask = cap - 1;
for (uint32_t i = 1; i < m->nsyms; i++) {
if (!sym_defined(m, i) || !*sym_name(m, i)) continue;
const char *s = sym_name(m, i);
uint32_t p = fnv(s) & m->hash_mask;
while (m->hash[p] && strcmp(s, sym_name(m, m->hash[p]))) p = (p + 1) & m->hash_mask;
if (!m->hash[p]) m->hash[p] = i;
}
}
static uint64_t module_symbol(const Mod *m, const char *name) {
for (uint32_t p = fnv(name) & m->hash_mask; m->hash[p]; p = (p + 1) & m->hash_mask) {
uint32_t i = m->hash[p];
if (!strcmp(name, sym_name(m, i))) return m->pub.base + m->syms[i].value;
}
return 0;
}
// The defined symbol in load order (main, subsdk0, sdk); *which is its module index.
static uint64_t find_symbol(const char *name, int *which) {
for (int i = 0; i < nmods; i++) {
uint64_t a = module_symbol(&mods[i], name);
if (a) {
if (which) *which = i;
return a;
}
}
return 0;
}
uint64_t ldr_symbol(const char *name) { return find_symbol(name, NULL); }
static const Mod *sort_mod;
static int by_value(const void *a, const void *b) {
uint64_t x = sort_mod->syms[*(const uint32_t *)a].value, y = sort_mod->syms[*(const uint32_t *)b].value;
return x < y ? -1 : x > y;
}
static void make_funcs(Mod *m) {
m->funcs = malloc((m->nsyms ? m->nsyms : 1) * sizeof *m->funcs);
if (!m->funcs) rt_fatal("loader: function table allocation");
for (uint32_t i = 1; i < m->nsyms; i++)
if (sym_defined(m, i) && (m->syms[i].info & 0xF) == STT_FUNC && *sym_name(m, i)) m->funcs[m->nfuncs++] = i;
sort_mod = m;
qsort(m->funcs, m->nfuncs, sizeof *m->funcs, by_value);
}
// ---- loading
static void parse_dynamic(Mod *m) {
uint64_t base = m->pub.base;
uint64_t mod0 = base + rd32(base + 4);
if (memcmp((const void *)(uintptr_t)mod0, "MOD0", 4)) rt_fatal("loader: %s has no MOD0 header", m->pub.name);
uint64_t dyn = mod0 + (int64_t)(int32_t)rd32(mod0 + 4);
uint64_t rela = 0, relasz = 0, jmprel = 0, pltsz = 0, hash = 0, gnu_hash = 0, relr = 0, relrsz = 0;
for (const uint64_t *d = (const uint64_t *)(uintptr_t)dyn; d[0] != DT_NULL; d += 2) {
switch (d[0]) {
case DT_SYMTAB: m->syms = (const Elf64Sym *)(uintptr_t)(base + d[1]); break;
case DT_STRTAB: m->strtab = (const char *)(uintptr_t)(base + d[1]); break;
case DT_HASH: hash = base + d[1]; break;
case DT_GNU_HASH: gnu_hash = base + d[1]; break;
case DT_RELR: relr = base + d[1]; break;
case DT_RELRSZ: relrsz = d[1]; break;
case DT_RELA: rela = base + d[1]; break;
case DT_RELASZ: relasz = d[1]; break;
case DT_JMPREL: jmprel = base + d[1]; break;
case DT_PLTRELSZ: pltsz = d[1]; break;
case DT_INIT: m->pub.init = base + d[1]; m->init_off = (uint32_t)d[1]; break;
case DT_INIT_ARRAY: m->pub.init_array = base + d[1]; break;
case DT_INIT_ARRAYSZ: m->pub.init_array_size = d[1]; break;
}
}
if (!m->syms || !m->strtab || (!hash && !gnu_hash))
rt_fatal("loader: %s lacks DT_SYMTAB, DT_STRTAB or a symbol hash", m->pub.name);
if (hash) {
m->nsyms = rd32(hash + 4); // nchain
} else {
// DT_GNU_HASH only (1.4.3 sdk, subsdk0): one past the last symbol the highest bucket's chain reaches.
uint32_t nbuckets = rd32(gnu_hash), symoffset = rd32(gnu_hash + 4), bloom = rd32(gnu_hash + 8);
uint64_t buckets = gnu_hash + 16 + 8ull * bloom, chains = buckets + 4ull * nbuckets;
uint32_t last = 0;
for (uint32_t i = 0; i < nbuckets; i++)
if (rd32(buckets + 4ull * i) > last) last = rd32(buckets + 4ull * i);
if (last < symoffset) {
m->nsyms = symoffset;
} else {
while (!(rd32(chains + 4ull * (last - symoffset)) & 1)) last++;
m->nsyms = last + 1;
}
}
m->relr = (const uint64_t *)(uintptr_t)relr;
m->nrelr = relrsz / 8;
m->rela = (const Elf64Rela *)(uintptr_t)rela;
m->nrela = relasz / sizeof(Elf64Rela);
m->jmprel = (const Elf64Rela *)(uintptr_t)jmprel;
m->njmprel = pltsz / sizeof(Elf64Rela);
}
// The constructor state flag DT_INIT checks: ADRP xN at DT_INIT+16, LDR wM, [xN, #imm] at +20 or +24
// (MNG scripts/prepare_runtime.py).
static uint64_t decode_ctor_flag(const Mod *m) {
uint64_t init = m->pub.init;
if (!init) return 0;
uint32_t a = rd32(init + 16);
if ((a & 0x9f000000u) != 0x90000000u) return 0;
uint32_t reg = a & 31;
for (int at = 20; at <= 24; at += 4) {
uint32_t w = rd32(init + at);
if ((w & 0xffc00000u) != 0xb9400000u || ((w >> 5) & 31) != reg) continue;
int64_t imm = (int64_t)(((a >> 29) & 3) | (((a >> 5) & 0x7ffff) << 2));
imm = (imm ^ (1 << 20)) - (1 << 20);
uint64_t flag = ((init + 16) & ~0xFFFull) + (uint64_t)(imm << 12) + ((w >> 10) & 0xfff) * 4;
uint64_t data = m->pub.base + m->pub.data_off;
return flag >= data && flag < m->pub.base + m->pub.size ? flag : 0;
}
return 0;
}
static void load_module(Mod *m, const char *dir, const RtLayoutEntry *lay) {
size_t len;
uint8_t *raw = ldr_read_verified(dir, lay->name, &len);
Seg seg[3];
uint64_t size = nso_segments(raw, len, lay->name, seg);
if (size != lay->size)
rt_fatal("loader: %s image is %#llx bytes, layout.h says %#llx: generated C and game files differ", lay->name,
(unsigned long long)size, (unsigned long long)lay->size);
if (!plat_mem_commit(lay->base, size))
rt_fatal("loader: cannot commit %s at %#llx", lay->name, (unsigned long long)lay->base);
for (int i = 0; i < 3; i++) {
if ((uint64_t)seg[i].mem_off + seg[i].size > size) rt_fatal("loader: %s segment %d exceeds the image", lay->name, i);
nso_segment(raw, &seg[i], (uint8_t *)(uintptr_t)(lay->base + seg[i].mem_off), lay->name, i);
m->seg_off[i] = seg[i].mem_off;
m->seg_size[i] = seg[i].size;
}
free(raw);
m->pub.name = lay->name;
m->pub.base = lay->base;
m->pub.size = size;
m->pub.text_end = seg[0].mem_off + seg[0].size;
m->pub.data_off = seg[2].mem_off;
if (m->pub.data_off & 0xFFF) rt_fatal("loader: %s data segment is not page aligned", lay->name);
if (seg[1].mem_off < m->pub.text_end || seg[1].mem_off + seg[1].size > m->pub.data_off)
rt_fatal("loader: %s segments are out of order", lay->name);
parse_dynamic(m);
make_hash(m);
make_funcs(m);
m->res_plain = calloc(m->nsyms, sizeof(Res));
m->res_jump = calloc(m->nsyms, sizeof(Res));
if (!m->res_plain || !m->res_jump) rt_fatal("loader: resolution table allocation");
}
// ---- relocation
// A strong import nothing defines: calling it is fatal with its name (expected count 0).
static void unresolved_stub(Cpu *c) {
char lr[96];
rt_fatal("call to unresolved import %s from %s", hostfn_name(c->pc), ldr_describe(c->x[30] - 4, lr, sizeof lr));
}
static void mark_used(int observers, const char *name) {
int n = hostfn_table_size(observers);
for (int i = 0; i < n; i++)
if (!strcmp(hostfn_table_name(observers, i), name)) (observers ? obs_used : ovr_used)[i] = 1;
}
// Order (MNG loader.c:216-224): observer (JUMP_SLOT only), override by name, own definition, other modules in load
// order, weak -> 0, strong -> named fatal stub. Overrides also replace a module's own symbols.
static Res resolve(Mod *m, uint32_t si, int jump) {
Res *cache = jump ? &m->res_jump[si] : &m->res_plain[si];
if (cache->kind != RES_UNSET) return *cache;
const char *name = sym_name(m, si);
uint64_t guest = sym_defined(m, si) ? m->pub.base + m->syms[si].value : find_symbol(name, NULL);
GuestFn f;
Res r;
if (jump && (f = hostfn_observer(name))) {
r = (Res){hostfn_register_observer(name, f, guest), RES_OBSERVER};
mark_used(1, name);
} else if ((f = hostfn_override(name))) {
r = (Res){hostfn_register(name, f), RES_OVERRIDE};
mark_used(0, name);
} else if (guest) {
r = (Res){guest, RES_GUEST};
} else if ((m->syms[si].info >> 4) == STB_WEAK) {
r = (Res){0, RES_WEAK};
} else {
r = (Res){hostfn_register(name, unresolved_stub), RES_STUB};
stubs++;
rt_log("%s: strong import %s is unresolved", m->pub.name, name);
}
*cache = r;
return r;
}
static void relocate_table(Mod *m, const Elf64Rela *r, uint64_t n) {
uint64_t base = m->pub.base;
for (uint64_t i = 0; i < n; i++) {
uint32_t type = (uint32_t)r[i].info, si = (uint32_t)(r[i].info >> 32);
if (r[i].off < m->pub.data_off || r[i].off > m->pub.size - 8)
rt_fatal("loader: %s relocation %llu writes +%#llx, outside the data segment", m->pub.name,
(unsigned long long)i, (unsigned long long)r[i].off);
uint64_t v;
if (type == R_RELATIVE) {
m->kinds[K_RELATIVE]++;
v = base + (uint64_t)r[i].addend;
} else {
int k = type == R_ABS64 ? K_ABS64 : type == R_GLOB_DAT ? K_GLOB_DAT : type == R_JUMP_SLOT ? K_JUMP_SLOT : -1;
if (k < 0) rt_fatal("loader: %s relocation %llu has type %u", m->pub.name, (unsigned long long)i, type);
if (si == 0 || si >= m->nsyms) rt_fatal("loader: %s relocation %llu names symbol %u", m->pub.name,
(unsigned long long)i, si);
m->kinds[k]++;
if (sym_defined(m, si)) m->sym_own++;
else m->sym_import++;
Res s = resolve(m, si, type == R_JUMP_SLOT);
v = s.kind == RES_WEAK ? 0 : s.value + (type == R_JUMP_SLOT ? 0 : (uint64_t)r[i].addend);
}
ST64(base + r[i].off, v);
}
}
// DT_RELR: an even entry is an offset to relocate (the next bitmap starts after it); an odd entry is a bitmap of the
// 63 words that follow. Each relocated word holds its addend, as R_AARCH64_RELATIVE with an implicit addend.
static void relocate_relr(Mod *m) {
uint64_t base = m->pub.base, where = 0;
for (uint64_t i = 0; i < m->nrelr; i++) {
uint64_t e = m->relr[i];
for (int k = -1; k < 63; k++) {
uint64_t off;
if (k < 0) {
if (e & 1) continue;
off = e;
where = e + 8;
} else {
if (!(e & 1) || !((e >> (k + 1)) & 1)) continue;
off = where + 8ull * k;
}
if (off < m->pub.data_off || off > m->pub.size - 8)
rt_fatal("loader: %s RELR entry %llu writes +%#llx, outside the data segment", m->pub.name,
(unsigned long long)i, (unsigned long long)off);
ST64(base + off, base + LD64(base + off));
m->kinds[K_RELATIVE]++;
m->relr_count++;
}
if (e & 1) where += 63 * 8;
}
}
// Import table statistics: every undefined named symbol, bound by guest definition (overrides aside).
static void count_imports(Mod *m) {
for (uint32_t i = 1; i < m->nsyms; i++) {
if (sym_defined(m, i) || !*sym_name(m, i)) continue;
m->imports++;
int which = -1;
if (find_symbol(sym_name(m, i), &which)) m->import_to[which]++;
else if ((m->syms[i].info >> 4) == STB_WEAK) m->weak_unresolved++;
else m->strong_unresolved++;
}
}
// Every non-JUMP_SLOT reference to nnMain holds the real address (MNG loader.c:229-239).
static void verify_identity(void) {
uint64_t real = ldr_symbol("nnMain");
if (!real) rt_fatal("loader: nnMain is not defined");
for (int i = 0; i < nmods; i++) {
Mod *m = &mods[i];
for (int t = 0; t < 2; t++) {
const Elf64Rela *r = t ? m->jmprel : m->rela;
uint64_t n = t ? m->njmprel : m->nrela;
for (uint64_t j = 0; j < n; j++) {
uint32_t type = (uint32_t)r[j].info, si = (uint32_t)(r[j].info >> 32);
if (type == R_RELATIVE || type == R_JUMP_SLOT || strcmp(sym_name(m, si), "nnMain")) continue;
uint64_t actual = LD64(m->pub.base + r[j].off);
if (actual != real)
rt_fatal("loader: %s +%#llx refers to nnMain as %#llx, not %#llx", m->pub.name,
(unsigned long long)r[j].off, (unsigned long long)actual, (unsigned long long)real);
identity_refs++;
}
}
}
if (!identity_refs) rt_fatal("loader: no reference to nnMain to check");
}
// .text and .rodata read-only: guest code never executes from guest memory, and nothing may write them.
static void seal(Mod *m) {
if (!plat_mem_protect(m->pub.base, m->pub.data_off, PLAT_PROT_READ))
rt_fatal("loader: cannot seal %s", m->pub.name);
}
void ldr_load(const char *exefs_dir) {
if (nmods) rt_fatal("ldr_load called twice");
if (!mem_layout()->pool) rt_fatal("ldr_load before mem_init");
if (rt_layout_count > MOD_COUNT) rt_fatal("loader: %d modules in the layout", rt_layout_count);
for (int i = 0; i < rt_layout_count; i++) load_module(&mods[i], exefs_dir, &rt_layout[i]);
nmods = rt_layout_count;
ovr_used = calloc(hostfn_table_size(0) + 1, sizeof(int));
obs_used = calloc(hostfn_table_size(1) + 1, sizeof(int));
for (int i = 0; i < nmods; i++) {
relocate_table(&mods[i], mods[i].rela, mods[i].nrela);
relocate_table(&mods[i], mods[i].jmprel, mods[i].njmprel);
relocate_relr(&mods[i]);
count_imports(&mods[i]);
mods[i].pub.ctor_flag = decode_ctor_flag(&mods[i]);
}
verify_identity();
for (int i = 0; i < nmods; i++) seal(&mods[i]);
for (int i = 0; i < nmods; i++) {
const Mod *m = &mods[i];
rt_log("loaded %-8s at %#llx size %#llx: %llu relocations, %u imports", m->pub.name,
(unsigned long long)m->pub.base, (unsigned long long)m->pub.size,
(unsigned long long)(m->nrela + m->njmprel + m->relr_count), m->imports);
}
}
// ---- lookups
static int module_of(uint64_t a) {
for (int i = 0; i < nmods; i++)
if (a - mods[i].pub.base < mods[i].pub.size) return i;
return -1;
}
const char *ldr_describe(uint64_t a, char *buf, int n) {
int i = module_of(a);
if (i < 0) {
const char *h = hostfn_name(a);
if (h) snprintf(buf, n, "host %s", h);
else snprintf(buf, n, "%#llx", (unsigned long long)a);
return buf;
}
const Mod *m = &mods[i];
uint64_t off = a - m->pub.base;
uint32_t lo = 0, hi = m->nfuncs; // last function with value <= off
while (lo < hi) {
uint32_t mid = (lo + hi) / 2;
if (m->syms[m->funcs[mid]].value <= off) lo = mid + 1;
else hi = mid;
}
if (lo) {
const Elf64Sym *s = &m->syms[m->funcs[lo - 1]];
if (off - s->value < (s->size ? s->size : 0x100000)) {
snprintf(buf, n, "%s+%#llx (%s+%#llx)", m->pub.name, (unsigned long long)off, m->strtab + s->name,
(unsigned long long)(off - s->value));
return buf;
}
}
snprintf(buf, n, "%s+%#llx", m->pub.name, (unsigned long long)off);
return buf;
}
static int by_slot(const void *a, const void *b) {
uint64_t x = ((const uint64_t *)a)[0], y = ((const uint64_t *)b)[0];
return x < y ? -1 : x > y;
}
// PLT stubs: adrp x16, page; ldr x17, [x16, #slot]; add x16, x16, #slot; br x17.
static void find_plt(Mod *m) {
uint64_t text = m->pub.base + m->seg_off[0], end = text + m->seg_size[0];
uint64_t cap = 1024, n = 0, *pairs = malloc(cap * 16);
for (uint64_t a = text; a + 16 <= end && pairs; a += 4) {
uint32_t w0 = rd32(a), w1 = rd32(a + 4), w3 = rd32(a + 12);
if ((w0 & 0x9f00001fu) != 0x90000010u || (w1 & 0xffc003ffu) != 0xf9400211u || w3 != 0xd61f0220u) continue;
int64_t imm = (int64_t)(((w0 >> 29) & 3) | (((w0 >> 5) & 0x7ffff) << 2));
imm = (imm ^ (1 << 20)) - (1 << 20);
uint64_t slot = (a & ~0xFFFull) + (uint64_t)(imm << 12) + ((w1 >> 10) & 0xfff) * 8;
if (n == cap) pairs = realloc(pairs, (cap *= 2) * 16);
if (!pairs) break;
pairs[2 * n] = slot;
pairs[2 * n + 1] = a;
n++;
}
if (!pairs) rt_fatal("loader: PLT table allocation");
qsort(pairs, n, 16, by_slot);
m->plt_slot = pairs;
m->nplt = n;
}
uint64_t ldr_plt_stub(const char *module, const char *name) {
Mod *m = NULL;
for (int i = 0; i < nmods; i++)
if (!strcmp(mods[i].pub.name, module)) m = &mods[i];
if (!m) return 0;
plat_mutex_lock(&plt_lock);
if (!m->plt_slot) find_plt(m);
plat_mutex_unlock(&plt_lock);
for (uint64_t j = 0; j < m->njmprel; j++) {
uint32_t si = (uint32_t)(m->jmprel[j].info >> 32);
if ((uint32_t)m->jmprel[j].info != R_JUMP_SLOT || strcmp(sym_name(m, si), name)) continue;
uint64_t slot = m->pub.base + m->jmprel[j].off;
uint64_t lo = 0, hi = m->nplt;
while (lo < hi) {
uint64_t mid = (lo + hi) / 2;
if (m->plt_slot[2 * mid] < slot) lo = mid + 1;
else hi = mid;
}
if (lo < m->nplt && m->plt_slot[2 * lo] == slot) return m->plt_slot[2 * lo + 1];
}
return 0;
}
// ---- --probe-loader
// Expected values, measured on the 1.1.0, 1.2.1 and 1.4.3 modules; the 1.4.3 relocation totals match the ones
// MacNativeGames' port notes give for that version. 1.1.0 shares subsdk0 and sdk with 1.2.1 down to every count.
typedef struct {
const char *name;
uint64_t kinds[K_COUNT], total, sym_import, sym_own;
uint32_t imports, to[MOD_COUNT], weak, init_off, init_array;
} Expect;
static const Expect expect_121[] = {
{"main", {508816, 1415, 32, 641}, 510904, 2088, 0, 675, {0, 16, 647}, 12, 0x30, 10673},
{"subsdk0", {1623, 12820, 1030, 3924}, 19397, 6576, 11198, 667, {9, 0, 654}, 4, 0x30, 45},
{"sdk", {10917, 13976, 1207, 8932}, 35032, 16, 24099, 13, {8, 0, 0}, 5, 0x30, 116},
};
static const Expect expect_110[] = {
{"main", {508604, 1412, 32, 639}, 510687, 2083, 0, 673, {0, 16, 645}, 12, 0x30, 10668},
{"subsdk0", {1623, 12820, 1030, 3924}, 19397, 6576, 11198, 667, {9, 0, 654}, 4, 0x30, 45},
{"sdk", {10917, 13976, 1207, 8932}, 35032, 16, 24099, 13, {8, 0, 0}, 5, 0x30, 116},
};
static const Expect expect_143[] = {
{"main", {499078, 1393, 28, 650}, 501149, 2071, 0, 680, {0, 16, 656}, 8, 0x10b4f10, 10717},
{"subsdk0", {3424, 10927, 905, 3817}, 19073, 6557, 9092, 663, {9, 0, 654}, 0, 0x20, 45},
{"sdk", {15013, 10135, 1079, 7832}, 34059, 10, 19036, 8, {8, 0, 0}, 0, 0x400, 83},
};
#define expect (RT_V143 ? expect_143 : RT_V110 ? expect_110 : expect_121)
static int failures;
#define CHECK(cond, ...) do { if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
#define PROBE_SYMBOL "_ZN2nn2os13GetSystemTickEv"
#define PROBE_MAGIC 0x5eed0123456789abull
static volatile int32_t probe_hits;
static void probe_override(Cpu *c) {
plat_inc32(&probe_hits);
c->x[0] = PROBE_MAGIC;
}
// .text and .rodata in guest memory equal the NSO: relocations never touched them.
static void check_segments(const char *dir) {
for (int i = 0; i < nmods; i++) {
const Mod *m = &mods[i];
size_t len;
uint8_t *raw = ldr_read_verified(dir, m->pub.name, &len);
Seg seg[3];
nso_segments(raw, len, m->pub.name, seg);
int same = 1;
for (int s = 0; s < 2; s++) {
uint8_t *buf = malloc(seg[s].size ? seg[s].size : 1);
if (!buf) rt_fatal("probe: allocation");
nso_segment(raw, &seg[s], buf, m->pub.name, s);
if (memcmp(buf, (const void *)(uintptr_t)(m->pub.base + seg[s].mem_off), seg[s].size)) same = 0;
free(buf);
}
free(raw);
uint64_t ro_end;
int ro = plat_mem_state(m->pub.base, m->pub.base + m->pub.data_off, &ro_end) == PLAT_PROT_READ &&
ro_end == m->pub.base + m->pub.data_off;
printf(" %-8s text %#x+%#x rodata %#x+%#x: bytes %s the NSO, %s\n", m->pub.name, seg[0].mem_off, seg[0].size,
seg[1].mem_off, seg[1].size, same ? "equal" : "DIFFER FROM", ro ? "read-only" : "NOT READ-ONLY");
CHECK(same, "%s .text/.rodata changed after relocation", m->pub.name);
CHECK(ro, "%s .text/.rodata are not sealed read-only", m->pub.name);
}
}
static void check_got(void) {
const Mod *m = &mods[0];
uint64_t want = 0, slot = 0, nslots = 0;
for (uint64_t j = 0; j < m->njmprel; j++) {
uint32_t si = (uint32_t)(m->jmprel[j].info >> 32);
if ((uint32_t)m->jmprel[j].info == R_JUMP_SLOT && !strcmp(sym_name(m, si), PROBE_SYMBOL)) {
slot = m->pub.base + m->jmprel[j].off;
nslots++;
}
}
for (int i = 0; i < 4096 && !want; i++) {
const char *n = hostfn_name(RT_HOSTFN_BASE + 16ull * i);
if (!n) break;
if (!strcmp(n, PROBE_SYMBOL)) want = RT_HOSTFN_BASE + 16ull * i;
}
uint64_t got = slot ? LD64(slot) : 0;
char d[128];
printf("probe override %s: main GOT slot %s holds %#llx (%s)\n", PROBE_SYMBOL, ldr_describe(slot, d, sizeof d),
(unsigned long long)got, hostfn_name(got) ? "host function" : "not a host function");
CHECK(nslots == 1 && want && got == want, "the probe override did not reach main's GOT slot");
uint64_t stub = ldr_plt_stub("main", PROBE_SYMBOL);
printf("probe override: main PLT stub %s\n", ldr_describe(stub, d, sizeof d));
CHECK(stub, "no PLT stub in main for %s", PROBE_SYMBOL);
hostfn_seal();
Cpu *c = plat_aligned_alloc(sizeof(Cpu), 64);
memset(c, 0, sizeof *c);
uint8_t *stack = mem_kernel_alloc(0x10000, 16);
c->sp = (uint64_t)(uintptr_t)stack + 0x10000;
c->excl_addr = ~0ull;
c->x[19] = 0x1919;
uint64_t r1 = rt_call(c, got, 0);
long h1 = probe_hits;
uint64_t r2 = stub ? rt_call(c, stub, 0) : 0;
long h2 = probe_hits;
printf("probe override: dispatch through the GOT value -> %#llx (%ld calls), through the lifted PLT stub -> %#llx "
"(%ld calls)\n", (unsigned long long)r1, h1, (unsigned long long)r2, h2);
CHECK(r1 == PROBE_MAGIC && h1 == 1, "dispatching the GOT value did not reach the host function");
CHECK(r2 == PROBE_MAGIC && h2 == 2, "the lifted PLT stub did not reach the host function");
CHECK(c->x[30] == 0 && c->pc == 0 && c->x[19] == 0x1919, "rt_call did not restore x30/pc or lost x19");
mem_kernel_free(stack, 0x10000);
plat_aligned_free(c);
}
int probe_loader(void) {
const RtConfig *cfg = rt_config();
hostfn_add_override(PROBE_SYMBOL, probe_override);
ldr_load(cfg->exefs);
if (nmods != (int)(sizeof expect_121 / sizeof *expect_121)) {
printf("probe-loader: FAILED (%d modules, expected 3)\n", nmods);
return 1;
}
printf("modules (exefs %s):\n", cfg->exefs);
for (int i = 0; i < nmods; i++) {
const Mod *m = &mods[i];
char to[128] = "";
for (int k = 0; k < nmods; k++)
if (m->import_to[k])
snprintf(to + strlen(to), sizeof to - strlen(to), "%s%s %u", *to ? ", " : "", mods[k].pub.name,
m->import_to[k]);
printf(" %-8s base %#llx image %#llx DT_INIT +%#x init_array %llu (%llu B) imports %u: %s; weak unresolved "
"%u\n", m->pub.name, (unsigned long long)m->pub.base, (unsigned long long)m->pub.size, m->init_off,
(unsigned long long)(m->pub.init_array_size / 8), (unsigned long long)m->pub.init_array_size,
m->imports, to, m->weak_unresolved);
CHECK(!strcmp(m->pub.name, expect[i].name), "module %d is %s", i, m->pub.name);
CHECK(m->imports == expect[i].imports && m->weak_unresolved == expect[i].weak &&
!memcmp(m->import_to, expect[i].to, sizeof m->import_to), "%s import table differs from section 0.1",
m->pub.name);
CHECK(m->init_off == expect[i].init_off && m->pub.init_array_size / 8 == expect[i].init_array,
"%s DT_INIT/init_array differ from section 0.1", m->pub.name);
}
printf("relocations:\n");
for (int i = 0; i < nmods; i++) {
const Mod *m = &mods[i];
uint64_t total = m->kinds[0] + m->kinds[1] + m->kinds[2] + m->kinds[3];
printf(" %-8s RELATIVE %llu ABS64 %llu GLOB_DAT %llu JUMP_SLOT %llu total %llu; symbolic against imports %llu, "
"against own symbols %llu\n", m->pub.name, (unsigned long long)m->kinds[K_RELATIVE],
(unsigned long long)m->kinds[K_ABS64], (unsigned long long)m->kinds[K_GLOB_DAT],
(unsigned long long)m->kinds[K_JUMP_SLOT], (unsigned long long)total,
(unsigned long long)m->sym_import, (unsigned long long)m->sym_own);
CHECK(!memcmp(m->kinds, expect[i].kinds, sizeof m->kinds) && total == expect[i].total &&
m->sym_import == expect[i].sym_import && m->sym_own == expect[i].sym_own,
"%s relocations differ from section 0.2", m->pub.name);
}
printf("constructors:\n");
for (int i = 0; i < nmods; i++) {
const RtLoaded *m = &mods[i].pub;
char d[128];
printf(" %-8s DT_INIT %#llx init_array %#llx (%llu entries) ctor_flag %s\n", m->name,
(unsigned long long)m->init, (unsigned long long)m->init_array,
(unsigned long long)(m->init_array_size / 8), m->ctor_flag ? ldr_describe(m->ctor_flag, d, sizeof d)
: "NOT DECODED");
CHECK(m->init && m->init - m->base < m->text_end, "%s DT_INIT outside .text", m->name);
CHECK(m->init_array - m->base >= mods[i].seg_off[1] && m->init_array + m->init_array_size <= m->base + m->size,
"%s init_array outside rodata/data", m->name);
CHECK(m->ctor_flag, "%s constructor flag not decoded", m->name);
}
check_segments(cfg->exefs);
uint32_t strong = 0;
for (int i = 0; i < nmods; i++) strong += mods[i].strong_unresolved;
printf("strong unresolved %u\n", strong);
CHECK(strong == 0 && stubs == 0, "strong imports are unresolved");
int linked = hostfn_table_size(0), installed = 0, olinked = hostfn_table_size(1), oinstalled = 0;
for (int i = 0; i < linked; i++) {
installed += ovr_used[i];
if (!ovr_used[i]) printf(" override %s matches no relocation\n", hostfn_table_name(0, i));
}
for (int i = 0; i < olinked; i++) {
oinstalled += obs_used[i];
if (!obs_used[i]) printf(" observer %s matches no JUMP_SLOT\n", hostfn_table_name(1, i));
}
printf("overrides installed %d (%d linked, + 1 probe override)\n", installed, linked);
printf("observers installed %d (%d linked)\n", oinstalled, olinked);
printf("nnMain identity ok (%llu refs)\n", (unsigned long long)identity_refs);
check_got();
printf("probe-loader: %s (%d failures)\n", failures ? "FAILED" : "passed", failures);
return failures ? 1 : 0;
}