pureboot 5: one command pair for every memory, and a clock-free backend
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>
This commit is contained in:
@@ -1,47 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Position-independence lint: the two link-time facts that let the identical
|
||||
image run from any slot, asserted from the built ELF.
|
||||
"""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. The info block within the image's first 256 bytes: 'b' rebuilds its
|
||||
address as (running slot high byte : link address low byte).
|
||||
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> <nm> <elf> <text_start_hex>
|
||||
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, nm, elf, text_start = sys.argv[1:]
|
||||
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)
|
||||
]
|
||||
absolute = [line for line in listing.splitlines() if re.search(r"\t(jmp|call)\t", line)]
|
||||
if absolute:
|
||||
print("FAIL: absolute control flow in the image:")
|
||||
print("\n".join(absolute))
|
||||
sys.exit(1)
|
||||
fail("absolute control flow in the image:\n" + "\n".join(absolute))
|
||||
|
||||
symbols = subprocess.run([nm, "-C", elf], capture_output=True, text=True, check=True).stdout
|
||||
info = [line for line in symbols.splitlines() if "flash_table" in line and "::storage" in line]
|
||||
if len(info) != 1:
|
||||
print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
|
||||
sys.exit(1)
|
||||
address = int(info[0].split()[0], 16)
|
||||
offset = address - text_start
|
||||
if not 0 <= offset < 256:
|
||||
print(f"FAIL: info block at image offset {offset:#x}, must sit in the first 256 bytes")
|
||||
sys.exit(1)
|
||||
# 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")
|
||||
|
||||
print(f"PI lint: control flow PC-relative, info block at offset {offset:#x}")
|
||||
# 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__":
|
||||
|
||||
Reference in New Issue
Block a user