Files
nixie-clock/tools/esp8266_image_to_elf.py
T
mangavalkandClaude Opus 5.5 8efa810e4e Map stock firmware GPIO use from disassembly
Disassembled the SV1.3 app image and confirmed the shift-register
(HSPI 13/14 + latch 15), WS2812 (12), LDR (A0) and button pins, the
16-bit bit map and the tube order. The stock firmware has no I2C code
and drives a DFPlayer on UART0. Adds the ELF/xref helper tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:42:56 +01:00

103 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Wrap the segments of an ESP8266 (v1) app image in a minimal ELF so binutils can read it.
Reads only the app image (the 0x1000 slot of a full flash dump), never SPIFFS.
python tools/esp8266_image_to_elf.py <full_dump.bin> <out.elf> [--offset 0x1000]
Each image segment becomes one section (.irom0, .iram0_N, .dram0_N) at its load address,
so `xtensa-lx106-elf-objdump -d out.elf` disassembles with real addresses.
"""
import argparse
import struct
APP_SLOT_MAX = 0x100000 # never read past the 1 MB app slot (SPIFFS starts at 0x200000)
def read_segments(image):
magic, nseg, _mode, _sizefreq, entry = struct.unpack_from("<BBBBI", image, 0)
if magic != 0xE9:
raise SystemExit(f"bad image magic 0x{magic:02x}")
off = 8
segs = []
for _ in range(nseg):
addr, size = struct.unpack_from("<II", image, off)
off += 8
segs.append((addr, image[off:off + size]))
off += size
return entry, segs
def section_name(addr, counters):
if 0x40200000 <= addr < 0x40300000:
kind = "irom0"
elif 0x40100000 <= addr < 0x40108000:
kind = "iram0"
else:
kind = "dram0"
counters[kind] = counters.get(kind, 0) + 1
return f".{kind}_{counters[kind] - 1}"
def build_elf(entry, segs):
counters = {}
names = [section_name(a, counters) for a, _ in segs]
shstrtab = b"\0"
name_off = []
for n in names + [".shstrtab"]:
name_off.append(len(shstrtab))
shstrtab += n.encode() + b"\0"
ehsize, phentsize, shentsize = 52, 32, 40
nseg = len(segs)
data_off = ehsize + phentsize * nseg
body = b""
offsets = []
for _, data in segs:
while (data_off + len(body)) % 4:
body += b"\0"
offsets.append(data_off + len(body))
body += data
shstr_off = data_off + len(body)
body += shstrtab
while (data_off + len(body)) % 4:
body += b"\0"
shoff = data_off + len(body)
shnum = nseg + 2
out = bytearray()
out += b"\x7fELF" + bytes([1, 1, 1]) + bytes(9)
out += struct.pack("<HHIIIIIHHHHHH", 2, 94, 1, entry, ehsize, shoff, 0x300,
ehsize, phentsize, nseg, shentsize, shnum, shnum - 1)
for (addr, data), o in zip(segs, offsets):
flags = 5 if addr >= 0x40000000 else 6 # R+X for code, R+W for data
out += struct.pack("<IIIIIIII", 1, o, addr, addr, len(data), len(data), flags, 4)
out += body
out += bytes(shentsize) # null section
for i, ((addr, data), o) in enumerate(zip(segs, offsets)):
code = addr >= 0x40000000
sh_flags = 0x2 | (0x4 if code else 0x1) # ALLOC + EXECINSTR / WRITE
out += struct.pack("<IIIIIIIIII", name_off[i], 1, sh_flags, addr, o, len(data), 0, 0, 4, 0)
out += struct.pack("<IIIIIIIIII", name_off[-1], 3, 0, 0, shstr_off, len(shstrtab), 0, 0, 1, 0)
return bytes(out)
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("dump")
ap.add_argument("out")
ap.add_argument("--offset", type=lambda s: int(s, 0), default=0x1000)
args = ap.parse_args()
with open(args.dump, "rb") as f:
f.seek(args.offset)
image = f.read(APP_SLOT_MAX - args.offset)
entry, segs = read_segments(image)
with open(args.out, "wb") as f:
f.write(build_elf(entry, segs))
for addr, data in segs:
print(f"0x{addr:08x} 0x{len(data):06x}")
if __name__ == "__main__":
main()