Files
kaptan/firmware/scripts/analyze-fw.py
T

266 lines
8.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Validate and show flash, static RAM and ELF-section use.
Usage:
scripts/analyze-fw.py <firmware.elf> <memory.x>
"""
import json
import re
from collections import namedtuple
from pathlib import Path
import subprocess
import sys
REGION = re.compile(
r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*:\s*"
r"ORIGIN\s*=\s*([^,]+),\s*LENGTH\s*=\s*([^\s/}]+)",
re.MULTILINE,
)
Section = namedtuple("Section", "name size address allocated")
LoadSegment = namedtuple(
"LoadSegment", "virtual_address physical_address file_size memory_size"
)
def number(text):
"""Parse linker numbers such as 0x2001_0000, 176K, and 1M."""
text = text.strip().replace("_", "")
scale = {"K": 1024, "M": 1024 * 1024}.get(text[-1:].upper(), 1)
return int(text[:-1] if scale != 1 else text, 0) * scale
def run(command):
"""Run a tool and return stdout."""
return subprocess.run(command, check=True, text=True, capture_output=True).stdout
def llvm_tool(name):
"""Find one LLVM binary installed by the active Rust toolchain."""
sysroot = run(["rustc", "--print", "sysroot"]).strip()
host = next(
line.split(": ", 1)[1]
for line in run(["rustc", "-vV"]).splitlines()
if line.startswith("host: ")
)
tool = Path(sysroot) / "lib" / "rustlib" / host / "bin" / name
if not tool.is_file():
raise RuntimeError(
f"{name} is unavailable; install it with: rustup component add llvm-tools-preview"
)
return tool
def regions(memory_x):
"""Return every named linker region as (name, origin, length)."""
return [
(name, number(origin), number(length))
for name, origin, length in REGION.findall(memory_x.read_text())
]
def elf_layout(output):
"""Return allocated-section and load-segment metadata from llvm-readobj JSON."""
documents = json.loads(output)
if len(documents) != 1:
raise ValueError("llvm-readobj returned no unique ELF document")
document = documents[0]
section_list = []
for wrapped in document.get("Sections", []):
section = wrapped["Section"]
flags = {flag["Name"] for flag in section["Flags"]["Flags"]}
section_list.append(
Section(
section["Name"]["Name"],
section["Size"],
section["Address"],
"SHF_ALLOC" in flags,
)
)
load_segments = []
for wrapped in document.get("ProgramHeaders", []):
segment = wrapped["ProgramHeader"]
if segment["Type"]["Name"] == "PT_LOAD":
load_segments.append(
LoadSegment(
segment["VirtualAddress"],
segment["PhysicalAddress"],
segment["FileSize"],
segment["MemSize"],
)
)
return section_list, load_segments
def region_for(address, layout):
"""Return the linker region containing an address, if any."""
for name, origin, length in layout:
if origin <= address < origin + length:
return name
return "outside"
def region_for_span(address, size, layout):
"""Return the one linker region containing the complete nonempty span."""
end = address + size
for name, origin, length in layout:
if origin <= address and end <= origin + length:
return name
return None
def merged_span_size(spans):
"""Return bytes covered by the union of half-open address spans."""
merged = []
for start, end in sorted(spans):
if not merged or start > merged[-1][1]:
merged.append([start, end])
else:
merged[-1][1] = max(merged[-1][1], end)
return sum(end - start for start, end in merged)
def region_usage(name, layout, section_list, load_segments):
"""Return runtime or load-image bytes occupying one linker region.
Allocated-section VMAs account for runtime RAM, while PT_LOAD physical file
spans account for initializers stored in flash (notably a future `.data`).
Taking their union avoids counting ordinary flash sections twice.
"""
spans = [
(section.address, section.address + section.size)
for section in section_list
if section.allocated
and section.size
and region_for_span(section.address, section.size, layout) == name
]
spans.extend(
(
segment.physical_address,
segment.physical_address + segment.file_size,
)
for segment in load_segments
if segment.file_size
and region_for_span(segment.physical_address, segment.file_size, layout) == name
)
return merged_span_size(spans)
def validation_errors(layout, section_list, load_segments):
"""Return every placement error that makes a raw firmware image unsafe."""
errors = []
for section in section_list:
if not section.allocated or section.size == 0:
continue
region = region_for_span(section.address, section.size, layout)
if region is None:
errors.append(
f"allocated section {section.name} spans "
f"0x{section.address:08X}..0x{section.address + section.size:08X} "
"outside declared memory"
)
for index, segment in enumerate(load_segments):
if segment.memory_size and region_for_span(
segment.virtual_address, segment.memory_size, layout
) is None:
errors.append(
f"PT_LOAD[{index}] virtual span "
f"0x{segment.virtual_address:08X}.."
f"0x{segment.virtual_address + segment.memory_size:08X} "
"is outside declared memory"
)
if segment.file_size and region_for_span(
segment.physical_address, segment.file_size, layout
) is None:
errors.append(
f"PT_LOAD[{index}] physical span "
f"0x{segment.physical_address:08X}.."
f"0x{segment.physical_address + segment.file_size:08X} "
"is outside declared memory"
)
return errors
def main():
if len(sys.argv) != 3:
raise ValueError(f"usage: {sys.argv[0]} <firmware.elf> <memory.x>")
elf = Path(sys.argv[1])
memory_x = Path(sys.argv[2])
if not elf.is_file():
raise ValueError(f"ELF does not exist: {elf}")
if not memory_x.is_file():
raise ValueError(f"linker layout does not exist: {memory_x}")
layout = regions(memory_x)
if not layout:
raise ValueError(f"no MEMORY regions found in: {memory_x}")
readobj = llvm_tool("llvm-readobj")
section_list, load_segments = elf_layout(
run(
[
str(readobj),
"--sections",
"--program-headers",
"--elf-output-style=JSON",
str(elf),
]
)
)
if not section_list:
raise ValueError(f"no sections found in: {elf}")
if not load_segments:
raise ValueError(f"no loadable segments found in: {elf}")
print(f"\nFirmware: {elf}")
print(f"ELF file size: {elf.stat().st_size:,} bytes")
print("\nLinker regions:")
print(" Region Origin Used / Capacity Remaining")
for name, origin, length in layout:
used = region_usage(name, layout, section_list, load_segments)
remaining = length - used
print(
f" {name:<12} 0x{origin:08X} {used / 1024:>7.2f} KiB /"
f" {length / 1024:>7.2f} KiB {remaining / 1024:>7.2f} KiB"
f" ({100 * used / length:5.2f}%)"
)
print("\nELF sections:")
print(" Region Address Size Section")
for section in sorted(section_list, key=lambda item: item.address):
if section.size:
location = (
region_for(section.address, layout)
if section.allocated
else "non-alloc"
)
print(
f" {location:<12} "
f"0x{section.address:08X} {section.size:>10,} {section.name}"
)
errors = validation_errors(layout, section_list, load_segments)
if errors:
raise ValueError("firmware memory validation failed:\n " + "\n ".join(errors))
if __name__ == "__main__":
try:
main()
except (
IndexError,
KeyError,
OSError,
RuntimeError,
TypeError,
ValueError,
subprocess.CalledProcessError,
) as error:
print(f"error: {error}", file=sys.stderr)
raise SystemExit(1) from error