ELF emitter: EM_NDS32 ET_EXEC, PT_LOAD per region at true addresses; readelf-validated; tests (structure + segment fidelity); MTK_FW_DIR env override
This commit is contained in:
parent
8c4a55e60a
commit
c167f893b5
4 changed files with 182 additions and 2 deletions
5
Makefile
5
Makefile
|
|
@ -2,7 +2,7 @@ BLOBS ?= $(wildcard /lib/firmware/mediatek/mt79*_wm.bin) \
|
|||
$(wildcard /lib/firmware/mediatek/mt79*_wa.bin) \
|
||||
$(wildcard /lib/firmware/mediatek/mt79*_rom_patch.bin)
|
||||
|
||||
.PHONY: help extract test
|
||||
.PHONY: help extract test elf
|
||||
|
||||
help:
|
||||
@echo "make extract - carve all MediaTek Connac2 blobs found on this host"
|
||||
|
|
@ -13,3 +13,6 @@ extract:
|
|||
|
||||
test:
|
||||
python3 -m unittest discover -s tests -v
|
||||
|
||||
elf:
|
||||
python3 tools/mtk_fw_elf.py $(BLOBS) -o extracted
|
||||
|
|
|
|||
78
tests/test_elf.py
Normal file
78
tests/test_elf.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""ELF emitter tests: structural validity + segment fidelity.
|
||||
|
||||
Validates with readelf where available and always with a programmatic
|
||||
re-parse against the container's own region table.
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / 'tools'))
|
||||
import mtk_fw_elf as E # noqa: E402
|
||||
|
||||
FW = Path(os.environ.get('MTK_FW_DIR', '/lib/firmware/mediatek'))
|
||||
|
||||
|
||||
def blobs():
|
||||
return sorted(FW.glob('mt79*_wm.bin')) + sorted(FW.glob('mt79*_wa.bin')) \
|
||||
+ sorted(FW.glob('mt79*_rom_patch.bin'))
|
||||
|
||||
|
||||
class Emitter(unittest.TestCase):
|
||||
|
||||
def test_structure_and_fidelity(self):
|
||||
found = blobs()
|
||||
if not found:
|
||||
self.skipTest('no mediatek blobs')
|
||||
for p in found:
|
||||
with self.subTest(blob=p.name):
|
||||
d = p.read_bytes()
|
||||
if 'patch' in p.name:
|
||||
regions = list(E.patch_regions(d))
|
||||
else:
|
||||
regions = list(E.ram_regions(d))
|
||||
regions = [(a, dat) for a, dat in regions if dat]
|
||||
elf = E.build_elf(regions)
|
||||
# ehdr sanity
|
||||
self.assertEqual(elf[:4], b'\x7fELF')
|
||||
etype, mach, _, entry, phoff = struct.unpack_from('<HHIII', elf, 16)
|
||||
self.assertEqual((etype, mach), (2, 167))
|
||||
self.assertEqual(phoff, 52)
|
||||
phentsize, phnum = struct.unpack_from('<HH', elf, 42)
|
||||
self.assertEqual((phentsize, phnum), (32, len(regions)))
|
||||
# segment fidelity
|
||||
for i, (addr, dat) in enumerate(regions):
|
||||
p_type, p_off, p_va, p_pa, p_filesz, p_memsz = \
|
||||
struct.unpack_from('<IIIIII', elf, phoff + i * 32)
|
||||
self.assertEqual((p_type, p_va, p_pa), (1, addr, addr))
|
||||
self.assertEqual((p_filesz, p_memsz), (len(dat),) * 2)
|
||||
self.assertEqual(elf[p_off:p_off + p_filesz], dat)
|
||||
|
||||
def test_readelf_accepts(self):
|
||||
found = blobs()
|
||||
if not found:
|
||||
self.skipTest('no mediatek blobs')
|
||||
if subprocess.run(['which', 'readelf'], capture_output=True).returncode:
|
||||
self.skipTest('readelf unavailable')
|
||||
import tempfile
|
||||
for p in found[:4]:
|
||||
with self.subTest(blob=p.name):
|
||||
d = p.read_bytes()
|
||||
regs = list(E.patch_regions(d) if 'patch' in p.name
|
||||
else E.ram_regions(d))
|
||||
elf = E.build_elf(regs)
|
||||
with tempfile.NamedTemporaryFile(suffix='.elf') as f:
|
||||
f.write(elf)
|
||||
f.flush()
|
||||
r = subprocess.run(['readelf', '-l', f.name],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn('LOAD', r.stdout)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -5,13 +5,14 @@ Golden values observed from linux-firmware snapshot 2026-04-20; when a value
|
|||
here fails against a newer snapshot, the blob changed — update the golden
|
||||
value *and* note it in the changelog, don't silently absorb it.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
FW = Path('/lib/firmware/mediatek')
|
||||
FW = Path(os.environ.get('MTK_FW_DIR', '/lib/firmware/mediatek'))
|
||||
|
||||
# filename -> (chip_id, eco, n_region, hidden_trailer_string_or_None)
|
||||
GOLDEN_WM = {
|
||||
|
|
|
|||
98
tools/mtk_fw_elf.py
Executable file
98
tools/mtk_fw_elf.py
Executable file
|
|
@ -0,0 +1,98 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Emit a spec-correct 32-bit LE ELF (e_machine=EM_NDS32) from a MediaTek
|
||||
Connac2 WiFi firmware container, one PT_LOAD segment per region at true
|
||||
load addresses.
|
||||
|
||||
Structure is validated by readelf; disassembly requires Ghidra (official
|
||||
NDS32 module) or an Andes nds32le-elf toolchain — Debian binutils dropped
|
||||
NDS32. Entry point is unknown pre-RE (e_entry=0); set it in your tool after
|
||||
import.
|
||||
|
||||
NON_DL regions are included as loadable segments by default; they are
|
||||
host-side data per the driver, not downloaded to the MCU.
|
||||
"""
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from mtk_fw_extract import PATCH_HDR, PATCH_SEC, REGION, TRAILER # noqa: E402
|
||||
|
||||
EM_NDS32 = 167
|
||||
EHDR = struct.Struct('<16sHHIIIIIHHHHHH') # 52 bytes
|
||||
PHDR = struct.Struct('<IIIIIIII') # 32 bytes
|
||||
PT_LOAD, PF_RWX = 1, 7
|
||||
|
||||
|
||||
def ram_regions(d: bytes):
|
||||
"""Yield (addr, data) for each region of a RAM container."""
|
||||
t = TRAILER.unpack_from(d, len(d) - TRAILER.size)
|
||||
n = t[2]
|
||||
table = len(d) - TRAILER.size - n * REGION.size
|
||||
off = 0
|
||||
for i in range(n):
|
||||
r = REGION.unpack_from(d, table + i * REGION.size)
|
||||
addr, ln = r[4], r[5]
|
||||
data = d[off:off + ln]
|
||||
off += ln
|
||||
yield addr, data
|
||||
|
||||
|
||||
def patch_regions(d: bytes):
|
||||
"""Yield (addr, data) for each section of a patch container."""
|
||||
h = PATCH_HDR.unpack_from(d, 0)
|
||||
n = h[9]
|
||||
for i in range(n):
|
||||
s = PATCH_SEC.unpack_from(d, PATCH_HDR.size + i * PATCH_SEC.size)
|
||||
typ, offs, size = s[0], s[1], s[2]
|
||||
addr = s[3]
|
||||
yield addr, d[offs:offs + size]
|
||||
|
||||
|
||||
def build_elf(regions) -> bytes:
|
||||
regions = [(a, dat) for a, dat in regions if dat]
|
||||
n = len(regions)
|
||||
phoff = EHDR.size
|
||||
data_off = phoff + n * PHDR.size
|
||||
eh = EHDR.pack(
|
||||
b'\x7fELF' + bytes([1, 1, 1, 0]) + b'\x00' * 8, # ELFCLASS32/LSB/v1
|
||||
2, EM_NDS32, 1,
|
||||
regions[0][0] if regions else 0, # e_entry (first region; see doc)
|
||||
phoff, 0, 0,
|
||||
EHDR.size, PHDR.size, n,
|
||||
40, 0, 0)
|
||||
out = bytearray(eh)
|
||||
offset = data_off
|
||||
for addr, dat in regions:
|
||||
out += PHDR.pack(PT_LOAD, offset, addr, addr, len(dat), len(dat),
|
||||
PF_RWX, 0x1000)
|
||||
offset += len(dat)
|
||||
for _, dat in regions:
|
||||
out += dat
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit(f'usage: {sys.argv[0]} <blob> [<blob>...] [-o outdir]')
|
||||
args, outdir = [], Path('extracted')
|
||||
it = iter(sys.argv[1:])
|
||||
for a in it:
|
||||
if a == '-o':
|
||||
outdir = Path(next(it))
|
||||
else:
|
||||
args.append(a)
|
||||
for path in args:
|
||||
p = Path(path)
|
||||
d = p.read_bytes()
|
||||
regions = patch_regions(d) if 'patch' in p.name else ram_regions(d)
|
||||
elf = build_elf(regions)
|
||||
out = outdir / p.stem / f'{p.stem}.elf'
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_bytes(elf)
|
||||
n_seg = struct.unpack_from('<H', elf, 44)[0] # e_phnum
|
||||
print(f'{p.name} -> {out} ({len(elf)} bytes, {n_seg} PT_LOAD)')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Reference in a new issue