When linker-inserted padding breaks list integrity, pointer arithmetic like (end - start) / sizeof(struct) produces garbage. GCC optimizes division by constants using multiplicative inverses, which only works when the dividend is an exact multiple. With padding, outputs like "Running -858993444 bloblist tests" appear instead of the correct count. Enhance the linker list checking script to detect these problems by adding symbol size tracking using nm -S. This enables: 1. Padding detection: Compare symbol sizes to gaps. If gap > size, padding was inserted, breaking contiguous array assumptions. 2. Pointer arithmetic bugs: Check if (end - start) marker span is a multiple of struct size. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com>
299 lines
11 KiB
Python
Executable File
299 lines
11 KiB
Python
Executable File
#!/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<base_name>_u_boot_list_\d+_\w+)(?:_info)?_2_')
|
|
# Pattern to match _1 (start) and _3 (end) markers
|
|
marker_pattern = re.compile(
|
|
r'^(?P<base_name>_u_boot_list_\d+_\w+)_(?P<marker>[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())
|