#!/usr/bin/env bash # build-aux/lint-bin - Lint final binary images # # Copyright (C) 2025 Luke T. Shumaker # SPDX-License-Identifier: AGPL-3.0-or-later set -euE -o pipefail shopt -s extglob # There are several exisi files we can use: # # Binaries: # - ${elf} : firmware image with debug symbols and relocationd data # - ${elf%.elf}.bin : raw firmware image # - ${elf%.elf}.hex : .bin as Intel HEX # - ${elf%.elf}.uf2 : .bin as USB Flashing Format (UF2) # # Textual info: # - ${elf%.elf}.dis : `objdump --section-headers ${elf}; objdump --disassemble ${elf}; picotool coprodis --quiet ${elf}` # - ${elf}.map : `ld --print-map` info # - ${elf%.elf}_stack.c : `stack.c.gen` RED=$(tput setaf 1) RESET=$(tput sgr0) err() { printf "${RED}%s${RESET}: %s\n" "$1" "$2" >&2 r=1 } # Input is `ld --print-map` format. # # Output is a series of lines in the format "symbol location size # source". Whitespace may seem silly. objdump_globals() { sed -E -n '/^ \.t?(data|bss)\./{ / 0x/{ p; D; }; N; s/\n/ /; p; }' <"$1" } readelf_funcs() { local in_elffile in_elffile=$1 readelf --syms --wide -- "$in_elffile" | awk '$4 == "FUNC" { print $8 }' } lint_globals() { local in_mapfile in_mapfile=$1 local rel_base rel_base=${in_mapfile#build/*/} rel_base=${rel_base%/*} local topdir topdir=$PWD { echo 'Source Symbol Size' objdump_globals "$in_mapfile" | { cd "$rel_base" total=0 while read -r symbol addr size source; do if (( addr == 0 )); then continue fi case "$source" in /*) # libg.a(whatever.o) -> libg.a source="${source%(*)}" # resolve `..` components source="$(realpath --canonicalize-missing --no-symlinks -- "$source")" ;; CMakeFiles/*.dir/*.@(obj|o)) # CMakeFiles/sbc_harnes_objs.dir/... source="${source#CMakeFiles/*.dir/}" source="${source%.@(obj|o)}" source="${source//__/..}" source="$(realpath --canonicalize-missing --no-symlinks --relative-to="$topdir" -- "$source")" ;; esac printf "%s %s 0x%04x (%'d)\n" "$source" "$symbol" "$size" "$size" total=$((total + size)) done printf "~ Total 0x%04x (%'d)\n" "$total" "$total" } | LC_COLLATE=C sort } | column -t } lint_stack() { local in_elffile in_elffile=$1 IFS='' while read -r line; do func=${line#$'\t'} if [[ $line == $'\t'* ]]; then err "$in_elffile" "function in binary but not _stack.c: ${func}" else err "$in_elffile" "function in _stack.c but not binary: ${func}" fi done < <( comm -3 \ <(sed -En 's/^included: (.*:)?//p' "${in_elffile%.elf}_stack.c" | sort -u) \ <(readelf_funcs "$in_elffile" | sed -E -e 's/\.part\.[0-9]*$//' -e 's/^__(.*)_veneer$/\1/' | sort -u)) } lint_func_blocklist() { local in_elffile in_elffile=$1 local blocklist=( gpio_default_irq_handler ) while read -r func; do err "$in_elffile" "Contains blocklisted function: ${func}" done < <(readelf --syms --wide -- "$in_elffile" | awk '$4 == "FUNC" { print $8 }' | grep -Fx "${blocklist[@]/#/-e}") } main() { r=0 local elf for elf in "$@"; do { echo 'Global variables:' lint_globals "${elf}.map" | sed 's/^/ /' } > "${elf%.elf}.lint.globals" (lint_stack "$elf") &> "${elf%.elf}.lint.stack" lint_func_blocklist "$elf" done return $r } main "$@"