mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 18:01:03 +00:00
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>
738 lines
33 KiB
C
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;
|
|
}
|