[RFC PATCH 1/2] scripts: Add a script check consistency of linker lists
Simon Glass
sjg at chromium.org
Tue May 26 00:26:31 CEST 2026
If linker lists have inconsistent alignment it can cause strange
runtime errors. Add a script that can detect and report these problems.
Signed-off-by: Simon Glass <simon.glass at canonical.com>
---
Makefile | 5 +
scripts/check_linker_lists.py | 215 ++++++++++++++++++++++++++++++++++
2 files changed, 220 insertions(+)
create mode 100755 scripts/check_linker_lists.py
diff --git a/Makefile b/Makefile
index f07faada3d5..fe8df271c38 100644
--- a/Makefile
+++ b/Makefile
@@ -1389,6 +1389,10 @@ define deprecated
endef
+# Check linker lists are consistent
+quiet_cmd_llcheck = LLCHK $2
+cmd_llcheck = $(srctree)/scripts/check_linker_lists.py $2
+
# Timestamp file to make sure that binman always runs
.binman_stamp: $(INPUTS-y) FORCE
ifeq ($(CONFIG_BINMAN),y)
@@ -2114,6 +2118,7 @@ ifeq ($(CONFIG_KALLSYMS),y)
$(call cmd,smap)
$(call cmd,u-boot__) common/system_map.o
endif
+ $(call cmd,llcheck,u-boot)
ifeq ($(CONFIG_RISCV),y)
@tools/prelink-riscv $@
diff --git a/scripts/check_linker_lists.py b/scripts/check_linker_lists.py
new file mode 100755
index 00000000000..46ff4465989
--- /dev/null
+++ b/scripts/check_linker_lists.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0+
+"""Check alignment of U-Boot linker lists.
+
+Auto-discover and verify the uniform spacing of all U-Boot linker list symbols.
+
+Analyze the symbol table of a U-Boot ELF file to ensure that all entries in all
+linker-generated lists are separated by a consistent number of bytes. Detect
+problems caused by linker-inserted alignment padding.
+
+By default, produce no output if no problems are found.
+Use the -v flag to force output even on success.
+
+Exit Codes:
+ 0: Success - no alignment problems were found
+ 1: Usage Error - the script was not called with the correct arguments
+ 2: Execution Error - failed to run 'nm' or the ELF file was not found
+ 3: Problem Found - an inconsistent gap was detected in at least one list
+"""
+
+import sys
+import subprocess
+import re
+import argparse
+from statistics import mode
+from collections import defaultdict, namedtuple
+
+# Information about the gap between two consecutive symbols
+Gap = namedtuple('Gap', ['gap', 'prev_sym', 'next_sym'])
+# Holds all the analysis results from checking the lists
+Results = namedtuple('Results', [
+ 'total_problems', 'total_symbols', 'all_lines', 'max_name_len',
+ 'list_count'])
+
+# Column widths used by the report. NAME_PAD is added to the longest list name
+# to leave a small gutter before the next column.
+NAME_PAD = 2
+SYM_COL = 12
+SIZE_COL = 17
+
+# Matches a list entry symbol (the '_2_' infix marks the actual list elements,
+# as opposed to the '_1' start and '_3' end markers).
+ENTRY_PATTERN = re.compile(r'^_u_boot_list_\d+_(?P<base_name>\w+)_2_')
+
+def eprint(*args, **kwargs):
+ """Print to stderr"""
+ print(*args, file=sys.stderr, **kwargs)
+
+def check_single_list(name, symbols, max_name_len):
+ """Check alignment for a single list and return its findings
+
+ Args:
+ name (str): The cleaned-up name of the list for display
+ symbols (list): A list of (address, name) tuples, sorted by address
+ max_name_len (int): The max length of list names for column formatting
+
+ Returns:
+ tuple: (problem_count, list_of_output_lines)
+ """
+ if len(symbols) < 2:
+ return 0, []
+
+ gaps = []
+ for i in range(len(symbols) - 1):
+ addr1, name1 = symbols[i]
+ addr2, name2 = symbols[i + 1]
+ gaps.append(Gap(gap=addr2 - addr1, prev_sym=name1, next_sym=name2))
+
+ expected_gap = mode(g.gap for g in gaps)
+ lines = [f'{name:<{max_name_len + NAME_PAD}} {len(symbols):>{SYM_COL}} '
+ f'{f"0x{expected_gap:x}":>{SIZE_COL}}']
+
+ problem_count = 0
+ for g in gaps:
+ if g.gap != expected_gap:
+ problem_count += 1
+ lines.append(
+ f' - Bad gap (0x{g.gap:x}) before symbol: {g.next_sym}')
+
+ return problem_count, lines
+
+def run_nm_and_get_lists(elf_path):
+ """Run 'nm' and parse the output to discover all linker lists
+
+ Args:
+ elf_path (str): The path to the ELF file to process
+
+ Returns:
+ dict or None: A dictionary of discovered lists, or None on error
+ """
+ cmd = ['nm', '-n', elf_path]
+ try:
+ proc = subprocess.run(cmd, capture_output=True, text=True, check=True)
+ except FileNotFoundError:
+ eprint(
+ "Error: The 'nm' command was not found. "
+ 'Please ensure binutils is installed')
+ return None
+ except subprocess.CalledProcessError as e:
+ eprint(
+ f"Error: Failed to execute 'nm' on '{elf_path}'.\n"
+ f' Return Code: {e.returncode}\n Stderr:\n{e.stderr}')
+ return None
+
+ lists = defaultdict(list)
+ for line in proc.stdout.splitlines():
+ if '_u_boot_list_' not in line:
+ continue
+ try:
+ parts = line.strip().split()
+ address, name = int(parts[0], 16), parts[-1]
+ match = ENTRY_PATTERN.match(name)
+ if match:
+ lists[match.group('base_name')].append((address, name))
+ except (ValueError, IndexError):
+ eprint(f'Warning: Could not parse line: {line}')
+
+ return lists
+
+def collect_data(lists):
+ """Collect alignment check data for all lists
+
+ Args:
+ lists (dict): A dictionary of lists and their symbols
+
+ Returns:
+ Results: A namedtuple containing the analysis results
+ """
+ max_name_len = max((len(n) for n in lists), default=0)
+
+ total_problems = 0
+ total_symbols = 0
+ all_lines = []
+ for list_name in sorted(lists):
+ symbols = lists[list_name]
+ total_symbols += len(symbols)
+ problem_count, lines = check_single_list(list_name, symbols,
+ max_name_len)
+ total_problems += problem_count
+ all_lines.extend(lines)
+
+ return Results(
+ total_problems=total_problems,
+ total_symbols=total_symbols,
+ all_lines=all_lines,
+ max_name_len=max_name_len,
+ list_count=len(lists))
+
+def show_output(results, verbose):
+ """Print the collected results to stderr based on verbosity
+
+ Args:
+ results (Results): The analysis results from collect_data()
+ verbose (bool): True to print output even on success
+ """
+ if results.total_problems == 0 and not verbose:
+ return
+
+ name_col = results.max_name_len + NAME_PAD
+ eprint(f'{"List Name":<{name_col}} {"# Symbols":>{SYM_COL}} '
+ f'{"Struct Size (hex)":>{SIZE_COL}}')
+ eprint(f'{"-" * name_col} {"-" * SYM_COL} {"-" * SIZE_COL}')
+ for line in results.all_lines:
+ eprint(line)
+
+ # Print footer
+ eprint(f'{"-" * name_col} {"-" * SYM_COL}')
+ eprint(f'{f"{results.list_count} lists":<{name_col}} '
+ f'{results.total_symbols:>{SYM_COL}}')
+
+ if results.total_problems > 0:
+ eprint(f'\nFAILURE: Found {results.total_problems} alignment problems')
+ elif verbose:
+ eprint('\nSUCCESS: All discovered lists have consistent alignment')
+
+def main():
+ """Main entry point of the script, returns an exit code"""
+ epilog_text = '''
+Auto-discover all linker-generated lists in a U-Boot ELF file
+(e.g., for drivers, commands, etc.) and verify their integrity. Check
+that all elements in a given list are separated by a consistent number of
+bytes.
+
+Problems typically indicate that the linker has inserted alignment padding
+between two elements in a list, which can break U-Boot's assumption that the
+list is a simple, contiguous array of same-sized structs.
+'''
+ parser = argparse.ArgumentParser(
+ description='Check alignment of U-Boot linker lists in an ELF file.',
+ epilog=epilog_text,
+ formatter_class=argparse.RawDescriptionHelpFormatter
+ )
+ parser.add_argument('elf_path', metavar='ELF',
+ help='Path to the U-Boot ELF file to check')
+ parser.add_argument('-v', '--verbose', action='store_true',
+ help='Print detailed output even on success')
+
+ args = parser.parse_args()
+
+ lists = run_nm_and_get_lists(args.elf_path)
+ if lists is None:
+ return 2 # Error running nm
+
+ if not lists:
+ if args.verbose:
+ eprint('Success: No U-Boot linker lists found to check')
+ return 0
+
+ results = collect_data(lists)
+ show_output(results, args.verbose)
+
+ return 3 if results.total_problems > 0 else 0
+
+if __name__ == '__main__':
+ sys.exit(main())
--
2.43.0
More information about the U-Boot
mailing list