R/r/w/F collapse into G and g over a selector byte naming the space — flash,
EEPROM, data, fuse, SPM — with the flash bank in its high nibble. Four command
bodies, four transfer loops and four argument decodes become one of each, and
W joins the same decode instead of keeping an address form of its own. The
loader shrinks while gaining everything below: on the 1284P the stock build
goes 480 -> 432 B and the software one 496 -> 450.
What the freed space buys:
- Data space. On AVR one pointer spans SRAM, the register file and the whole
I/O space, so G over space 2 reads all three. pureboot keeps zero static
RAM and pushes no register, so at loader entry an application's SRAM is
still what the application left there — this is a post-mortem, not just a
poke hole. As its own command it needed a dispatch arm and a loop; as one
more space it is a single ld/st.
- Host-issued SPM. W fills the page buffer and stops; erase, write and RWW
re-enable are writes to space 4, which reach the same fused store-and-SPM
pair through the transfer's own address and data. Any SPM operation, lock
bits included, is now reachable and the loader carries no page-commit logic.
The four-cycle SPMCSR-to-SPM window is why that primitive stays fused: no
host can hit it across a serial link, and that — not the byte count — is
the floor on how low-level a bootloader's primitives can go.
- Byte addresses everywhere. The bank in the selector retires the
word-addressed wire the >64 KiB parts needed, so the 1284s stop being the
outlier.
SERIAL autobaud is a third backend on the same loader, over libavr's
software_autobaud: no clock, no baud, one binary per chip for every F_CPU and
every rate. Activation counts poll iterations rather than seconds and bounds
every wait, so a stray pulse cannot hold an unattended device.
b answers with the version and signature only; the host derives geometry from
the signature, which is what an autobaud build requires anyway. An update image
is a bare slot with no device to ask, so every image carries a six-byte stamp —
the same bytes b answers with, and the source of both — that the loader never
reads from flash and the host refuses to install a mismatch against. The
running-slot write guard moved onto the SPM commit, which covers erase and
write both where guarding W covered neither directly.
The position-independence lint now proves the property instead of a proxy for
it: the image must come out byte-identical linked at a different base.
-fno-move-loop-invariants left the tuned flag set — it was fitted to a command
loop carrying four transfer bodies and costs bytes now that it carries one.
Verified: the exhaustive matrix on all 37 chips (every clock x every baud x
every backend, non-standard rates included, plus the autobaud build) —
8174 size checks, no failures, tightest fit the 1284s' autobaud at 510 of 512.
Behavioral suites green on every chip class: t13a 10/10, t85 11/11, m8 13/13,
m16a 13/13, m48pa 13/13, 328P 23/23, 644A 17/17, 1284P 17/17. Data-space
round trip through --peek/--poke and the autobaud handshake are both red-green
proven.
The two prototype sources and their findings file go; the README carries the
protocol and dev/done.md in libavr carries the reasoning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
77 lines
3.4 KiB
Python
77 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Position-independence lint: the property that lets the identical image run
|
|
from any slot, asserted from the built ELF and its object.
|
|
|
|
1. No absolute jmp/call — -mrelax normally guarantees it, but a branch that
|
|
grows out of relaxation range would break it silently.
|
|
2. Nothing flash-resident to address: the image is .text alone, so there is
|
|
no table whose runtime address has to be reconstructed.
|
|
3. The image is byte-identical when linked at a different base. This is
|
|
position independence itself rather than a proxy for it — an absolute
|
|
address anywhere in the image would move with the link and show up as a
|
|
differing byte.
|
|
|
|
Usage: check_pi.py <objdump> <objcopy> <cxx> <mcu> <elf> <object> <text_start_hex>
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
|
|
def fail(message):
|
|
print(f"FAIL: {message}")
|
|
sys.exit(1)
|
|
|
|
|
|
def main():
|
|
objdump, objcopy, cxx, mcu, elf, obj, text_start = sys.argv[1:]
|
|
text_start = int(text_start, 0)
|
|
|
|
listing = subprocess.run([objdump, "-d", elf], capture_output=True, text=True, check=True).stdout
|
|
absolute = [line for line in listing.splitlines() if re.search(r"\t(jmp|call)\t", line)]
|
|
if absolute:
|
|
fail("absolute control flow in the image:\n" + "\n".join(absolute))
|
|
|
|
# Allocated flash beyond .text would be data the running copy has to find.
|
|
# Only ALLOC sections reach the device at all; .comment and the debug
|
|
# sections ride along in the ELF container and are never flashed. objdump
|
|
# prints each section's flags on the line following its header.
|
|
headers = subprocess.run([objdump, "-h", elf], capture_output=True, text=True, check=True).stdout.splitlines()
|
|
for index, line in enumerate(headers):
|
|
fields = line.split()
|
|
if len(fields) < 6 or not fields[0].isdigit():
|
|
continue
|
|
name, size = fields[1], int(fields[2], 16)
|
|
flags = headers[index + 1] if index + 1 < len(headers) else ""
|
|
if "ALLOC" not in flags or not size:
|
|
continue
|
|
if name not in (".text", ".noinit", ".bss"):
|
|
fail(f"flash-resident section {name} ({size} bytes): the image must be .text alone")
|
|
|
|
# Relink at a different base and compare the bytes.
|
|
with tempfile.TemporaryDirectory() as work:
|
|
elsewhere = text_start - 0x200 if text_start >= 0x200 else text_start + 0x200
|
|
images = []
|
|
for base, tag in ((text_start, "here"), (elsewhere, "there")):
|
|
relinked = os.path.join(work, f"{tag}.elf")
|
|
binary = os.path.join(work, f"{tag}.bin")
|
|
subprocess.run(
|
|
[cxx, f"-mmcu={mcu}", "-nostartfiles", f"-Wl,--section-start=.text={base:#x}",
|
|
"-Wl,--defsym=pureboot_app=0", "-mrelax", obj, "-o", relinked],
|
|
check=True, capture_output=True)
|
|
subprocess.run([objcopy, "-O", "binary", relinked, binary], check=True)
|
|
images.append(open(binary, "rb").read())
|
|
if images[0] != images[1]:
|
|
differing = [i for i, (a, b) in enumerate(zip(*images)) if a != b]
|
|
fail(f"the image changes when linked at {elsewhere:#x} instead of {text_start:#x}: "
|
|
f"{len(differing)} byte(s) differ, first at offset {differing[0]:#x}")
|
|
|
|
print(f"PI lint: control flow PC-relative, .text only, identical linked at {text_start:#x} and {elsewhere:#x}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|