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*_wa.bin) \
|
||||||
$(wildcard /lib/firmware/mediatek/mt79*_rom_patch.bin)
|
$(wildcard /lib/firmware/mediatek/mt79*_rom_patch.bin)
|
||||||
|
|
||||||
.PHONY: help extract test
|
.PHONY: help extract test elf
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "make extract - carve all MediaTek Connac2 blobs found on this host"
|
@echo "make extract - carve all MediaTek Connac2 blobs found on this host"
|
||||||
|
|
@ -13,3 +13,6 @@ extract:
|
||||||
|
|
||||||
test:
|
test:
|
||||||
python3 -m unittest discover -s tests -v
|
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
|
here fails against a newer snapshot, the blob changed — update the golden
|
||||||
value *and* note it in the changelog, don't silently absorb it.
|
value *and* note it in the changelog, don't silently absorb it.
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
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)
|
# filename -> (chip_id, eco, n_region, hidden_trailer_string_or_None)
|
||||||
GOLDEN_WM = {
|
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