Files
Valiantsin DzerakhandClaude Opus 5.5 776d547169 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>
2026-10-26 17:27:36 +03:00

135 lines
5.8 KiB
Python

# Adapted from LAS Recomp tools/nso.py @ da35b83 (MIT, Copyright (c) 2026 Peiikai).
"""NSO loader: decompress segments, build the memory image, parse MOD0/dynamic info."""
import struct
import sys
from collections import Counter
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
class Nso:
def __init__(self, path):
raw = open(path, 'rb').read()
assert raw[:4] == b'NSO0', path
flags = struct.unpack_from('<I', raw, 0xC)[0]
self.build_id = raw[0x40:0x60].rstrip(b'\0').hex()
segs = []
for i in range(3):
file_off, mem_off, dec_size = struct.unpack_from('<III', raw, 0x10 + i * 0x10)
comp_size = struct.unpack_from('<I', raw, 0x60 + i * 4)[0]
data = raw[file_off:file_off + comp_size]
if flags & (1 << i):
data = lz4.block.decompress(data, uncompressed_size=dec_size)
segs.append((mem_off, data))
self.bss_size = struct.unpack_from('<I', raw, 0x3C)[0]
self.text = (segs[0][0], len(segs[0][1]))
self.rodata = (segs[1][0], len(segs[1][1]))
self.data = (segs[2][0], len(segs[2][1]))
end = segs[2][0] + len(segs[2][1]) + self.bss_size
self.size = (end + 0xFFF) & ~0xFFF
img = bytearray(self.size)
for off, d in segs:
img[off:off + len(d)] = d
self.image = img
self._parse_mod0()
def u32(self, off): return struct.unpack_from('<I', self.image, off)[0]
def u64(self, off): return struct.unpack_from('<Q', self.image, off)[0]
def _parse_mod0(self):
mod0 = self.u32(4)
assert self.image[mod0:mod0 + 4] == b'MOD0'
rel = lambda i: mod0 + struct.unpack_from('<i', self.image, mod0 + 4 + i * 4)[0]
self.dynamic, self.bss_start, self.bss_end = rel(0), rel(1), rel(2)
self.eh_start, self.eh_end, self.module_obj = rel(3), rel(4), rel(5)
dyn = {}
needed = []
off = self.dynamic
while True:
tag, val = self.u64(off), self.u64(off + 8)
off += 16
if tag == DT_NULL:
break
if tag == DT_NEEDED:
needed.append(val)
dyn.setdefault(tag, val)
self.dyn = dyn
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, or from DT_GNU_HASH (1.4.3 sdk and subsdk0 have no DT_HASH)
self.syms = []
nsyms = self.symbol_count()
if nsyms and DT_SYMTAB in dyn:
st = dyn[DT_SYMTAB]
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)
self.syms.append((self.cstr(strtab + name), info, shndx, value, size))
self.relocs = []
for base_tag, size_tag in ((DT_RELA, DT_RELASZ), (DT_JMPREL, DT_PLTRELSZ)):
if base_tag in dyn:
b, n = dyn[base_tag], dyn[size_tag] // 24
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)
return self.image[off:end].decode('utf-8', 'replace')
def imports(self):
return [s for s in self.syms if s[0] and s[2] == 0]
def exports(self):
return [s for s in self.syms if s[0] and s[2] != 0]
if __name__ == '__main__':
for p in sys.argv[1:]:
n = Nso(p)
print(f'== {p} build {n.build_id[:16]} image 0x{n.size:x}')
print(f' text 0x{n.text[0]:x}+0x{n.text[1]:x} ro 0x{n.rodata[0]:x}+0x{n.rodata[1]:x} '
f'data 0x{n.data[0]:x}+0x{n.data[1]:x} bss 0x{n.bss_size:x}')
print(f' needed {n.needed} syms {len(n.syms)} imports {len(n.imports())} exports {len(n.exports())}')
print(f' relocs {dict(Counter(r[1] for r in n.relocs))}')