#!/usr/bin/env python3 import argparse import os import re import subprocess import urllib.parse # ELF analysis def loadable_sections(elf_file): objdump_section_headers_pattern = re.compile("^\s+\d+\s+(\.[\w\.]+)\s+([0-9a-f]+)\s+([0-9a-f]+)\s+([0-9a-f]+)\s+([0-9a-f]+)", flags=re.MULTILINE) objdump_output = subprocess.check_output(["arm-none-eabi-objdump", "-h", "-w", elf_file]).decode('utf-8') sections = [] for (name, size, vma, lma, offset) in re.findall(objdump_section_headers_pattern, objdump_output): int_size = int(size,16) if (int_size > 0): sections.append({'name': name, 'size': int_size, 'vma': int(vma,16), 'lma': int(lma,16), 'offset':int(offset,16)}) return sections # Data filtering def row_for_elf(elf, requested_section_prefixes): sections = loadable_sections(elf) result = {} for prefix in requested_section_prefixes: for s in sections: section_name = s['name'] if s['name'].startswith(prefix): if not prefix in result: result[prefix] = 0 result[prefix] += s['size'] return result # String formatting def iso_separate(string): space = '' # We may want to use a thin non-breaking space as thousands separator return string.replace('_',space).replace('+','+'+space).replace('-','-'+space) def format_bytes(value, force_sign=False): if value is None: return '' number_format = '{:' if force_sign: number_format += '+' number_format += '_} bytes' return iso_separate(number_format.format(value)) def format_percentage(value): if value is None: return '' return iso_separate("{:+.1f} %".format(100*value)) # Markdown def emphasize(string): if string: return '_' + string + '_' else: return '' def strong(string): if string: return '**' + string + '**' else: return '' # Deltas def absolute_delta(x,y): if x is None or y is None: return None return x-y def ratio_delta(x,y): if x is None or y is None: return None return (x-y)/y # Table formatting def format_row(row, header=False): result = '|' if header: result += strong(header) result += '|' for v in row: result += v result += '|' result += '\n' return result def format_table(table): base = table[0]['values'] listed_sections = base.keys() result = '' result += format_row(listed_sections) result += '|-|' + '-:|'*len(listed_sections) + '\n' for i,row in enumerate(table): v = row['values'] result += format_row((format_bytes(v.get(s)) for s in listed_sections), header=row['label']) if i != 0: result += format_row(emphasize(format_bytes(absolute_delta(v.get(s), base.get(s)), force_sign=True)) for s in listed_sections) result += format_row(emphasize(format_percentage(ratio_delta(v.get(s), base.get(s)))) for s in listed_sections) return result # Argument parsing parser = argparse.ArgumentParser(description='Compute binary size metrics') parser.add_argument('files', type=str, nargs='+', help='an ELF file') parser.add_argument('--labels', type=str, nargs='+', help='label for ELF file') parser.add_argument('--sections', type=str, nargs='+', help='Section (prefix) to list') parser.add_argument('--escape', action='store_true', help='Escape the output') parser.add_argument('--custom', type=str, action='append', nargs='+', help='Custom sections, made from the addition of other sections.') args = parser.parse_args() # Execution table = [] for i,filename in enumerate(args.files): label = os.path.basename(filename) if args.labels and i < len(args.labels): label = args.labels[i] values = row_for_elf(filename, args.sections) for custom_section in args.custom: if (len(custom_section) >= 2): custom_section_size = 0 for i in range(len(custom_section) - 1): custom_section_size += values[custom_section[i + 1]] values[custom_section[0]] = custom_section_size table.append({'label': label, 'values': values}) formatted_table = format_table(table) if args.escape: print(urllib.parse.quote(formatted_table, safe='| :*+()')) else: print(formatted_table)