#!/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 a symbol: address, size (from nm -S), and name Symbol = namedtuple('Symbol', ['address', 'size', 'name']) # Information about the gap between two consecutive symbols Gap = namedtuple('Gap', ['gap', 'prev_sym', 'next_sym', 'prev_size']) # Start and end marker addresses for a list Markers = namedtuple('Markers', ['start', 'end']) # Holds all the analysis results from checking the lists Results = namedtuple('Results', [ 'total_problems', 'total_symbols', 'all_lines', 'max_name_len', 'list_count']) def eprint(*args, **kwargs): """Print to stderr""" print(*args, file=sys.stderr, **kwargs) def check_single_list(name, symbols, max_name_len, marker_info=None): """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 Symbol tuples, sorted by address max_name_len (int): The max length of list names for column formatting marker_info (Markers): Optional namedtuple with start and end addresses Returns: tuple: (problem_count, list_of_output_lines) """ lines = [] if len(symbols) < 2: return 0, [] gaps = [] for i in range(len(symbols) - 1): sym1, sym2 = symbols[i], symbols[i+1] gaps.append(Gap(gap=sym2.address - sym1.address, prev_sym=sym1.name, next_sym=sym2.name, prev_size=sym1.size)) expected_gap = mode(g.gap for g in gaps) problem_count = 0 hex_gap = f'0x{expected_gap:x}' line = f'{name:<{max_name_len + 2}} {len(symbols):>12} {hex_gap:>17}' lines.append(line) 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}') elif g.prev_size and g.gap > g.prev_size: # Gap is larger than symbol size - padding was inserted problem_count += 1 lines.append( f' - Padding: gap 0x{g.gap:x} > size 0x{g.prev_size:x}' f' before: {g.next_sym}') # Check if start/end marker span is a multiple of the struct size # If not, pointer subtraction (end - start) will produce wrong results # due to compiler optimization using magic number multiplication if marker_info: total_span = marker_info.end - marker_info.start if total_span % expected_gap != 0: problem_count += 1 remainder = total_span % expected_gap lines.append( f' - Pointer arithmetic bug: span 0x{total_span:x} is not a ' f'multiple of struct size 0x{expected_gap:x} ' f'(remainder: {remainder})') return problem_count, lines def run_nm_and_get_lists(elf_path): """Run `nm -S` and parse the output to discover all linker lists Args: elf_path (str): The path to the ELF file to process Returns: tuple or None: (lists_dict, markers_dict) or None on error lists_dict: entries keyed by base_name markers_dict: start/end marker addresses keyed by base_name """ cmd = ['nm', '-S', '-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 # Pattern to match _2_ entries (the actual list elements) entry_pattern = re.compile( r'^(?P_u_boot_list_\d+_\w+)(?:_info)?_2_') # Pattern to match _1 (start) and _3 (end) markers marker_pattern = re.compile( r'^(?P_u_boot_list_\d+_\w+)_(?P[13])$') lists = defaultdict(list) markers = defaultdict(dict) # {base_name: {'start': addr, 'end': addr}} for line in proc.stdout.splitlines(): if '_u_boot_list_' not in line: continue try: parts = line.strip().split() name = parts[-1] address = int(parts[0], 16) # Size is present if we have 4 parts and parts[2] is a single char if len(parts) == 4 and len(parts[2]) == 1: size = int(parts[1], 16) else: size = 0 # Size not available # Check for entry (_2_) symbols - must be uppercase D if ' D _u_boot_list_' in line: match = entry_pattern.match(name) if match: base_name = match.group('base_name') lists[base_name].append(Symbol(address, size, name)) continue # Check for marker (_1 or _3) symbols - can be any type match = marker_pattern.match(name) if match: base_name = match.group('base_name') marker_type = match.group('marker') if marker_type == '1': markers[base_name]['start'] = address else: # marker_type == '3' markers[base_name]['end'] = address except (ValueError, IndexError): eprint(f'Warning: Could not parse line: {line}') # Convert marker dicts to Markers namedtuples (only if both start/end exist) marker_tuples = {} for base_name, m in markers.items(): if 'start' in m and 'end' in m: marker_tuples[base_name] = Markers(m['start'], m['end']) return lists, marker_tuples def collect_data(lists, markers): """Collect alignment check data for all lists Args: lists (dict): A dictionary of lists and their symbols markers (dict): A dictionary of start/end marker addresses per list Returns: Results: A namedtuple containing the analysis results """ if markers is None: markers = {} names = {} prefix_to_strip = '_u_boot_list_2_' for list_name in lists.keys(): name = list_name[len(prefix_to_strip):] if list_name.startswith( prefix_to_strip) else list_name names[list_name] = name max_name_len = max(len(name) for name in names.values()) if names else 0 total_problems = 0 total_symbols = 0 all_lines = [] for list_name in sorted(lists.keys()): symbols = lists[list_name] total_symbols += len(symbols) name = names[list_name] marker_info = markers.get(list_name) problem_count, lines = check_single_list(name, symbols, max_name_len, marker_info) 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 header = (f"{'List Name':<{results.max_name_len + 2}} {'# Symbols':>12} " f"{'Struct Size (hex)':>17}") sep = f"{'-' * (results.max_name_len + 2)} {'-' * 12} {'-' * 17}" eprint(header) eprint(sep) for line in results.all_lines: eprint(line) # Print footer eprint(f"{'-' * (results.max_name_len + 2)} {'-' * 12}") eprint(f"{f'{results.list_count} lists':<{results.max_name_len + 2}} " f"{results.total_symbols:>12}") 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. Problems detected (cause build failure): 1. Inconsistent gaps: Elements in a list should all be separated by the same number of bytes (the struct size). If the linker inserts padding between some elements but not others, this is detected and reported. 2. Padding detection: Using symbol sizes from nm -S, the script compares each symbol's size to the gap after it. If gap > size, the linker inserted padding, which breaks U-Boot's assumption that the list is a contiguous array of same-sized structs. 3. Pointer arithmetic bugs: Each list has start (_1) and end (_3) markers. If the span (end - start) is not a multiple of struct size, pointer subtraction produces garbage due to GCC's magic-number division. ''' 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='path_to_elf_file', 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() result = run_nm_and_get_lists(args.elf_path) if result is None: return 2 # Error running nm lists, markers = result if not lists: if args.verbose: eprint('Success: No U-Boot linker lists found to check') return 0 results = collect_data(lists, markers) show_output(results, args.verbose) return 3 if results.total_problems > 0 else 0 if __name__ == '__main__': sys.exit(main())