| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077 |
- #!/usr/bin/env python3
- #
- # Copyright (c) 2016, Intel Corporation
- #
- # SPDX-License-Identifier: Apache-2.0
- # Based on a script by:
- # Chereau, Fabien <fabien.chereau@intel.com>
- # ^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)
|