summaryrefslogtreecommitdiff
path: root/build-aux/lint-bin
diff options
context:
space:
mode:
Diffstat (limited to 'build-aux/lint-bin')
-rwxr-xr-xbuild-aux/lint-bin142
1 files changed, 142 insertions, 0 deletions
diff --git a/build-aux/lint-bin b/build-aux/lint-bin
new file mode 100755
index 0000000..91f1612
--- /dev/null
+++ b/build-aux/lint-bin
@@ -0,0 +1,142 @@
+#!/usr/bin/env bash
+# build-aux/lint-bin - Lint final binary images
+#
+# Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+# 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 "$@"