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 <[email protected]> --- 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

