check_pi.py asserts the two link-time facts position independence rests on (no absolute jmp/call; the info block within the image's first 256 bytes); the size gates drop to 510 on the tinies for the trampoline word. New per-chip tests beside the reworked protocol test: the planner units (programming orders and their recovery properties, the surgery, staging composition, boot-fuse decode, and the update preflight's error/warning matrix over synthetic fuse bytes), the relocated-copy sweep (the identical image installed one slot lower serves the full command set — the PI acceptance test, and the one that caught the temporary-buffer trap), and the self-update end-to-end: --update-loader to a re-timed build (pureboot9, byte-different by PUREBOOT_TIMEOUT alone), then every power-fail phase killed mid-write, restarted from the runner's flash dump, and completed by a re-run with the application intact throughout. The mega rounds run the BOOTRST-unprogrammed profile: the fixture application's 'L' jump is the application-owned loader entry that profile relies on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
54 lines
1.9 KiB
Python
54 lines
1.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Position-independence lint for the pureboot image.
|
|
|
|
The self-staging design lets the identical binary run from any 512-byte
|
|
slot, which holds only if nothing in the image addresses itself absolutely.
|
|
Two link-time facts guarantee it, both asserted here from the built ELF:
|
|
|
|
1. No absolute jmp/call opcodes — all control flow is PC-relative
|
|
(rjmp/rcall/ijmp/icall). -mrelax normally guarantees this; a code
|
|
change that grows a branch out of relaxation range would break it
|
|
silently.
|
|
2. The info block sits within the image's first 256 bytes: the 'b'
|
|
command rebuilds its address as (running slot high byte : low byte of
|
|
the link address), which needs the offset to fit that low byte.
|
|
|
|
Usage: check_pi.py <objdump> <nm> <elf> <text_start_hex>
|
|
"""
|
|
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
def main():
|
|
objdump, nm, elf, 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:
|
|
print("FAIL: absolute control flow in the image:")
|
|
print("\n".join(absolute))
|
|
sys.exit(1)
|
|
|
|
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)
|
|
offset = int(info[0].split()[0], 16) - 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)
|
|
|
|
print(f"PI lint: control flow PC-relative, info block at offset {offset:#x}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|