diff --git a/lib/lv_drv_tft_espi/tft_espi_drv.cpp b/lib/lv_drv_tft_espi/tft_espi_drv.cpp index 0eab2dd29..2b51d4622 100644 --- a/lib/lv_drv_tft_espi/tft_espi_drv.cpp +++ b/lib/lv_drv_tft_espi/tft_espi_drv.cpp @@ -33,7 +33,7 @@ * STATIC PROTOTYPES **********************/ static void tftShowConfig(TFT_eSPI & tft); -static void tftShowLogo(TFT_eSPI & tft); +static inline void tftShowLogo(TFT_eSPI & tft); /********************** * STATIC VARIABLES @@ -242,7 +242,7 @@ static void tftShowConfig(TFT_eSPI & tft) } } -static void tftShowLogo(TFT_eSPI & tft) +static inline void tftShowLogo(TFT_eSPI & tft) { tft.fillScreen(TFT_DARKCYAN); int x = (tft.width() - logoWidth) / 2; diff --git a/platformio.ini b/platformio.ini index bd4cd4ad2..7b671f8fd 100644 --- a/platformio.ini +++ b/platformio.ini @@ -130,7 +130,10 @@ ps_ram = -mfix-esp32-psram-cache-issue extra_scripts = + ${env.extra_scripts} tools/copy_partitions.py + tools/analyze_elf.py + ; -- The ESP32 has 2 SPI Hardware Busses available to use: vspi = diff --git a/tools/analyze_elf.py b/tools/analyze_elf.py new file mode 100644 index 000000000..6bfcf9112 --- /dev/null +++ b/tools/analyze_elf.py @@ -0,0 +1,25 @@ +Import("env") + + +#env.AddCustomTarget("my_test", None, 'python C:\Users\fvanroie\.platformio\packages\toolchain-xtensa32\bin\elf-size-analyze.py -a -H -w 120 $BUILD_DIR\${PROGNAME} -F -m 250') + +# Add custom target to explorer +env.AddTarget( + name = "analyze_ram", + dependencies = "$BUILD_DIR\${PROGNAME}${PROGSUFFIX}", + actions = 'python $PROJECT_DIR/tools/elf-size-analyze.py $BUILD_DIR\${PROGNAME}${PROGSUFFIX} -t $PROJECT_PACKAGES_DIR\\toolchain-xtensa32\\bin\\xtensa-esp32-elf- -a -H -w 120 -R -m 512', + title = "Analyze RAM", + description = "Build and analyze", + group="Analyze" +) +env.AddTarget( + name = "analyze_flash", + dependencies = "$BUILD_DIR\${PROGNAME}${PROGSUFFIX}", + actions = 'python $PROJECT_DIR/tools/elf-size-analyze.py $BUILD_DIR\${PROGNAME}${PROGSUFFIX} -t $PROJECT_PACKAGES_DIR\\toolchain-xtensa32\\bin\\xtensa-esp32-elf- -a -H -w 120 -F -m 512', + title = "Analyze Flash", + description = "Build and analyze", + group="Analyze" +) + + +print('=====================================') diff --git a/tools/elf-size-analyze.py b/tools/elf-size-analyze.py new file mode 100644 index 000000000..f2b1c7e9f --- /dev/null +++ b/tools/elf-size-analyze.py @@ -0,0 +1,1077 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2016, Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +# Based on a script by: +# Chereau, Fabien + +# ^originial comments before my modifications +# This script is based on 'size_report' from Zephyr Project scripts: +# https://github.com/zephyrproject-rtos/zephyr/blob/master/scripts/footprint/size_report +# +# It has been modified to be more flexible for different (also not-bare-metal) ELF files, +# and adds some more data visualization options. Parsing has been updated to use +# regular expressions as it is much more robust solution. + +import os +import re +import sys +import math +import shutil +import logging +import pathlib +import argparse +import itertools +import subprocess +import platform + + +# default logging configuration +log = logging.getLogger('elf-size-analyze') +console = logging.StreamHandler() +formatter = logging.Formatter('[%(levelname)s] %(message)s') +console.setFormatter(formatter) +log.setLevel(logging.ERROR) +log.addHandler(console) + + +def parse_args(): + parser = argparse.ArgumentParser(description=""" +Prints report of memory usage of the given executable. +Shows how different source files contribute to the total size. +Uses inforamtion contained in ELF executable and binutils programs. +For best results the program should be compiled with maximum debugging information +(e.g. GCC flag: `-g`, or more: `-ggdb3`). + """, epilog=""" +This script is based on 'size_report' script from Zephyr Project: +https://github.com/zephyrproject-rtos/zephyr (scripts/footprint/size_report). + """) + + parser.add_argument('elf', metavar='ELF_FILE', + help='path to the examined ELF file') + + memory_group = parser.add_argument_group( + 'Memory type', """ +Specifies memory types for which statistics should be printed. +Choosing at least one of these options is required. +RAM/ROM options may be oversimplifed for some targets, under the hood they just filter the symbols +by sections in the following manner: +sections must have ALLOC flag and: for RAM - have WRITE flag, for ROM - not have NOBITS type. + """) + memory_group.add_argument('-R', '--ram', action='store_true', + help='print RAM statistics') + memory_group.add_argument('-F', '--rom', action='store_true', + help='print ROM statistics ("Flash")') + memory_group.add_argument('-P', '--print-sections', action='store_true', + help='print section headers that can be used for filtering symbols with -S option' + + ' (output is almost identical to `readelf -WS ELF_FILE`)') + memory_group.add_argument('-S', '--use-sections', nargs='+', metavar='NUMBER', + help='manually select sections from which symbols will be used (by number)') + + basic_group = parser.add_argument_group( + 'Basic arguments') + basic_group.add_argument('-t', '--toolchain-triplet', '--toolchain-path', + default='', metavar='PATH', + help='toolchain triplet/path to prepend to binutils program names,' + + ' this is important for examining cross-compiled ELF files,' + + ' e.g `arm-none-eabi-` or `/my/path/arm-none-eabi-` or `/my/path/`') + basic_group.add_argument('-v', '--verbose', action='count', + help='increase verbosity, can be specified up to 3 times' + + ' (versobity levels: ERROR -> WARNING -> INFO -> DEBUG)') + + printing_group = parser.add_argument_group( + 'Printing options', 'Options for changing the output formatting.') + printing_group.add_argument('-w', '--max-width', default=80, type=int, + help='set maximum output width, 0 for unlimited width (default 80)') + printing_group.add_argument('-m', '--min-size', default=0, type=int, + help='do not print symbols with size below this value') + printing_group.add_argument('-f', '--fish-paths', action='store_true', + help='when merging paths, use fish-like method to shrink them') + printing_group.add_argument('-s', '--sort-by-name', action='store_true', + help='sort symbols by name instead of sorting by size') + printing_group.add_argument('-H', '--human-readable', action='store_true', + help='print sizes in human readable format') + printing_group.add_argument('-o', '--files-only', action='store_true', + help='print only files (to be used with cumulative size enabled)') + printing_group.add_argument('-a', '--alternating-colors', action='store_true', + help='use alternating colors when printing symbols') + + printing_group.add_argument('--no-demangle', action='store_true', + help='disable demangling of C++ symbol names') + printing_group.add_argument('--no-merge-paths', action='store_true', + help='disable merging paths in the table') + printing_group.add_argument('--no-color', action='store_true', + help='disable colored output') + printing_group.add_argument('--no-cumulative-size', action='store_true', + help='disable printing of cumulative sizes for paths') + printing_group.add_argument('--no-totals', action='store_true', + help='disable printing the total symbols size') + + args = parser.parse_args() + + return args + + +################################################################################ + +class TreeNode: + """ + Simple implementation of a tree with dynamic number of nodes. + Provides a depth-first iterator. Someone could actually call this + class TreeNode, as every object represents a single node. + """ + + def __init__(self, parent=None): + self.parent = parent + self.children = [] + + def add(self, children): + if not isinstance(children, (list, tuple)): + children = (children, ) + for child in children: + self.children.append(child) + child.parent = self + + def pre_order(self): + """Iterator that yields tuples (node, depth). Depth-first, pre-order traversal.""" + return self.PreOrderIterator(self) + + def post_order(self): + """Iterator that yields tuples (node, depth). Depth-first, post-order traversal.""" + return self.PostOrderIterator(self) + + def __iter__(self): + for child in self.children: + yield child + + class TreeIterator: + def __init__(self, root, depth=0): + self.root = root + self.depth = depth + + def __iter__(self): + raise NotImplementedError('Should yield pairs (node, depth)') + + # depth-first tree iterators + class PreOrderIterator(TreeIterator): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __iter__(self): + yield self.root, self.depth + children_iters = map(lambda child: + self.__class__(child, self.depth + 1), self.root) + for node in itertools.chain(*children_iters): + yield node + + class PostOrderIterator(TreeIterator): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __iter__(self): + children_iters = map(lambda child: + self.__class__(child, self.depth + 1), self.root) + for node in itertools.chain(*children_iters): + yield node + yield self.root, self.depth + + +# only for testing the implementation +def test__TreeNode(): + class NameTree(TreeNode): + def __init__(self, name, *args, **kwargs): + self.name = name + super().__init__(*args, **kwargs) + + def __repr__(self): + return 'Node(%s)' % self.name + + def create_tree(): + root = NameTree('root') + root.add([NameTree('n1'), NameTree('n2'), NameTree('n3')]) + root.children[0].add([NameTree('n1n1'), NameTree('n1n2'), NameTree('n1n3')]) + root.children[1].add([NameTree('n2n1'), NameTree('n2n2')]) + root.children[2].add([NameTree('n3n1')]) + root.children[2].children[0].add([NameTree('n3n1n1')]) + return root + + root = create_tree() + print('\nIterate over a node (root node):') + for node in root: + print(' |%s' % node) + + methods = [TreeNode.pre_order, TreeNode.post_order] + for method in methods: + print('\nIterate over tree (%s):' % method.__name__) + for node, depth in method(root): + print(' |%s%-30s parent=%s' % (' ' * depth, node, node.parent)) + + sys.exit(0) + + +# test__TreeNode() + + +################################################################################ + +class Color: + """ + Class for easy color codes manipulations. + """ + + _base_string = '\033[%sm' + _colors = { + 'BLACK': 0, + 'RED': 1, + 'GREEN': 2, + 'YELLOW': 3, + 'BLUE': 4, + 'MAGENTA': 5, + 'CYAN': 6, + 'GRAY': 7, + } + + def __init__(self, color_codes=[]): + try: + self.color_codes = set(color_codes) + except TypeError: + self.color_codes = set([color_codes]) + + def __add__(self, other): + if isinstance(other, Color): + return Color(self.color_codes.union(other.color_codes)) + elif isinstance(other, str): + return str(self) + other + return NotImplemented + + def __radd__(self, other): + if isinstance(other, str): + return other + str(self) + return NotImplemented + + def __str__(self): + return self._base_string % ';'.join(str(c) for c in self.color_codes) + + def __repr__(self): + return 'Color(%s)' % self.color_codes + + +# should probably be done in a metaclass or something +for name, value in Color._colors.items(): + # regular color + setattr(Color, name, Color(value + 30)) + # lighter version + setattr(Color, 'L_%s' % name, Color(value + 90)) + # background + setattr(Color, 'BG_%s' % name, Color(value + 40)) + # lighter background + setattr(Color, 'BG_L_%s' % name, Color(value + 100)) + +setattr(Color, 'RESET', Color(0)) +setattr(Color, 'BOLD', Color(1)) +setattr(Color, 'DIM', Color(2)) +setattr(Color, 'UNDERLINE', Color(4)) +setattr(Color, 'BLINK', Color(5)) +setattr(Color, 'REVERSE', Color(7)) # swaps background and forground +setattr(Color, 'HIDDEN', Color(8)) + + +def test__colors(): + for attr in dir(Color): + if attr.isupper() and not attr.startswith('_'): + print(getattr(Color, attr) + 'attribute %s' % attr + Color.RESET) + sys.exit(0) + + +# test__colors() + + +################################################################################ + +# construct python regex named group +def g(name, regex): + return r'(?P<{}>{})'.format(name, regex) + + +# print human readable size +# https://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size +def sizeof_fmt(num, suffix='B'): + for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: + if abs(num) < 1024.0: + suffix_str = unit + suffix + return "%3.1f %-3s" % (num, suffix_str) + num /= 1024.0 + unit = 'Yi' + suffix_str = unit + suffix + return "%3.1f %-3s" % (num, suffix_str) + + +################################################################################ + +class Symbol: + """ + Represents a linker symbol in an ELF file. Attributes are as in the output + of readelf command. Additionally, has optional file path and line number. + """ + + def __init__(self, num, name, value, size, type, bind, visibility, section, + file=None, line=None): + self.num = num + self.name = name + self.value = value + self.size = size + self.type = type + self.bind = bind + self.visibility = visibility + self.section = section + self.file = file + self.line = line + + def __repr__(self): + return 'Symbol(%s)' % (self.name, ) + + # Regex for parsing readelf output lines + # Readelf output should look like the following: + # Symbol table '.symtab' contains 623 entries: + # Num: Value Size Type Bind Vis Ndx Name + # 0: 00000000 0 NOTYPE LOCAL DEFAULT UND + # ... + # 565: 08002bf9 2 FUNC WEAK DEFAULT 2 TIM2_IRQHandler + # 566: 200002a8 88 OBJECT GLOBAL DEFAULT 8 hspi1 + pattern_fields = [ + r'\s*', + g('num', r'\d+'), r':', + r'\s+', + g('value', r'[0-9a-fA-F]+'), + r'\s+', + g('size', r'[0-9]+'), + r'\s+', + g('type', r'\S+'), + r'\s+', + g('bind', r'\S+'), + r'\s+', + g('visibility', r'\S+'), + r'\s+', + g('section', r'\S+'), + r'\s+', + g('name', r'.*'), + ] + pattern = r'^{}$'.format(r''.join(pattern_fields)) + pattern = re.compile(pattern) + + @classmethod + def from_readelf_line(cls, line, + ignored_types=['NOTYPE', 'SECTION', 'FILE'], + ignore_zero_size=True): + """ + Create a Symbol from a line of `readelf -Ws` output. + """ + m = cls.pattern.match(line) + if not m: + log.debug('no match: ' + line.strip()) + return None + + # convert non-string values + m = m.groupdict() + m['num'] = int(m['num']) + m['value'] = int(m['value'], 16) + m['size'] = int(m['size']) # suprisingly in decimal + try: # for numeric sections + m['section'] = int(m['section']) + except ValueError: + pass + + # ignore if needed + if not m['name'].strip() \ + or m['type'].lower() in map(str.lower, ignored_types) \ + or (ignore_zero_size and m['size'] == 0): + log.debug('ignoring: ' + line.strip()) + return None + + # create the Symbol + s = Symbol(**m) + + return s + + @classmethod + def extract_elf_symbols_info(cls, elf_file, readelf_exe='readelf'): + """ + Uses binutils 'readelf' to find info about all symbols from an ELF file. + """ + flags = ['--wide', '--syms'] + readelf_proc = subprocess.Popen([readelf_exe, *flags, elf_file], + stdout=subprocess.PIPE, universal_newlines=True) + + # parse lines + log.info('Using readelf symbols regex: %s' % cls.pattern.pattern) + symbols = [Symbol.from_readelf_line(l) for l in readelf_proc.stdout] + n_ignored = len(list(filter(lambda x: x is None, symbols))) + symbols = list(filter(None, symbols)) + + if readelf_proc.wait(3) != 0: + raise subprocess.CalledProcessError(readelf_proc.returncode, + readelf_proc.args) + + log.info('ignored %d/%d symbols' % (n_ignored, len(symbols) + n_ignored)) + + return symbols + + +def extract_elf_symbols_fileinfo(elf_file, nm_exe='nm'): + """ + Uses binutils 'nm' to find files and lines where symbols from an ELF + executable were defined. + """ + # Regex for parsing nm output lines + # We use Posix mode, so lines should be in form: + # NAME TYPE VALUE SIZE[\tFILE[:LINE]] + # e.g. + # MemManage_Handler T 08004130 00000002 /some/path/file.c:80 + # memset T 08000bf0 00000010 + pattern_fields = [ + g('name', r'\S+'), + r'\s+', + g('type', r'\S+'), + r'\s+', + g('value', r'[0-9a-fA-F]+'), + r'\s+', + g('size', r'[0-9a-fA-F]+'), + g('fileinfo', r'.*'), + ] + pattern = r'^{}$'.format(r''.join(pattern_fields)) + pattern = re.compile(pattern) + log.info('Using nm symbols regex: %s' % pattern.pattern) + + # use posix format + flags = ['--portability', '--line-numbers'] + nm_proc = subprocess.Popen([nm_exe, *flags, elf_file], + stdout=subprocess.PIPE, universal_newlines=True) + + # process nm output + fileinfo_dict = {} + for line in nm_proc.stdout: + m = pattern.match(line) + if not m: + continue + + # parse the file info + file, line = None, None + fileinfo = m.group('fileinfo').strip() + if len(fileinfo) > 0: + # check for line number + line_i = fileinfo.rfind(':') + if line_i >= 0: + file = fileinfo[:line_i] + line = int(fileinfo[line_i + 1]) + else: + file = fileinfo + # try to make the path more readable + file = os.path.normpath(file) + + fileinfo_dict[m.group('name')] = file, line + + if nm_proc.wait(3) != 0: + raise subprocess.CalledProcessError(nm_proc.returncode, + nm_proc.args) + + return fileinfo_dict + + +def add_fileinfo_to_symbols(fileinfo_dict, symbols_list): + # use dictionary for faster access (probably) + symbols_dict = {s.name: s for s in symbols_list} + for symbol_name, (file, line) in fileinfo_dict.items(): + if file is None and line is None: + continue + if symbol_name in symbols_dict: + symbol = symbols_dict[symbol_name] + symbol.file = file + symbol.line = line + else: + log.warning('nm found fileinfo for symbol "%s", which has not been found by readelf' + % symbol_name) + + +def demangle_symbol_names(symbols, cppfilt_exe='c++filt'): + """ + Use c++filt to demangle symbol names in-place. + """ + flags = [] + cppfilt_proc = subprocess.Popen( + [cppfilt_exe, *flags], stdin=subprocess.PIPE, stdout=subprocess.PIPE, universal_newlines=True) + + for symbol in symbols: + # write the line and flush it + # not super-efficient but writing all at once for large list of symbols + # can block the program (probably due to buffering) + cppfilt_proc.stdin.write((symbol.name + ' \n')) + cppfilt_proc.stdin.flush() + new_name = cppfilt_proc.stdout.readline().strip() + symbol.name = new_name + cppfilt_proc.stdin.close() + + if cppfilt_proc.wait(3) != 0: + raise subprocess.CalledProcessError(cppfilt_proc.returncode, + cppfilt_proc.args) + + +# Some nice info about sections in ELF files: +# http://www.sco.com/developers/gabi/2003-12-17/ch4.sheader.html#sh_flags +class Section: + """Represents an ELF file section as read by `readelf -WS`.""" + + # Regex for parsing readelf sections information + # Example output: + # Section Headers: + # [Nr] Name Type Addr Off Size ES Flg Lk Inf Al + # [ 0] NULL 00000000 000000 000000 00 0 0 0 + # [ 1] .isr_vector PROGBITS 08000000 010000 000188 00 A 0 0 1 + # [ 2] .text PROGBITS 08000190 010190 00490c 00 AX 0 0 16 + # [ 3] .rodata PROGBITS 08004aa0 014aa0 000328 00 A 0 0 8 + # Regex test: https://regex101.com/r/N3YQYw/1 + pattern_fields = [ + r'\s*', + r'\[\s*', g('num', r'\d+'), r'\]', + r'\s+', + g('name', r'\S+'), + r'\s+', + g('type', r'\S+'), + r'\s+', + g('address', r'[0-9a-fA-F]+'), + r'\s+', + g('offset', r'[0-9a-fA-F]+'), + r'\s+', + g('size', r'[0-9a-fA-F]+'), + r'\s+', + g('entry_size', r'[0-9a-fA-F]+'), # whatever it is we don't need it + r'\s+', + g('flags', r'\S*'), + r'\s+', + g('link', r'[0-9a-fA-F]+'), # whatever it is we don't need it + r'\s+', + g('info', r'[0-9a-fA-F]+'), # whatever it is we don't need it + r'\s+', + g('alignment', r'[0-9a-fA-F]+'), # whatever it is we don't need it + r'\s*' + ] + pattern = r'^{}$'.format(r''.join(pattern_fields)) + pattern = re.compile(pattern) + + class Flag: + # key to flags + WRITE = 'W' + ALLOC = 'A' + EXECUTE = 'X' + MERGE = 'M' + STRINGS = 'S' + INFO = 'I' + LINK_ORDER = 'L' + EXTRA_OS_PROCESSINg_REQUIRED = 'O' + GROUP = 'G' + TLS = 'T' + COMPRESSED = 'C' + UNKNOWN = 'x' + OS_SPECIFIC = 'o' + EXCLUDE = 'E' + PURECODE = 'y' + PROCESSOR_SPECIFIC = 'p' + + @classmethod + def to_string(cls, flag): + for name, value in vars(cls).items(): + if not name.startswith('_'): + if value == flag: + return name + return None + + def __init__(self, **kwargs): + self.num = kwargs['num'] + self.name = kwargs['name'] + self.type = kwargs['type'] + self.address = kwargs['address'] + self.offset = kwargs['offset'] + self.size = kwargs['size'] + self.entry_size = kwargs['entry_size'] + self.flags = kwargs['flags'] + self.link = kwargs['link'] + self.info = kwargs['info'] + self.alignment = kwargs['alignment'] + + def is_read_only(self): + return self.Flag.WRITE not in self.flags + + def occupies_memory(self): + # these are the only relevant sections for us + return self.Flag.ALLOC in self.flags + + # these two methods are probably a big simplification + # as they may be true only for small embedded systems + def occupies_rom(self): + return self.occupies_memory() and \ + self.type not in ['NOBITS'] + + def occupies_ram(self): + return self.occupies_memory() and not self.is_read_only() + + @classmethod + def from_readelf_line(cls, line): + """ + Create a Section from a line of `readelf -WS` output. + """ + m = cls.pattern.match(line) + if not m: + log.debug('no match: ' + line.strip()) + return None + + # convert non-string values + m = m.groupdict() + m['num'] = int(m['num']) + m['address'] = int(m['address'], 16) + m['offset'] = int(m['offset'], 16) + m['size'] = int(m['size'], 16) + m['entry_size'] = int(m['entry_size'], 16) + # not sure if these are base-16 or base-10 + m['link'] = int(m['link'], 10) + m['info'] = int(m['info'], 10) + m['alignment'] = int(m['alignment'], 10) + + return Section(**m) + + @classmethod + def print(cls, sections): + lines = [] + for s in sections: + fields = [str(s.num), s.name, s.type, + hex(s.address), sizeof_fmt(s.size), + ','.join(cls.Flag.to_string(f) for f in s.flags)] + lines.append(fields) + sizes = [max(len(l[i]) for l in lines) for i in range(6)] + h_fmt = '{:%d} {:%d} {:%d} {:%d} {:%d} {:%d}' % (*sizes, ) + fmt = '{:>%d} {:%d} {:%d} {:>%d} {:>%d} {:%d}' % (*sizes, ) + header = h_fmt.format('N', 'Name', 'Type', 'Addr', 'Size', 'Flags') + separator = '=' * len(header) + top_header = '{:=^{size}s}'.format(' SECTIONS ', size=len(separator)) + print(Color.BOLD + top_header + Color.RESET) + print(Color.BOLD + header + Color.RESET) + print(Color.BOLD + separator + Color.RESET) + for line in lines: + print(fmt.format(*line)) + print(Color.BOLD + separator + Color.RESET) + + @classmethod + def extract_sections_info(cls, elf_file, readelf_exe='readelf'): + """ + Uses binutils 'readelf' to find info about all sections from an ELF file. + """ + flags = ['--wide', '--section-headers'] + readelf_proc = subprocess.Popen([readelf_exe, *flags, elf_file], + stdout=subprocess.PIPE, universal_newlines=True) + + # parse lines + log.info('Using readelf sections regex: %s' % cls.pattern.pattern) + sections = [Section.from_readelf_line(l) for l in readelf_proc.stdout] + sections = list(filter(None, sections)) + + if readelf_proc.wait(3) != 0: + raise subprocess.CalledProcessError(readelf_proc.returncode, + readelf_proc.args) + + return sections + + +class SymbolsTreeByPath: + """A tree built from symbols grouped by paths. Nodes can be symbols or paths.""" + + class Node(TreeNode): + def __init__(self, data, is_dir=False, *args, **kwargs): + self.data = data + self._is_dir = is_dir + self.cumulative_size = None # used for accumulating symbol sizes in paths + super().__init__(*args, **kwargs) + + def is_symbol(self): + return isinstance(self.data, Symbol) + + def is_root(self): + return self.data is None + + def is_path(self): + return not self.is_root() and not self.is_symbol() + + def is_dir(self): + return self.is_path() and self._is_dir + + def is_file(self): + return self.is_path() and not self._is_dir + + def __repr__(self): + string = self.data.name if self.is_symbol() else self.data + return 'Node(%s)' % string + + def __init__(self, symbols=[]): + self.tree_root = self.Node(None) + self.orphans = self.Node('?') + self.tree_root.add(self.orphans) + for symbol in symbols: + self.add(symbol) + self.total_size = None + + def add(self, symbol): + assert isinstance(symbol, Symbol), "Only instances of Symbol can be added!" + if symbol.file is None: + self.orphans.add(self.Node(symbol)) + else: + if not os.path.isabs(symbol.file): + log.warning('Symbol\'s path is not absolute: %s: %s' + % (symbol, symbol.file)) + self._add_symbol_with_path(symbol) + + def _add_symbol_with_path(self, symbol): + """ + Adds the given symbol by creating nodes for each path component + before adding symbol as the last ("leaf") node. + """ + path = pathlib.Path(symbol.file) + node = self.tree_root + for part in path.parts: + # find it the part exists in children + path_children = filter(self.Node.is_path, node.children) + path_child = list(filter(lambda node: node.data == part, path_children)) + assert len(path_child) <= 1 + # if it does not exsits, then create it and add + if len(path_child) == 0: + path_child = self.Node(part, is_dir=True) + node.add(path_child) + else: + path_child = path_child[0] + # go 'into' this path part's node + node = path_child + # remove directory signature from last path part + node._is_dir = False + # last, add the symbol, the "tree leaf" + node.add(self.Node(symbol)) + + def merge_paths(self, fish_like=False): + """Merges all path componenets that have only one child into single nodes.""" + for node, depth in self.tree_root.pre_order(): + # we want only path nodes that have only one path node + if node.is_path() and len(node.children) == 1: + child = node.children[0] + if child.is_path(): + # add this node's path to its child + this_path = node.data + if fish_like: + head, tail = os.path.split(this_path) + this_path = os.path.join(head, tail[:1]) + child.data = os.path.join(this_path, child.data) + # remove this node and reparent its child + node.parent.children.remove(node) + node.parent.add(child) + + def sort(self, key, reverse=False): + """ + Sort all symbol lists by the given key - function that takes a Symbol as an argument. + sort_paths_by_name - if specified, then paths are sorted by name (directories first). + reverse - applies to symbols + reverse_paths - appliesto paths (still, directories go first) + """ + # to avoid sorting the same list many times, gather them first + nodes_with_children = [] + for node, depth in self.tree_root.pre_order(): + if len(node.children) > 1: + nodes_with_children.append(node) + for node in nodes_with_children: + # we need tee to split generators into many so that filter will work as expected + ch1, ch2 = itertools.tee(node.children) + symbols = filter(self.Node.is_symbol, ch1) + non_symbols = filter(lambda n: not n.is_symbol(), ch2) + # sort others by size if available else by name, directories first + # add - to size, as we need reverse sorting for path names + path_key = lambda node: -node.cumulative_size if node.cumulative_size is not None else node.data + ns1, ns2, ns3 = itertools.tee(non_symbols, 3) + dirs = filter(self.Node.is_dir, ns1) + files = filter(self.Node.is_file, ns2) + others = filter(lambda n: not n.is_file() and not n.is_dir(), ns3) + non_symbols = sorted(dirs, key=path_key) \ + + sorted(files, key=path_key) + list(others) + symbols = sorted(symbols, key=lambda node: key(node.data), reverse=reverse) + children = list(non_symbols) + list(symbols) + node.children = children + + def accumulate_sizes(self, reset=True): + """ + Traverse tree bottom-up to accumulate symbol sizes in paths. + """ + if reset: + for node, depth in self.tree_root.pre_order(): + node.cumulative_size = None + for node, depth in self.tree_root.post_order(): + if node.parent is None: + continue + if node.parent.cumulative_size is None: + node.parent.cumulative_size = 0 + if node.is_symbol(): + node.cumulative_size = node.data.size + node.parent.cumulative_size += node.cumulative_size + + def calculate_total_size(self): + # calculate the total size + all_nodes = (node for node, _ in self.tree_root.pre_order()) + all_symbols = filter(self.Node.is_symbol, all_nodes) + self.total_size = sum(s.data.size for s in all_symbols) + + class Protoline: + def __init__(self, depth=0, node=None, string=None, colors=None): + self.depth = depth + self.node = node + self.string = string + self.field_strings = [] + self.colors = colors or [] # avoid creating one list shared by all objects + + def print(self): + if len(self.colors) > 0: + print(sum(self.colors, Color()) + self.string + Color.RESET) + else: + print(self.string) + + def generate_printable_lines(self, *, max_width=80, min_size=0, header=None, indent=2, + colors=True, alternating_colors=False, trim=True, human_readable=False): + """ + Creates printable output in form of Protoline objects. + Handles RIDICULLOUSLY complex printing. Someone could probably implement it easier. + """ + # create and initially fill the lines + protolines = self._generate_protolines(min_size) + self._add_field_strings(protolines, indent, human_readable) + # formatting string + h_fmt = '{:{s0}} {:{s1}} {:{s2}}' + fmt = '{:{s0}} {:>{s1}} {:>{s2}}' + t_fmt = '{:{s0}} {:>{s1}} {:>{s2}}' + table_headers = ('Symbol', 'Size', '%') + # calculate sizes + field_sizes = self._calculate_field_sizes(protolines, max_width=max_width, + initial=[len(h) for h in table_headers]) + # trim too long strings + if trim: + self._trim_strings(protolines, field_sizes) + # prepare sizes dict + sizes_dict = {'s%d' % i: s for i, s in enumerate(field_sizes)} + # "render" the strings + for line in protolines: + if line.string is None: + if len(line.field_strings) == 0: + line.string = '' + else: + line.string = fmt.format(*line.field_strings, **sizes_dict) + # preopare table header + header_lines = self._create_header_protolines(h_fmt, table_headers, sizes_dict, header) + for l in reversed(header_lines): + protolines.insert(0, l) + # prepare totals + if self.total_size is not None: + totals_lines = self._create_totals_protolines(t_fmt, sizes_dict, human_readable) + protolines.extend(totals_lines) + # add colors + if colors: + self._add_colors(protolines, alternating_colors) + return protolines + + def _generate_protolines(self, min_size): + # generate list of nodes with indent to be printed + protolines = [] + for node, depth in self.tree_root.pre_order(): + # we never print root so subtract its depth + depth = depth - 1 + if node.is_root(): + continue + elif not (node.is_symbol() or node.is_path()): + raise Exception('Wrong symbol type encountered') + elif node.is_symbol() and node.data.size < min_size: + continue + protolines.append(self.Protoline(depth, node)) + return protolines + + def _add_field_strings(self, protolines, indent, human_readable): + for line in protolines: + indent_str = ' ' * indent * line.depth + if line.node.is_path(): + size_str, percent_str = '-', '-' + if line.node.cumulative_size is not None: + size_str = self._size_string(line.node.cumulative_size, human_readable) + if self.total_size is not None: + percent_str = '%.2f' % (line.node.cumulative_size / self.total_size * 100) + fields = [indent_str + line.node.data, size_str, percent_str] + elif line.node.is_symbol(): + percent_str = '-' + if self.total_size is not None: + percent_str = '%.2f' % (line.node.data.size / self.total_size * 100) + size_str = self._size_string(line.node.data.size, human_readable) + fields = [indent_str + line.node.data.name, size_str, percent_str] + else: + raise Exception('Wrong symbol type encountered') + line.field_strings = fields + + def _calculate_field_sizes(self, protolines, initial, max_width=0): + field_sizes = initial + for line in protolines: + for i, s, in enumerate(line.field_strings): + field_sizes[i] = max(len(s), field_sizes[i]) + # trim the fields if max_width is > 0 + if max_width > 0: + if sum(field_sizes) > max_width: + field_sizes[0] -= sum(field_sizes) - max_width + return field_sizes + + def _trim_strings(self, protolines, field_sizes): + for line in protolines: + for i, s, in enumerate(line.field_strings): + if len(s) > field_sizes[i]: + line.field_strings[i] = s[:field_sizes[i] - 3] + '...' + + def _create_header_protolines(self, header_fmt, table_headers, sizes_dict, header): + table_header = header_fmt.format(*table_headers, **sizes_dict) + separator = self._separator_string(len(table_header)) + if header is None: + header = separator + else: + h = ' %s ' % header + mid = len(separator) // 2 + before, after = int(math.ceil(len(h)/2)), int(math.floor(len(h)/2)) + header = separator[:mid - before] + h + separator[mid+after:] + header_protolines = [self.Protoline(string=s) for s in [header, table_header, separator]] + return header_protolines + + def _create_totals_protolines(self, fmt, sizes_dict, human_readable): + totals = fmt.format('Symbols total', self._size_string(self.total_size, human_readable), '', + **sizes_dict) + separator = self._separator_string(len(totals)) + return [self.Protoline(string=s) for s in [separator, totals, separator]] + + def _separator_string(self, length): + return '=' * length + + def _add_colors(self, protolines, alternating_colors): + second_symbol_color = False + for line in protolines: + c = [] + if line.node is None: # header lines + c = [Color.BOLD, Color.BLUE] + elif line.node.is_file(): + c = [Color.L_BLUE] + elif line.node.is_dir(): + c = [Color.BLUE] + elif line.node.is_symbol(): + if second_symbol_color and alternating_colors: + c = [Color.L_GREEN] + second_symbol_color = False + else: + c = [Color.L_YELLOW] + second_symbol_color = True + line.colors += c + + def _size_string(self, size, human_readable): + if human_readable: + return sizeof_fmt(size) + return str(size) + + +################################################################################ + +def main(): + result = False + args = parse_args() + + # adjust verbosity + if args.verbose: + level = log.level - 10 * args.verbose + log.setLevel(max(level, logging.DEBUG)) + + # prepare arguments + if not os.path.isfile(args.elf): + print('ELF file %s does not exist' % args.elf, file=sys.stderr) + return result + + if not any([args.rom, args.ram, args.print_sections, args.use_sections]): + print('No memory type action specified (RAM/ROM or special). See -h for help.') + return result + + def get_exe(name): + cmd = args.toolchain_triplet + name + if 'Windows' == platform.system(): + cmd = cmd + '.exe' + assert shutil.which(cmd) is not None, \ + 'Executable "%s" could not be found!' % cmd + return args.toolchain_triplet + name + + # process symbols + symbols = Symbol.extract_elf_symbols_info(args.elf, get_exe('readelf')) + fileinfo = extract_elf_symbols_fileinfo(args.elf, get_exe('nm')) + add_fileinfo_to_symbols(fileinfo, symbols) + + # demangle only after fileinfo extraction! + if not args.no_demangle: + demangle_symbol_names(symbols, get_exe('c++filt')) + + # load section info + sections = Section.extract_sections_info(args.elf, get_exe('readelf')) + sections_dict = {sec.num: sec for sec in sections} + + def print_tree(header, symbols): + tree = SymbolsTreeByPath(symbols) + if not args.no_merge_paths: + tree.merge_paths(args.fish_paths) + if not args.no_cumulative_size: + tree.accumulate_sizes() + if args.sort_by_name: + tree.sort(key=lambda symbol: symbol.name, reverse=False) + else: # sort by size + tree.sort(key=lambda symbol: symbol.size, reverse=True) + if not args.no_totals: + tree.calculate_total_size() + min_size = math.inf if args.files_only else args.min_size + lines = tree.generate_printable_lines( + header=header, colors=not args.no_color, human_readable=args.human_readable, + max_width=args.max_width, min_size=min_size, alternating_colors=args.alternating_colors) + for line in lines: + line.print() + + def filter_symbols(section_key): + secs = filter(section_key, sections) + secs_str = ', '.join(s.name for s in secs) + log.info('Considering sections: ' + secs_str) + filtered = filter(lambda symbol: section_key(sections_dict.get(symbol.section, None)), + symbols) + out, test = itertools.tee(filtered) + if len(list(test)) == 0: + print(""" +ERROR: No symbols from given section found or all were ignored! + Sections were: %s + """.strip() % secs_str, file=sys.stderr) + sys.exit(1) + return out + + if args.print_sections: + Section.print(sections) + + if args.rom: + print_tree('ROM', filter_symbols(lambda sec: sec and sec.occupies_rom())) + + if args.ram: + print_tree('RAM', filter_symbols(lambda sec: sec and sec.occupies_ram())) + + if args.use_sections: + nums = list(map(int, args.use_sections)) + # secs = list(filter(lambda s: s.num in nums, sections)) + name = 'SECTIONS: %s' % ','.join(map(str, nums)) + print_tree(name, filter_symbols(lambda sec: sec and sec.num in nums)) + + return True + + +if __name__ == "__main__": + result = main() + if not result: + sys.exit(1) +