1#!/usr/bin/env python3
2#
3# Copyright (c) 2016, 2020-2024 Intel Corporation
4#
5# SPDX-License-Identifier: Apache-2.0
6
7# Based on a script by:
8#       Chereau, Fabien <fabien.chereau@intel.com>
9
10"""
11Process an ELF file to generate size report on RAM and ROM.
12"""
13
14import argparse
15import locale
16import os
17import sys
18import re
19from pathlib import Path
20import json
21
22from packaging import version
23
24from colorama import init, Fore
25
26from anytree import RenderTree, NodeMixin, findall_by_attr
27from anytree.exporter import DictExporter
28
29import elftools
30from elftools.elf.elffile import ELFFile
31from elftools.elf.sections import SymbolTableSection
32from elftools.dwarf.descriptions import describe_form_class
33from elftools.dwarf.descriptions import (
34    describe_DWARF_expr, set_global_machine_arch)
35from elftools.dwarf.locationlists import (
36    LocationExpr, LocationParser)
37
38if version.parse(elftools.__version__) < version.parse('0.24'):
39    sys.exit("pyelftools is out of date, need version 0.24 or later")
40
41
42# ELF section flags
43SHF_WRITE = 0x1
44SHF_ALLOC = 0x2
45SHF_EXEC = 0x4
46SHF_WRITE_ALLOC = SHF_WRITE | SHF_ALLOC
47SHF_ALLOC_EXEC = SHF_ALLOC | SHF_EXEC
48
49DT_LOCATION = re.compile(r"\(DW_OP_addr: ([0-9a-f]+)\)")
50
51SRC_FILE_EXT = ('.h', '.c', '.hpp', '.cpp', '.hxx', '.cxx', '.c++')
52
53
54def get_symbol_addr(sym):
55    """Get the address of a symbol"""
56    return sym['st_value']
57
58
59def get_symbol_size(sym):
60    """Get the size of a symbol"""
61    return sym['st_size']
62
63
64def is_symbol_in_ranges(sym, ranges):
65    """
66    Given a list of start/end addresses, test if the symbol
67    lies within any of these address ranges.
68    """
69    for bound in ranges:
70        if bound['start'] <= sym['st_value'] <= bound['end']:
71            return bound
72
73    return None
74
75
76def get_die_mapped_address(die, parser, dwarfinfo):
77    """Get the bounding addresses from a DIE variable or subprogram"""
78    low = None
79    high = None
80
81    if die.tag == 'DW_TAG_variable':
82        if 'DW_AT_location' in die.attributes:
83            loc_attr = die.attributes['DW_AT_location']
84            if parser.attribute_has_location(loc_attr, die.cu['version']):
85                loc = parser.parse_from_attribute(loc_attr, die.cu['version'], die)
86                if isinstance(loc, LocationExpr):
87                    addr = describe_DWARF_expr(loc.loc_expr,
88                                               dwarfinfo.structs)
89
90                    matcher = DT_LOCATION.match(addr)
91                    if matcher:
92                        low = int(matcher.group(1), 16)
93                        high = low + 1
94
95    if die.tag == 'DW_TAG_subprogram':
96        if 'DW_AT_low_pc' in die.attributes:
97            low = die.attributes['DW_AT_low_pc'].value
98
99            high_pc = die.attributes['DW_AT_high_pc']
100            high_pc_class = describe_form_class(high_pc.form)
101            if high_pc_class == 'address':
102                high = high_pc.value
103            elif high_pc_class == 'constant':
104                high = low + high_pc.value
105
106    return low, high
107
108
109def match_symbol_address(symlist, die, parser, dwarfinfo):
110    """
111    Find the symbol from a symbol list
112    where it matches the address in DIE variable,
113    or within the range of a DIE subprogram.
114    """
115    low, high = get_die_mapped_address(die, parser, dwarfinfo)
116
117    if low is None:
118        return None
119
120    for sym in symlist:
121        if low <= sym['symbol']['st_value'] < high:
122            return sym
123
124    return None
125
126
127def get_symbols(elf, addr_ranges):
128    """
129    Fetch the symbols from the symbol table and put them
130    into ROM, RAM, unassigned buckets.
131    """
132    rom_syms = dict()
133    ram_syms = dict()
134    unassigned_syms = dict()
135
136    rom_addr_ranges = addr_ranges['rom']
137    ram_addr_ranges = addr_ranges['ram']
138    unassigned_addr_ranges = addr_ranges['unassigned']
139
140    for section in elf.iter_sections():
141        if isinstance(section, SymbolTableSection):
142            for sym in section.iter_symbols():
143                # Ignore symbols with size == 0
144                if get_symbol_size(sym) == 0:
145                    continue
146
147                found_sec = False
148                entry = {'name': sym.name,
149                         'symbol': sym,
150                         'mapped_files': set(),
151                         'section': None}
152
153                # If symbol is in ROM area?
154                bound = is_symbol_in_ranges(sym, rom_addr_ranges)
155                if bound:
156                    if sym.name not in rom_syms:
157                        rom_syms[sym.name] = list()
158                    entry['section'] = bound['name']
159                    rom_syms[sym.name].append(entry)
160                    found_sec = True
161
162                # If symbol is in RAM area?
163                bound = is_symbol_in_ranges(sym, ram_addr_ranges)
164                if bound:
165                    if sym.name not in ram_syms:
166                        ram_syms[sym.name] = list()
167                    entry['section'] = bound['name']
168                    ram_syms[sym.name].append(entry)
169                    found_sec = True
170
171                if not found_sec:
172                    bound = is_symbol_in_ranges(sym, unassigned_addr_ranges)
173                    if bound:
174                        entry['section'] = bound['name']
175                    if sym.name not in unassigned_syms:
176                        unassigned_syms[sym.name] = list()
177                    unassigned_syms[sym.name].append(entry)
178
179    ret = {'rom': rom_syms,
180           'ram': ram_syms,
181           'unassigned': unassigned_syms}
182    return ret
183
184
185def print_section_info(section, descr=""):
186    if args.verbose:
187        sec_size = section['sh_size']
188        sec_start = section['sh_addr']
189        sec_end = sec_start + (sec_size - 1 if sec_size else 0)
190        print(f"DEBUG: "
191              f"0x{sec_start:08x}-0x{sec_end:08x} "
192              f"{descr} '{section.name}': size={sec_size}, "
193              f"{section['sh_type']}, 0x{section['sh_flags']:08x}")
194#
195
196
197def get_section_ranges(elf):
198    """
199    Parse ELF header to find out the address ranges of ROM or RAM sections
200    and their total sizes.
201    """
202    rom_addr_ranges = list()
203    ram_addr_ranges = list()
204    unassigned_addr_ranges = list()
205
206    rom_size = 0
207    ram_size = 0
208    unassigned_size = 0
209
210    for section in elf.iter_sections():
211        size = section['sh_size']
212        sec_start = section['sh_addr']
213        sec_end = sec_start + (size - 1 if size else 0)
214        bound = {'start': sec_start, 'end': sec_end, 'name': section.name}
215        is_assigned = False
216
217        if section['sh_type'] == 'SHT_NOBITS':
218            # BSS and noinit sections
219            ram_addr_ranges.append(bound)
220            ram_size += size
221            is_assigned = True
222            print_section_info(section, "RAM bss section")
223
224        elif section['sh_type'] == 'SHT_PROGBITS':
225            # Sections to be in flash or memory
226            flags = section['sh_flags']
227            if (flags & SHF_ALLOC_EXEC) == SHF_ALLOC_EXEC:
228                # Text section
229                rom_addr_ranges.append(bound)
230                rom_size += size
231                is_assigned = True
232                print_section_info(section, "ROM txt section")
233
234            elif (flags & SHF_WRITE_ALLOC) == SHF_WRITE_ALLOC:
235                # Data occupies both ROM and RAM
236                # since at boot, content is copied from ROM to RAM
237                rom_addr_ranges.append(bound)
238                rom_size += size
239                ram_addr_ranges.append(bound)
240                ram_size += size
241                is_assigned = True
242                print_section_info(section, "ROM,RAM section")
243
244            elif (flags & SHF_ALLOC) == SHF_ALLOC:
245                # Read only data
246                rom_addr_ranges.append(bound)
247                rom_size += size
248                is_assigned = True
249                print_section_info(section, "ROM r/o section")
250
251        if not is_assigned:
252            print_section_info(section, "unassigned section")
253            unassigned_addr_ranges.append(bound)
254            unassigned_size += size
255
256    ret = {'rom': rom_addr_ranges,
257           'rom_total_size': rom_size,
258           'ram': ram_addr_ranges,
259           'ram_total_size': ram_size,
260           'unassigned': unassigned_addr_ranges,
261           'unassigned_total_size': unassigned_size}
262    return ret
263
264
265def get_die_filename(die, lineprog):
266    """Get the source code filename associated with a DIE"""
267    file_index = die.attributes['DW_AT_decl_file'].value
268    file_entry = lineprog['file_entry'][file_index - 1]
269
270    dir_index = file_entry['dir_index']
271    if dir_index == 0:
272        filename = file_entry.name
273    else:
274        directory = lineprog.header['include_directory'][dir_index - 1]
275        filename = os.path.join(directory, file_entry.name)
276
277    path = Path(filename.decode(locale.getpreferredencoding()))
278
279    # Prepend output path to relative path
280    if not path.is_absolute():
281        output = Path(args.output)
282        path = output.joinpath(path)
283
284    # Change path to relative to Zephyr base
285    try:
286        path = path.resolve()
287    except OSError as e:
288        # built-ins can't be resolved, so it's not an issue
289        if '<built-in>' not in str(path):
290            raise e
291
292    return path
293
294
295def do_simple_name_matching(dwarfinfo, symbol_dict, processed):
296    """
297    Sequentially process DIEs in compiler units with direct file mappings
298    within the DIEs themselves, and do simply matching between DIE names
299    and symbol names.
300    """
301    mapped_symbols = processed['mapped_symbols']
302    mapped_addresses = processed['mapped_addr']
303    unmapped_symbols = processed['unmapped_symbols']
304    newly_mapped_syms = set()
305
306    location_lists = dwarfinfo.location_lists()
307    location_parser = LocationParser(location_lists)
308
309    unmapped_dies = set()
310
311    # Loop through all compile units
312    for compile_unit in dwarfinfo.iter_CUs():
313        lineprog = dwarfinfo.line_program_for_CU(compile_unit)
314        if lineprog is None:
315            continue
316
317        # Loop through each DIE and find variables and
318        # subprograms (i.e. functions)
319        for die in compile_unit.iter_DIEs():
320            sym_name = None
321
322            # Process variables
323            if die.tag == 'DW_TAG_variable':
324                # DW_AT_declaration
325
326                # having 'DW_AT_location' means this maps
327                # to an actual address (e.g. not an extern)
328                if 'DW_AT_location' in die.attributes:
329                    sym_name = die.get_full_path()
330
331            # Process subprograms (i.e. functions) if they are valid
332            if die.tag == 'DW_TAG_subprogram':
333                # Refer to another DIE for name
334                if ('DW_AT_abstract_origin' in die.attributes) or (
335                        'DW_AT_specification' in die.attributes):
336                    unmapped_dies.add(die)
337
338                # having 'DW_AT_low_pc' means it maps to
339                # an actual address
340                elif 'DW_AT_low_pc' in die.attributes:
341                    # DW_AT_low_pc == 0 is a weak function
342                    # which has been overriden
343                    if die.attributes['DW_AT_low_pc'].value != 0:
344                        sym_name = die.get_full_path()
345
346                # For mangled function names, the linkage name
347                # is what appears in the symbol list
348                if 'DW_AT_linkage_name' in die.attributes:
349                    linkage = die.attributes['DW_AT_linkage_name']
350                    sym_name = linkage.value.decode()
351
352            if sym_name is not None:
353                # Skip DIE with no reference back to a file
354                if not 'DW_AT_decl_file' in die.attributes:
355                    continue
356
357                is_die_mapped = False
358                if sym_name in symbol_dict:
359                    mapped_symbols.add(sym_name)
360                    symlist = symbol_dict[sym_name]
361                    symbol = match_symbol_address(symlist, die,
362                                                  location_parser,
363                                                  dwarfinfo)
364
365                    if symbol is not None:
366                        symaddr = symbol['symbol']['st_value']
367                        if symaddr not in mapped_addresses:
368                            is_die_mapped = True
369                            path = get_die_filename(die, lineprog)
370                            symbol['mapped_files'].add(path)
371                            mapped_addresses.add(symaddr)
372                            newly_mapped_syms.add(sym_name)
373
374                if not is_die_mapped:
375                    unmapped_dies.add(die)
376
377    mapped_symbols = mapped_symbols.union(newly_mapped_syms)
378    unmapped_symbols = unmapped_symbols.difference(newly_mapped_syms)
379
380    processed['mapped_symbols'] = mapped_symbols
381    processed['mapped_addr'] = mapped_addresses
382    processed['unmapped_symbols'] = unmapped_symbols
383    processed['unmapped_dies'] = unmapped_dies
384
385
386def mark_address_aliases(symbol_dict, processed):
387    """
388    Mark symbol aliases as already mapped to prevent
389    double counting.
390
391    There are functions and variables which are aliases to
392    other functions/variables. So this marks them as mapped
393    so they will not get counted again when a tree is being
394    built for display.
395    """
396    mapped_symbols = processed['mapped_symbols']
397    mapped_addresses = processed['mapped_addr']
398    unmapped_symbols = processed['unmapped_symbols']
399    already_mapped_syms = set()
400
401    for ums in unmapped_symbols:
402        for one_sym in symbol_dict[ums]:
403            symbol = one_sym['symbol']
404            if symbol['st_value'] in mapped_addresses:
405                already_mapped_syms.add(ums)
406
407    mapped_symbols = mapped_symbols.union(already_mapped_syms)
408    unmapped_symbols = unmapped_symbols.difference(already_mapped_syms)
409
410    processed['mapped_symbols'] = mapped_symbols
411    processed['mapped_addr'] = mapped_addresses
412    processed['unmapped_symbols'] = unmapped_symbols
413
414
415def do_address_range_matching(dwarfinfo, symbol_dict, processed):
416    """
417    Match symbols indirectly using address ranges.
418
419    This uses the address ranges of DIEs and map them to symbols
420    residing within those ranges, and works on DIEs that have not
421    been mapped in previous steps. This works on symbol names
422    that do not match the names in DIEs, e.g. "<func>" in DIE,
423    but "<func>.constprop.*" in symbol name list. This also
424    helps with mapping the mangled function names in C++,
425    since the names in DIE are actual function names in source
426    code and not mangled version of them.
427    """
428    if 'unmapped_dies' not in processed:
429        return
430
431    mapped_symbols = processed['mapped_symbols']
432    mapped_addresses = processed['mapped_addr']
433    unmapped_symbols = processed['unmapped_symbols']
434    newly_mapped_syms = set()
435
436    location_lists = dwarfinfo.location_lists()
437    location_parser = LocationParser(location_lists)
438
439    unmapped_dies = processed['unmapped_dies']
440
441    # Group DIEs by compile units
442    cu_list = dict()
443
444    for die in unmapped_dies:
445        cu = die.cu
446        if cu not in cu_list:
447            cu_list[cu] = {'dies': set()}
448        cu_list[cu]['dies'].add(die)
449
450    # Loop through all compile units
451    for cu in cu_list:
452        lineprog = dwarfinfo.line_program_for_CU(cu)
453
454        # Map offsets from DIEs
455        offset_map = dict()
456        for die in cu.iter_DIEs():
457            offset_map[die.offset] = die
458
459        for die in cu_list[cu]['dies']:
460            if not die.tag == 'DW_TAG_subprogram':
461                continue
462
463            path = None
464
465            # Has direct reference to file, so use it
466            if 'DW_AT_decl_file' in die.attributes:
467                path = get_die_filename(die, lineprog)
468
469            # Loop through indirect reference until a direct
470            # reference to file is found
471            if ('DW_AT_abstract_origin' in die.attributes) or (
472                    'DW_AT_specification' in die.attributes):
473                die_ptr = die
474                while path is None:
475                    if not (die_ptr.tag == 'DW_TAG_subprogram') or not (
476                            ('DW_AT_abstract_origin' in die_ptr.attributes) or
477                            ('DW_AT_specification' in die_ptr.attributes)):
478                        break
479
480                    if 'DW_AT_abstract_origin' in die_ptr.attributes:
481                        ofname = 'DW_AT_abstract_origin'
482                    elif 'DW_AT_specification' in die_ptr.attributes:
483                        ofname = 'DW_AT_specification'
484
485                    offset = die_ptr.attributes[ofname].value
486                    offset += die_ptr.cu.cu_offset
487
488                    # There is nothing to reference so no need to continue
489                    if offset not in offset_map:
490                        break
491
492                    die_ptr = offset_map[offset]
493                    if 'DW_AT_decl_file' in die_ptr.attributes:
494                        path = get_die_filename(die_ptr, lineprog)
495
496            # Nothing to map
497            if path is not None:
498                low, high = get_die_mapped_address(die, location_parser,
499                                                   dwarfinfo)
500                if low is None:
501                    continue
502
503                for ums in unmapped_symbols:
504                    for one_sym in symbol_dict[ums]:
505                        symbol = one_sym['symbol']
506                        symaddr = symbol['st_value']
507
508                        if symaddr not in mapped_addresses:
509                            if low <= symaddr < high:
510                                one_sym['mapped_files'].add(path)
511                                mapped_addresses.add(symaddr)
512                                newly_mapped_syms.add(ums)
513
514    mapped_symbols = mapped_symbols.union(newly_mapped_syms)
515    unmapped_symbols = unmapped_symbols.difference(newly_mapped_syms)
516
517    processed['mapped_symbols'] = mapped_symbols
518    processed['mapped_addr'] = mapped_addresses
519    processed['unmapped_symbols'] = unmapped_symbols
520
521
522def set_root_path_for_unmapped_symbols(symbol_dict, addr_range, processed):
523    """
524    Set root path for unmapped symbols.
525
526    Any unmapped symbols are added under the root node if those
527    symbols reside within the desired memory address ranges
528    (e.g. ROM or RAM).
529    """
530    mapped_symbols = processed['mapped_symbols']
531    mapped_addresses = processed['mapped_addr']
532    unmapped_symbols = processed['unmapped_symbols']
533    newly_mapped_syms = set()
534
535    for ums in unmapped_symbols:
536        for one_sym in symbol_dict[ums]:
537            symbol = one_sym['symbol']
538            symaddr = symbol['st_value']
539
540            if is_symbol_in_ranges(symbol, addr_range):
541                if symaddr not in mapped_addresses:
542                    path = Path(':')
543                    one_sym['mapped_files'].add(path)
544                    mapped_addresses.add(symaddr)
545                    newly_mapped_syms.add(ums)
546
547    mapped_symbols = mapped_symbols.union(newly_mapped_syms)
548    unmapped_symbols = unmapped_symbols.difference(newly_mapped_syms)
549
550    processed['mapped_symbols'] = mapped_symbols
551    processed['mapped_addr'] = mapped_addresses
552    processed['unmapped_symbols'] = unmapped_symbols
553
554def find_common_path_prefix(symbol_dict):
555    """
556    Find the common path prefix of all mapped files.
557    Must be called before set_root_path_for_unmapped_symbols().
558    """
559    paths = list()
560
561    for _, sym in symbol_dict.items():
562        for symbol in sym:
563            for file in symbol['mapped_files']:
564                paths.append(file)
565
566    try:
567        return os.path.commonpath(paths)
568    except ValueError:
569        return None
570
571class TreeNode(NodeMixin):
572    """
573    A symbol node.
574    """
575
576    def __init__(self, name, identifier, size=0, parent=None, children=None, address=None, section=None):
577        super().__init__()
578        self._name = name
579        self._size = size
580        self.parent = parent
581        self._identifier = identifier
582        if address is not None:
583            self.address = address
584        if section is not None:
585            self.section = section
586        if children:
587            self.children = children
588
589    def __repr__(self):
590        return self._name
591
592
593def sum_node_children_size(node):
594    """
595    Calculate the sum of symbol size of all direct children.
596    """
597    size = 0
598
599    for child in node.children:
600        size += child._size
601
602    return size
603
604
605def generate_any_tree(symbol_dict, total_size, path_prefix):
606    """
607    Generate a symbol tree for output.
608    """
609    root = TreeNode('Root', "root")
610    node_no_paths = TreeNode('(no paths)', ":", parent=root)
611
612    if path_prefix and Path(path_prefix) == Path(args.zephyrbase):
613        # All source files are under ZEPHYR_BASE so there is
614        # no need for another level.
615        node_zephyr_base = root
616        node_output_dir = root
617        node_workspace = root
618        node_others = root
619    else:
620        node_zephyr_base = TreeNode('ZEPHYR_BASE', args.zephyrbase)
621        node_output_dir = TreeNode('OUTPUT_DIR', args.output)
622        node_others = TreeNode("/", "/")
623
624        if args.workspace:
625            node_workspace = TreeNode('WORKSPACE', args.workspace)
626        else:
627            node_workspace = node_others
628
629    # A set of helper function for building a simple tree with a path-like
630    # hierarchy.
631    def _insert_one_elem(root, path, size, addr, section):
632        cur = None
633        node = None
634        parent = root
635        for part in path.parts:
636            if cur is None:
637                cur = part
638            else:
639                cur = str(Path(cur, part))
640
641            results = findall_by_attr(root, cur, name="_identifier")
642            if results:
643                item = results[0]
644                if not hasattr(item, 'address'):
645                    # Passing down through a non-terminal parent node.
646                    parent = item
647                    parent._size += size
648                else:
649                    # Another symbol node here with the same name; stick to its parent as well.
650                    parent = item.parent
651                    node = TreeNode(name=str(part), identifier=cur, size=size, parent=parent)
652            else:
653                # There is no such terminal symbol in the tree yet; let's add it.
654                if node:
655                    parent = node
656                node = TreeNode(name=str(part), identifier=cur, size=size, parent=parent)
657        if node:
658            # Set memory block address and section name properties only for terminal symbol nodes.
659            # Don't do it on file- and directory- level parent nodes.
660            node.address = addr
661            node.section = section
662        else:
663            # normally this shouldn't happen; just to detect data or logic errors.
664            print(f"ERROR: no end node created for {root}, {path}, 0x{addr:08x}+{size}@{section}")
665    #
666
667    # Mapping paths to tree nodes
668    path_node_map = [
669        [Path(args.zephyrbase), node_zephyr_base],
670        [Path(args.output), node_output_dir],
671    ]
672
673    if args.workspace:
674        path_node_map.append(
675            [Path(args.workspace), node_workspace]
676        )
677
678    for name, sym in symbol_dict.items():
679        for symbol in sym:
680            size = get_symbol_size(symbol['symbol'])
681            addr = get_symbol_addr(symbol['symbol'])
682            section = symbol['section']
683            for file in symbol['mapped_files']:
684                path = Path(file, name)
685                if path.is_absolute():
686                    has_node = False
687
688                    for one_path in path_node_map:
689                        if one_path[0] in path.parents:
690                            path = path.relative_to(one_path[0])
691                            dest_node = one_path[1]
692                            has_node = True
693                            break
694
695                    if not has_node:
696                        dest_node = node_others
697                else:
698                    dest_node = node_no_paths
699
700                _insert_one_elem(dest_node, path, size, addr, section)
701
702
703    if node_zephyr_base is not root:
704        # ZEPHYR_BASE and OUTPUT_DIR nodes don't have sum of symbol size
705        # so calculate them here.
706        node_zephyr_base._size = sum_node_children_size(node_zephyr_base)
707        node_output_dir._size = sum_node_children_size(node_output_dir)
708
709        # Find out which nodes need to be in the tree.
710        # "(no path)", ZEPHYR_BASE nodes are essential.
711        children = [node_no_paths, node_zephyr_base]
712        if node_output_dir.height != 0:
713            # OUTPUT_DIR may be under ZEPHYR_BASE.
714            children.append(node_output_dir)
715        if node_others.height != 0:
716            # Only include "others" node if there is something.
717            children.append(node_others)
718
719        if args.workspace:
720            node_workspace._size = sum_node_children_size(node_workspace)
721            if node_workspace.height != 0:
722                children.append(node_workspace)
723
724        root.children = children
725
726    root._size = total_size
727
728    # Need to account for code and data where there are not emitted
729    # symbols associated with them.
730    node_hidden_syms = TreeNode('(hidden)', "(hidden)", parent=root)
731    node_hidden_syms._size = root._size - sum_node_children_size(root)
732
733    return root
734
735
736def node_sort(items):
737    """
738    Node sorting used with RenderTree.
739    """
740    return sorted(items, key=lambda item: item._name)
741
742
743def print_any_tree(root, total_size, depth):
744    """
745    Print the symbol tree.
746    """
747    print('{:98s} {:>7s} {:>7s} {:11s} {:16s}'.format(
748        Fore.YELLOW + "Path", "Size", "%", " Address", "Section" + Fore.RESET))
749    print('=' * 138)
750    for row in RenderTree(root, childiter=node_sort, maxlevel=depth):
751        f = len(row.pre) + len(row.node._name)
752        s = str(row.node._size).rjust(100-f)
753        percent = 100 * float(row.node._size) / float(total_size)
754
755        hex_addr = "-"
756        section_name = ""
757        cc = cr = ""
758        if not row.node.children:
759            if hasattr(row.node, 'section'):
760                section_name = row.node.section
761            if hasattr(row.node, 'address'):
762                hex_addr = "0x{:08x}".format(row.node.address)
763                cc = Fore.CYAN
764                cr = Fore.RESET
765        elif row.node._name.endswith(SRC_FILE_EXT):
766            cc = Fore.GREEN
767            cr = Fore.RESET
768
769        print(f"{row.pre}{cc}{row.node._name} {s} {cr}{Fore.BLUE}{percent:6.2f}%{Fore.RESET}  {hex_addr} {section_name}")
770    print('=' * 138)
771    print(f'{total_size:>101}')
772
773
774def parse_args():
775    """
776    Parse command line arguments.
777    """
778    global args
779
780    parser = argparse.ArgumentParser(allow_abbrev=False)
781
782    parser.add_argument("-k", "--kernel", required=True,
783                        help="Zephyr ELF binary")
784    parser.add_argument("-z", "--zephyrbase", required=True,
785                        help="Zephyr base path")
786    parser.add_argument("-q", "--quiet", action="store_true",
787                        help="Do not output anything on the screen.")
788    parser.add_argument("-o", "--output", required=True,
789                        help="Output path")
790    parser.add_argument("-w", "--workspace", default=None,
791                        help="Workspace path (Usually the same as WEST_TOPDIR)")
792    parser.add_argument("target", choices=['rom', 'ram', 'all'])
793    parser.add_argument("-d", "--depth", dest="depth",
794                        type=int, default=None,
795                        help="How deep should we go into the tree",
796                        metavar="DEPTH")
797    parser.add_argument("-v", "--verbose", action="store_true",
798                        help="Print extra debugging information")
799    parser.add_argument("--json", help="store results in a JSON file.")
800    args = parser.parse_args()
801
802
803def main():
804    """
805    Main program.
806    """
807    parse_args()
808
809    sys.stdout.reconfigure(encoding='utf-8')
810
811    # Init colorama
812    init()
813
814    assert os.path.exists(args.kernel), "{0} does not exist.".format(args.kernel)
815    if args.target == 'ram':
816        targets = ['ram']
817    elif args.target == 'rom':
818        targets = ['rom']
819    elif args.target == 'all':
820        targets = ['rom', 'ram']
821
822    elf = ELFFile(open(args.kernel, "rb"))
823    assert elf.has_dwarf_info(), "ELF file has no DWARF information"
824
825    set_global_machine_arch(elf.get_machine_arch())
826    addr_ranges = get_section_ranges(elf)
827    dwarfinfo = elf.get_dwarf_info()
828
829    for t in targets:
830
831        symbols = get_symbols(elf, addr_ranges)
832
833        for sym in symbols['unassigned'].values():
834            for sym_entry in sym:
835                print(f"WARN: Symbol '{sym_entry['name']}' section '{sym_entry['section']}' "
836                      "is not in RAM or ROM.")
837
838        if args.json:
839            jsonout = args.json
840        else:
841            jsonout = os.path.join(args.output, f'{t}.json')
842
843        symbol_dict = symbols[t]
844        symsize = addr_ranges[f'{t}_total_size']
845        ranges = addr_ranges[t]
846
847        if symbol_dict is not None:
848            processed = {"mapped_symbols": set(),
849                         "mapped_addr": set(),
850                         "unmapped_symbols": set(symbol_dict.keys())}
851
852            do_simple_name_matching(dwarfinfo, symbol_dict, processed)
853            mark_address_aliases(symbol_dict, processed)
854            do_address_range_matching(dwarfinfo, symbol_dict, processed)
855            mark_address_aliases(symbol_dict, processed)
856            common_path_prefix = find_common_path_prefix(symbol_dict)
857            set_root_path_for_unmapped_symbols(symbol_dict, ranges, processed)
858
859            if args.verbose:
860                for sym in processed['unmapped_symbols']:
861                    print("INFO: Unmapped symbol: {0}".format(sym))
862
863            root = generate_any_tree(symbol_dict, symsize, common_path_prefix)
864            if not args.quiet:
865                print_any_tree(root, symsize, args.depth)
866
867            exporter = DictExporter(attriter=lambda attrs: [(k.lstrip('_'), v) for k, v in attrs])
868            data = dict()
869            data["symbols"] = exporter.export(root)
870            data["total_size"] = symsize
871            with open(jsonout, "w") as fp:
872                json.dump(data, fp, indent=4)
873
874
875if __name__ == "__main__":
876    main()
877