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>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-25 20:42:56 +01:00
1 parent 9ffcfb95d2
commit 8efa810e4e
4 files changed
+598 -15

No files matched your search

+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env python3
"""Cross-reference helper for the ESP8266 stock firmware (Xtensa LX106, call0 ABI).
Linear disassembly of the image goes out of sync on inline literal pools, so xrefs are
found by brute force: every byte offset is decoded as l32r / call0 and kept if it hits
the target. Results are then shown with objdump from a given start address, with each
l32r annotated with the literal value it loads.
python tools/xref.py <dump.bin> lit 0x60000304 # literal slots holding a value + their l32r users
python tools/xref.py <dump.bin> calls 0x40100abc # call0 sites + l32r/callx users of a function
python tools/xref.py <dump.bin> str "Light" # string address(es) + l32r users
python tools/xref.py <dump.bin> dis 0x40100abc 0x40100b20
Only the app image (0x1000 slot) is read. The objdump path defaults to the PlatformIO
toolchain; override with $OBJDUMP.
"""
import os
import re
import struct
import subprocess
import sys
import tempfile
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from esp8266_image_to_elf import APP_SLOT_MAX, build_elf, read_segments # noqa: E402
OBJDUMP = os.environ.get("OBJDUMP") or os.path.expanduser(
"~/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-objdump")
class Image:
def __init__(self, dump, offset=0x1000):
with open(dump, "rb") as f:
f.seek(offset)
raw = f.read(APP_SLOT_MAX - offset)
self.entry, self.segs = read_segments(raw)
self._elf = None
def code_segs(self):
return [(a, d) for a, d in self.segs if a >= 0x40000000]
def seg_of(self, addr):
for a, d in self.segs:
if a <= addr < a + len(d):
return a, d
return None
def word(self, addr):
s = self.seg_of(addr)
if not s:
return None
a, d = s
o = addr - a
return struct.unpack_from("<I", d, o)[0] if o + 4 <= len(d) else None
def literals(self, value):
"""Aligned words equal to value (literal pool slots)."""
out = []
pat = struct.pack("<I", value)
for a, d in self.segs:
i = d.find(pat)
while i >= 0:
if (a + i) % 4 == 0:
out.append(a + i)
i = d.find(pat, i + 1)
return out
def l32r_users(self, lit_addr):
"""(pc, reg) of every byte offset that decodes as l32r loading lit_addr."""
out = []
for a, d in self.code_segs():
for i in range(len(d) - 2):
b0 = d[i]
if b0 & 0xF != 1:
continue
imm16 = d[i + 1] | (d[i + 2] << 8)
pc = a + i
tgt = (((pc + 3) & ~3) + ((imm16 | ~0xFFFF) << 2)) & 0xFFFFFFFF
if tgt == lit_addr:
out.append((pc, b0 >> 4))
return out
def call0_sites(self, func):
"""Byte offsets that decode as call0 to func (CALL format: op0=5, n=0)."""
out = []
for a, d in self.code_segs():
for i in range(len(d) - 2):
v = d[i] | (d[i + 1] << 8) | (d[i + 2] << 16)
if v & 0x3F != 0x05:
continue
off = v >> 6
if off & 0x20000:
off -= 0x40000
pc = a + i
if ((pc & ~3) + (off << 2) + 4) & 0xFFFFFFFF == func:
out.append(pc)
return out
def elf(self):
if not self._elf:
fd, path = tempfile.mkstemp(suffix=".elf")
with os.fdopen(fd, "wb") as f:
f.write(build_elf(self.entry, self.segs))
self._elf = path
return self._elf
def dis(self, start, stop):
out = subprocess.run([OBJDUMP, "-d", f"--start-address={start}",
f"--stop-address={stop}", self.elf()],
capture_output=True, text=True).stdout
lines = []
for ln in out.splitlines():
m = re.match(r"\s*([0-9a-f]+):\s", ln)
if not m:
continue
m2 = re.search(r"l32r\s+a\d+, 0x([0-9a-f]+)", ln)
if m2:
w = self.word(int(m2.group(1), 16))
if w is not None:
ln += f" ; ={w:#010x}{self.describe(w)}"
lines.append(ln)
return "\n".join(lines)
def ctx(self, addr, before=24, after=12):
"""Disassembly around addr, starting at an offset that decodes in step with addr."""
tag = f"{addr:x}:"
for back in range(before, before + 16):
text = self.dis(addr - back, addr + after)
lines = text.splitlines()
if any(ln.strip().startswith(tag) for ln in lines):
return "\n".join((">" if ln.strip().startswith(tag) else " ") + ln for ln in lines)
return self.dis(addr - before, addr + after)
def describe(self, w):
"""Short hint for literal values: a printable string or a known register."""
regs = {0x60000300: "GPO", 0x60000304: "GPOS", 0x60000308: "GPOC",
0x6000030c: "GPE", 0x60000310: "GPES", 0x60000314: "GPEC",
0x60000318: "GPI", 0x6000031c: "GPIE", 0x60000320: "GPIES",
0x60000324: "GPIEC", 0x60000800: "IOMUX", 0x60000768: "GP16O",
0x60000774: "GP16E", 0x6000078c: "GP16I", 0x60000790: "GPF16",
0x60000100: "SPI1CMD", 0x60000140: "SPI1W0"}
if w in regs:
return f" {regs[w]}"
if 0x60000000 <= w < 0x60001000:
return " (periph)"
s = self.seg_of(w)
if s and w < 0x40300000:
a, d = s
o = w - a
end = d.find(b"\0", o, o + 64)
if end > o + 2:
t = d[o:end]
if all(32 <= c < 127 for c in t):
return f' "{t.decode()}"'
return ""
def main():
if len(sys.argv) < 4:
raise SystemExit(__doc__)
img = Image(sys.argv[1])
cmd, arg = sys.argv[2], sys.argv[3]
if cmd == "lit":
for lit in img.literals(int(arg, 0)):
users = ", ".join(f"{pc:#x}(a{r})" for pc, r in img.l32r_users(lit))
print(f"literal @ {lit:#x}: {users or '-'}")
elif cmd == "calls":
f = int(arg, 0)
for pc in img.call0_sites(f):
print(f"call0 @ {pc:#x}")
for lit in img.literals(f):
users = ", ".join(f"{pc:#x}(a{r})" for pc, r in img.l32r_users(lit))
print(f"literal @ {lit:#x}: {users or '-'}")
elif cmd == "str":
pat = arg.encode()
for a, d in img.segs:
i = d.find(pat)
while i >= 0:
sa = a + i
users = []
for lit in img.literals(sa):
users += [f"{pc:#x}(a{r})" for pc, r in img.l32r_users(lit)]
print(f"{sa:#x}: {', '.join(users) or '-'}")
i = d.find(pat, i + 1)
elif cmd == "dis":
print(img.dis(int(arg, 0), int(sys.argv[4], 0)))
elif cmd == "ctx":
before = int(sys.argv[4], 0) if len(sys.argv) > 4 else 24
after = int(sys.argv[5], 0) if len(sys.argv) > 5 else 12
for a in arg.split(","):
print(f"--- {a}")
print(img.ctx(int(a, 0), before, after))
else:
raise SystemExit(__doc__)
if __name__ == "__main__":
main()