Load 1.4.3 modules: DT_RELR, symbol count from DT_GNU_HASH, stg as a no-op, version-aware boot anchors and loader probe values

The jump-table check now explains oracle targets for case indices peeled off by cmp #k; b.ne before the table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valiantsin DzerakhandClaude Opus 5.5 committed 2026-10-26 17:27:36 +03:00
1 parent ba2b3f439f
commit 776d547169
8 files changed
+141 -29

No files matched your search

+12
View File
@@ -383,8 +383,20 @@ class Lifter:
return self.ldst_pair(w)
if bits(w, 27, 3) == 0b111:
return self.ldst_reg(w)
if (w & 0xFFE00000) == 0xD9200000 and bits(w, 10, 2):
return self.stg(w)
raise Unsupported(f'ldst {w:08x}')
def stg(self, w):
"""STG (post-index, signed offset, pre-index). Horizon runs without MTE, so storing an allocation tag
changes no memory; only the base writeback remains (1.4.3 sdk+0x640050)."""
mode, rn = bits(w, 10, 2), bits(w, 5, 5)
if mode == 2:
return Insn('op', '/* stg */')
off = sext(bits(w, 12, 9), 9) << 4
dst = 'c->sp' if rn == 31 else f'c->x[{rn}]'
return Insn('op', f'{dst} = {XS(rn)} + {hx(off & 0xFFFFFFFFFFFFFFFF)}; /* stg */')
def ldst_excl(self, w, pc):
size, o2, L, o1, rs, o0, rn, rt = (bits(w, 30, 2), bits(w, 23, 1), bits(w, 22, 1), bits(w, 21, 1),
bits(w, 16, 5), bits(w, 15, 1), bits(w, 5, 5), bits(w, 0, 5))
+38 -4
View File
@@ -8,6 +8,7 @@ import lz4.block
DT_NULL, DT_NEEDED, DT_PLTRELSZ, DT_HASH, DT_STRTAB, DT_SYMTAB, DT_RELA, DT_RELASZ = 0, 1, 2, 4, 5, 6, 7, 8
DT_STRSZ, DT_JMPREL, DT_RELACOUNT = 10, 23, 0x6FFFFFF9
DT_RELRSZ, DT_RELR, DT_GNU_HASH = 0x23, 0x24, 0x6FFFFEF5
R_AARCH64_ABS64, R_AARCH64_GLOB_DAT, R_AARCH64_JUMP_SLOT, R_AARCH64_RELATIVE = 257, 1025, 1026, 1027
@@ -61,12 +62,12 @@ class Nso:
strtab = dyn.get(DT_STRTAB, 0)
self.strtab = strtab
self.needed = [self.cstr(strtab + n) for n in needed]
# dynsym count from DT_HASH nchain
# dynsym count from DT_HASH nchain, or from DT_GNU_HASH (1.4.3 sdk and subsdk0 have no DT_HASH)
self.syms = []
if DT_HASH in dyn and DT_SYMTAB in dyn:
nchain = self.u32(dyn[DT_HASH] + 4)
nsyms = self.symbol_count()
if nsyms and DT_SYMTAB in dyn:
st = dyn[DT_SYMTAB]
for i in range(nchain):
for i in range(nsyms):
o = st + i * 24
name, info, other, shndx = struct.unpack_from('<IBBH', self.image, o)
value, size = struct.unpack_from('<QQ', self.image, o + 8)
@@ -78,6 +79,39 @@ class Nso:
for i in range(n):
r_off, r_info, r_add = struct.unpack_from('<QQq', self.image, b + i * 24)
self.relocs.append((r_off, r_info & 0xFFFFFFFF, r_info >> 32, r_add))
# DT_RELR (1.4.3): packed RELATIVE relocations whose addend is the word in place. An even entry is an address
# (relocate it, continue after it); an odd entry is a bitmap of the next 63 words.
if DT_RELR in dyn:
where = 0
for i in range(dyn[DT_RELRSZ] // 8):
e = self.u64(dyn[DT_RELR] + i * 8)
if e & 1 == 0:
self.relocs.append((e, R_AARCH64_RELATIVE, 0, self.u64(e)))
where = e + 8
continue
for k in range(63):
if e >> (k + 1) & 1:
a = where + k * 8
self.relocs.append((a, R_AARCH64_RELATIVE, 0, self.u64(a)))
where += 63 * 8
def symbol_count(self):
"""Number of .dynsym entries: DT_HASH nchain, else one past the last symbol a DT_GNU_HASH chain reaches."""
dyn = self.dyn
if DT_HASH in dyn:
return self.u32(dyn[DT_HASH] + 4)
if DT_GNU_HASH not in dyn:
return 0
h = dyn[DT_GNU_HASH]
nbuckets, symoffset, bloom_size = self.u32(h), self.u32(h + 4), self.u32(h + 8)
buckets = h + 16 + bloom_size * 8
last = max([self.u32(buckets + 4 * i) for i in range(nbuckets)] + [0])
if last < symoffset:
return symoffset
chains = buckets + 4 * nbuckets
while not self.u32(chains + 4 * (last - symoffset)) & 1:
last += 1
return last + 1
def cstr(self, off):
end = self.image.index(b'\0', off)
+15 -9
View File
@@ -4,8 +4,10 @@
// The boot driver: the guest main thread runs the game's own nn::init::Start
// with three host callbacks, the second of which runs DT_INIT of main, subsdk0 and sdk; Start then calls nnMain.
// --boot checks two checkpoints on the way and ends at the first nvnBootstrapLoader call:
// SetFocusHandlingMode called from main+0xe7a954 in nnMain, every module's constructor flag reads 2
// nvnBootstrapLoader called from main+0xe7eb2c in the graphics bring-up; logs the run statistics, exits 0
// SetFocusHandlingMode called from main+0xe7a954 (1.4.3: +0xbdb48c) in nnMain, every module's constructor flag
// reads 2
// nvnBootstrapLoader called from main+0xe7eb2c (1.4.3: +0xbe0360) in the graphics bring-up; logs the run
// statistics, exits 0
// With --frames N the run continues past that call; rt_frame_presented ends it at the Nth present.
#include <windows.h>
#include <stdio.h>
@@ -18,13 +20,17 @@
#include "vk.h"
#define SYM_INIT_START "_ZN2nn4init5StartEmmPFvvES2_S2_"
#define CHECKPOINT_RET 0xe7a954ull // main: return address of nnMain's SetFocusHandlingMode call (section 0.4)
#define NVN_RET 0xe7eb2cull // main: return address of the first nvnBootstrapLoader call
// main: return addresses of nnMain's SetFocusHandlingMode call (section 0.4; 1.4.3: MNG scripts/verify_boot.py:9) and
// of the first nvnBootstrapLoader call, per game version.
#define CHECKPOINT_RET (RT_V143 ? 0xbdb48cull : 0xe7a954ull)
#define NVN_RET (RT_V143 ? 0xbe0360ull : 0xe7eb2cull)
#define CTOR_DONE 2 // a module's constructor flag after its DT_INIT ran the init_array
// The init_array sizes of 1.2.1 (section 0.1); MacNativeGames' boot check expects the same.
static const struct { const char *name; uint64_t entries; } expected_inits[] = {
{"main", 10673}, {"subsdk0", 45}, {"sdk", 116}};
// The init_array sizes (1.2.1: section 0.1; 1.4.3: DT_INIT_ARRAYSZ); MacNativeGames' boot check expects the same.
typedef struct { const char *name; uint64_t entries; } InitCount;
static const InitCount inits_121[] = {{"main", 10673}, {"subsdk0", 45}, {"sdk", 116}};
static const InitCount inits_143[] = {{"main", 10717}, {"subsdk0", 45}, {"sdk", 83}};
#define expected_inits (RT_V143 ? inits_143 : inits_121)
static volatile LONG inits_done, checkpoint_seen;
@@ -63,7 +69,7 @@ static void cb_loader_ready(Cpu *c) {
// afterwards (MacNativeGames loader.c:458-462).
static void cb_module_inits(Cpu *c) {
if (InterlockedCompareExchange(&inits_done, 0, 0)) rt_fatal("boot: module constructors requested twice");
for (unsigned k = 0; k < sizeof expected_inits / sizeof *expected_inits; k++) {
for (unsigned k = 0; k < sizeof inits_121 / sizeof *inits_121; k++) {
const RtLoaded *m = module_named(expected_inits[k].name);
uint64_t entries = m->init_array_size / 8;
if (!m->init) rt_fatal("boot: %s has no DT_INIT", m->name);
@@ -73,7 +79,7 @@ static void cb_module_inits(Cpu *c) {
printf("boot: DT_INIT %s state=%u (%llu)\n", m->name, state, (unsigned long long)entries);
if (state != CTOR_DONE) rt_fatal("boot: %s DT_INIT left its constructor flag at %u, not 2", m->name, state);
if (entries != expected_inits[k].entries)
rt_fatal("boot: %s has %llu init_array entries, 1.2.1 has %llu", m->name, (unsigned long long)entries,
rt_fatal("boot: %s has %llu init_array entries, this game version has %llu", m->name, (unsigned long long)entries,
(unsigned long long)expected_inits[k].entries);
}
InterlockedExchange(&inits_done, 1);
+3 -3
View File
@@ -106,10 +106,10 @@ static uint64_t read_ptr(uint64_t a) {
uint64_t p = 0;
return read_guest(a, &p, 8) ? p : 0;
}
// 1.2.1: [main+0x4728538] +0x1e8 scene, its component 13 lists the resident actors; the one named "Player" has its
// position at +0x2b4 (MacNativeGames world_survey.c, game_version.h TOTK_GAME_V121).
// 1.2.1: [main+0x4728538] (1.4.3: [main+0x3add238]) +0x1e8 scene, its component 13 lists the resident actors; the one
// named "Player" has its position at +0x2b4 (MacNativeGames world_survey.c, game_version.h TOTK_GAME_V121/V143).
static int player_pos(float xyz[3]) {
uint64_t module = read_ptr(MOD_MAIN_BASE + 0x4728538), scene = module ? read_ptr(module + 0x1e8) : 0;
uint64_t module = read_ptr(MOD_MAIN_BASE + (RT_V143 ? 0x3add238 : 0x4728538)), scene = module ? read_ptr(module + 0x1e8) : 0;
int32_t count = 0;
if (!scene || !read_guest(scene + 0x50, &count, 4) || count < 14 || count > 256) return 0;
uint64_t comps = read_ptr(scene + 0x58), resident = comps ? read_ptr(comps + 13 * 8) : 0;
+2
View File
@@ -163,6 +163,8 @@ typedef struct { const char *file, *sha256; } RtFileHash;
extern const RtFileHash rt_exefs_sha256[]; // tools/data/expected-<version>.json "exefs_sha256"
extern const int rt_exefs_sha256_count;
extern const char rt_default_exefs[], rt_default_romfs[], rt_default_profile[];
extern const int rt_game_version; // the game build: 121 (1.2.1) or 143 (1.4.3)
#define RT_V143 (rt_game_version >= 143)
// ---- kernel (kernel.c, T2)
typedef struct RtThread RtThread;
+67 -9
View File
@@ -19,7 +19,7 @@ uint64_t hostfn_register_observer(const char *name, GuestFn fn, uint64_t origina
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_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 };
@@ -39,6 +39,8 @@ typedef struct {
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)
@@ -219,12 +221,15 @@ static void parse_dynamic(Mod *m) {
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;
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;
@@ -234,8 +239,26 @@ static void parse_dynamic(Mod *m) {
case DT_INIT_ARRAYSZ: m->pub.init_array_size = d[1]; break;
}
}
if (!m->syms || !m->strtab || !hash) rt_fatal("loader: %s lacks DT_SYMTAB, DT_STRTAB or DT_HASH", m->pub.name);
m->nsyms = rd32(hash + 4); // nchain
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;
@@ -363,6 +386,33 @@ static void relocate_table(Mod *m, const Elf64Rela *r, uint64_t n) {
}
}
// 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++) {
@@ -416,6 +466,7 @@ void ldr_load(const char *exefs_dir) {
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]);
}
@@ -425,7 +476,7 @@ void ldr_load(const char *exefs_dir) {
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->imports);
(unsigned long long)(m->nrela + m->njmprel + m->relr_count), m->imports);
}
}
@@ -515,16 +566,23 @@ uint64_t ldr_plt_stub(const char *module, const char *name) {
}
// ---- --probe-loader
// Expected values, measured on the 1.2.1 modules.
static const struct {
// Expected values, measured on the 1.2.1 and 1.4.3 modules (1.4.3 relocation totals match MNG v1.4.3-port.md:20).
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[] = {
} 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_143[] = {
{"main", {499078, 1393, 28, 650}, 501149, 0, 0, 0, {0, 0, 0}, 0, 0x10b4f10, 10717},
{"subsdk0", {3424, 10927, 905, 3817}, 19073, 0, 0, 0, {0, 0, 0}, 0, 0x20, 45},
{"sdk", {15013, 10135, 1079, 7832}, 34059, 0, 0, 0, {0, 0, 0}, 0, 0x400, 83},
};
#define expect (RT_V143 ? expect_143 : expect_121)
static int failures;
#define CHECK(cond, ...) do { if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
@@ -614,7 +672,7 @@ 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 / sizeof *expect)) {
if (nmods != (int)(sizeof expect_121 / sizeof *expect_121)) {
printf("probe-loader: FAILED (%d modules, expected 3)\n", nmods);
return 1;
}
Binary file not shown.
+4 -4
View File
@@ -187,9 +187,9 @@ def runtime_object(out, src):
return out / 'obj' / 'runtime' / rel.with_suffix('.o')
def module_glue(layout, linked, hashes, paths):
def module_glue(layout, linked, hashes, paths, version):
"""C for build/<version>/modules.c: the layout table, the linked modules' function tables (dispatch.h), the
expected ExeFS hashes and the default game paths (host.h)."""
expected ExeFS hashes, the game version and the default game paths (host.h)."""
out = [f'// Module glue written by tools/build_game.py; do not edit. {GENERATED_MARKER}',
'#include "dispatch.h"', '#include "host.h"', '#include "layout.h"', '']
for name, base, size in layout:
@@ -213,7 +213,7 @@ def module_glue(layout, linked, hashes, paths):
out += ['};', f'const int rt_exefs_sha256_count = {len(hashes)};', '']
for name in ('exefs', 'romfs', 'profile'):
out.append(f'const char rt_default_{name}[] = {c_string(paths[name].as_posix())};')
out.append('')
out += [f'const int rt_game_version = {int(DISPLAY_VERSIONS[version].replace(".", "")) if version in DISPLAY_VERSIONS else 0};', '']
return '\n'.join(out)
@@ -645,7 +645,7 @@ def main():
missing = [n for n in names if n not in hashes]
if missing:
sys.exit(f'{expected}: no exefs_sha256 for {missing}')
write_if_changed(out / 'modules.c', module_glue(layout, linked, hashes, paths))
write_if_changed(out / 'modules.c', module_glue(layout, linked, hashes, paths, args.version))
if (paths['romfs'] / 'index.json').is_file():
count, size, dt = romfs_index.write(paths['romfs'], out)
print(f'romfs index: {count} files, {mib(size)}, {dt:.1f} s')