1#!/usr/bin/env python3
2#
3# Copyright (c) 2016, 2020 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 True
72
73    return False
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'])
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 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
139    for section in elf.iter_sections():
140        if isinstance(section, SymbolTableSection):
141            for sym in section.iter_symbols():
142                # Ignore symbols with size == 0
143                if get_symbol_size(sym) == 0:
144                    continue
145
146                found_sec = False
147                entry = {'name': sym.name,
148                         'symbol': sym,
149                         'mapped_files': set()}
150
151                # If symbol is in ROM area?
152                if is_symbol_in_ranges(sym, rom_addr_ranges):
153                    if sym.name not in rom_syms:
154                        rom_syms[sym.name] = list()
155                    rom_syms[sym.name].append(entry)
156                    found_sec = True
157
158                # If symbol is in RAM area?
159                if is_symbol_in_ranges(sym, ram_addr_ranges):
160                    if sym.name not in ram_syms:
161                        ram_syms[sym.name] = list()
162                    ram_syms[sym.name].append(entry)
163                    found_sec = True
164
165                if not found_sec:
166                    unassigned_syms['sym_name'] = entry
167
168    ret = {'rom': rom_syms,
169           'ram': ram_syms,
170           'unassigned': unassigned_syms}
171    return ret
172
173
174def get_section_ranges(elf):
175    """
176    Parse ELF header to find out the address ranges of ROM or RAM sections
177    and their total sizes.
178    """
179    rom_addr_ranges = list()
180    ram_addr_ranges = list()
181    rom_size = 0
182    ram_size = 0
183
184    for section in elf.iter_sections():
185        size = section['sh_size']
186        sec_start = section['sh_addr']
187        sec_end = sec_start + size - 1
188        bound = {'start': sec_start, 'end': sec_end}
189
190        if section['sh_type'] == 'SHT_NOBITS':
191            # BSS and noinit sections
192            ram_addr_ranges.append(bound)
193            ram_size += size
194        elif section['sh_type'] == 'SHT_PROGBITS':
195            # Sections to be in flash or memory
196            flags = section['sh_flags']
197            if (flags & SHF_ALLOC_EXEC) == SHF_ALLOC_EXEC:
198                # Text section
199                rom_addr_ranges.append(bound)
200                rom_size += size
201            elif (flags & SHF_WRITE_ALLOC) == SHF_WRITE_ALLOC:
202                # Data occupies both ROM and RAM
203                # since at boot, content is copied from ROM to RAM
204                rom_addr_ranges.append(bound)
205                rom_size += size
206
207                ram_addr_ranges.append(bound)
208                ram_size += size
209            elif (flags & SHF_ALLOC) == SHF_ALLOC:
210                # Read only data
211                rom_addr_ranges.append(bound)
212                rom_size += size
213
214    ret = {'rom': rom_addr_ranges,
215           'rom_total_size': rom_size,
216           'ram': ram_addr_ranges,
217           'ram_total_size': ram_size}
218    return ret
219
220
221def get_die_filename(die, lineprog):
222    """Get the source code filename associated with a DIE"""
223    file_index = die.attributes['DW_AT_decl_file'].value
224    file_entry = lineprog['file_entry'][file_index - 1]
225
226    dir_index = file_entry['dir_index']
227    if dir_index == 0:
228        filename = file_entry.name
229    else:
230        directory = lineprog.header['include_directory'][dir_index - 1]
231        filename = os.path.join(directory, file_entry.name)
232
233    path = Path(filename.decode(locale.getpreferredencoding()))
234
235    # Prepend output path to relative path
236    if not path.is_absolute():
237        output = Path(args.output)
238        path = output.joinpath(path)
239
240    # Change path to relative to Zephyr base
241    try:
242        path = path.resolve()
243    except OSError as e:
244        # built-ins can't be resolved, so it's not an issue
245        if '<built-in>' not in str(path):
246            raise e
247
248    return path
249
250
251def do_simple_name_matching(elf, symbol_dict, processed):
252    """
253    Sequentially process DIEs in compiler units with direct file mappings
254    within the DIEs themselves, and do simply matching between DIE names
255    and symbol names.
256    """
257    mapped_symbols = processed['mapped_symbols']
258    mapped_addresses = processed['mapped_addr']
259    unmapped_symbols = processed['unmapped_symbols']
260    newly_mapped_syms = set()
261
262    dwarfinfo = elf.get_dwarf_info()
263    location_lists = dwarfinfo.location_lists()
264    location_parser = LocationParser(location_lists)
265
266    unmapped_dies = set()
267
268    # Loop through all compile units
269    for compile_unit in dwarfinfo.iter_CUs():
270        lineprog = dwarfinfo.line_program_for_CU(compile_unit)
271        if lineprog is None:
272            continue
273
274        # Loop through each DIE and find variables and
275        # subprograms (i.e. functions)
276        for die in compile_unit.iter_DIEs():
277            sym_name = None
278
279            # Process variables
280            if die.tag == 'DW_TAG_variable':
281                # DW_AT_declaration
282
283                # having 'DW_AT_location' means this maps
284                # to an actual address (e.g. not an extern)
285                if 'DW_AT_location' in die.attributes:
286                    sym_name = die.get_full_path()
287
288            # Process subprograms (i.e. functions) if they are valid
289            if die.tag == 'DW_TAG_subprogram':
290                # Refer to another DIE for name
291                if ('DW_AT_abstract_origin' in die.attributes) or (
292                        'DW_AT_specification' in die.attributes):
293                    unmapped_dies.add(die)
294
295                # having 'DW_AT_low_pc' means it maps to
296                # an actual address
297                elif 'DW_AT_low_pc' in die.attributes:
298                    # DW_AT_low_pc == 0 is a weak function
299                    # which has been overriden
300                    if die.attributes['DW_AT_low_pc'].value != 0:
301                        sym_name = die.get_full_path()
302
303                # For mangled function names, the linkage name
304                # is what appears in the symbol list
305                if 'DW_AT_linkage_name' in die.attributes:
306                    linkage = die.attributes['DW_AT_linkage_name']
307                    sym_name = linkage.value.decode()
308
309            if sym_name is not None:
310                # Skip DIE with no reference back to a file
311                if not 'DW_AT_decl_file' in die.attributes:
312                    continue
313
314                is_die_mapped = False
315                if sym_name in symbol_dict:
316                    mapped_symbols.add(sym_name)
317                    symlist = symbol_dict[sym_name]
318                    symbol = match_symbol_address(symlist, die,
319                                                  location_parser,
320                                                  dwarfinfo)
321
322                    if symbol is not None:
323                        symaddr = symbol['symbol']['st_value']
324                        if symaddr not in mapped_addresses:
325                            is_die_mapped = True
326                            path = get_die_filename(die, lineprog)
327                            symbol['mapped_files'].add(path)
328                            mapped_addresses.add(symaddr)
329                            newly_mapped_syms.add(sym_name)
330
331                if not is_die_mapped:
332                    unmapped_dies.add(die)
333
334    mapped_symbols = mapped_symbols.union(newly_mapped_syms)
335    unmapped_symbols = unmapped_symbols.difference(newly_mapped_syms)
336
337    processed['mapped_symbols'] = mapped_symbols
338    processed['mapped_addr'] = mapped_addresses
339    processed['unmapped_symbols'] = unmapped_symbols
340    processed['unmapped_dies'] = unmapped_dies
341
342
343def mark_address_aliases(symbol_dict, processed):
344    """
345    Mark symbol aliases as already mapped to prevent
346    double counting.
347
348    There are functions and variables which are aliases to
349    other functions/variables. So this marks them as mapped
350    so they will not get counted again when a tree is being
351    built for display.
352    """
353    mapped_symbols = processed['mapped_symbols']
354    mapped_addresses = processed['mapped_addr']
355    unmapped_symbols = processed['unmapped_symbols']
356    already_mapped_syms = set()
357
358    for ums in unmapped_symbols:
359        for one_sym in symbol_dict[ums]:
360            symbol = one_sym['symbol']
361            if symbol['st_value'] in mapped_addresses:
362                already_mapped_syms.add(ums)
363
364    mapped_symbols = mapped_symbols.union(already_mapped_syms)
365    unmapped_symbols = unmapped_symbols.difference(already_mapped_syms)
366
367    processed['mapped_symbols'] = mapped_symbols
368    processed['mapped_addr'] = mapped_addresses
369    processed['unmapped_symbols'] = unmapped_symbols
370
371
372def do_address_range_matching(elf, symbol_dict, processed):
373    """
374    Match symbols indirectly using address ranges.
375
376    This uses the address ranges of DIEs and map them to symbols
377    residing within those ranges, and works on DIEs that have not
378    been mapped in previous steps. This works on symbol names
379    that do not match the names in DIEs, e.g. "<func>" in DIE,
380    but "<func>.constprop.*" in symbol name list. This also
381    helps with mapping the mangled function names in C++,
382    since the names in DIE are actual function names in source
383    code and not mangled version of them.
384    """
385    if 'unmapped_dies' not in processed:
386        return
387
388    mapped_symbols = processed['mapped_symbols']
389    mapped_addresses = processed['mapped_addr']
390    unmapped_symbols = processed['unmapped_symbols']
391    newly_mapped_syms = set()
392
393    dwarfinfo = elf.get_dwarf_info()
394    location_lists = dwarfinfo.location_lists()
395    location_parser = LocationParser(location_lists)
396
397    unmapped_dies = processed['unmapped_dies']
398
399    # Group DIEs by compile units
400    cu_list = dict()
401
402    for die in unmapped_dies:
403        cu = die.cu
404        if cu not in cu_list:
405            cu_list[cu] = {'dies': set()}
406        cu_list[cu]['dies'].add(die)
407
408    # Loop through all compile units
409    for cu in cu_list:
410        lineprog = dwarfinfo.line_program_for_CU(cu)
411
412        # Map offsets from DIEs
413        offset_map = dict()
414        for die in cu.iter_DIEs():
415            offset_map[die.offset] = die
416
417        for die in cu_list[cu]['dies']:
418            if not die.tag == 'DW_TAG_subprogram':
419                continue
420
421            path = None
422
423            # Has direct reference to file, so use it
424            if 'DW_AT_decl_file' in die.attributes:
425                path = get_die_filename(die, lineprog)
426
427            # Loop through indirect reference until a direct
428            # reference to file is found
429            if ('DW_AT_abstract_origin' in die.attributes) or (
430                    'DW_AT_specification' in die.attributes):
431                die_ptr = die
432                while path is None:
433                    if not (die_ptr.tag == 'DW_TAG_subprogram') or not (
434                            ('DW_AT_abstract_origin' in die_ptr.attributes) or
435                            ('DW_AT_specification' in die_ptr.attributes)):
436                        break
437
438                    if 'DW_AT_abstract_origin' in die_ptr.attributes:
439                        ofname = 'DW_AT_abstract_origin'
440                    elif 'DW_AT_specification' in die_ptr.attributes:
441                        ofname = 'DW_AT_specification'
442
443                    offset = die_ptr.attributes[ofname].value
444                    offset += die_ptr.cu.cu_offset
445
446                    # There is nothing to reference so no need to continue
447                    if offset not in offset_map:
448                        break
449
450                    die_ptr = offset_map[offset]
451                    if 'DW_AT_decl_file' in die_ptr.attributes:
452                        path = get_die_filename(die_ptr, lineprog)
453
454            # Nothing to map
455            if path is not None:
456                low, high = get_die_mapped_address(die, location_parser,
457                                                   dwarfinfo)
458                if low is None:
459                    continue
460
461                for ums in unmapped_symbols:
462                    for one_sym in symbol_dict[ums]:
463                        symbol = one_sym['symbol']
464                        symaddr = symbol['st_value']
465
466                        if symaddr not in mapped_addresses:
467                            if low <= symaddr < high:
468                                one_sym['mapped_files'].add(path)
469                                mapped_addresses.add(symaddr)
470                                newly_mapped_syms.add(ums)
471
472    mapped_symbols = mapped_symbols.union(newly_mapped_syms)
473    unmapped_symbols = unmapped_symbols.difference(newly_mapped_syms)
474
475    processed['mapped_symbols'] = mapped_symbols
476    processed['mapped_addr'] = mapped_addresses
477    processed['unmapped_symbols'] = unmapped_symbols
478
479
480def set_root_path_for_unmapped_symbols(symbol_dict, addr_range, processed):
481    """
482    Set root path for unmapped symbols.
483
484    Any unmapped symbols are added under the root node if those
485    symbols reside within the desired memory address ranges
486    (e.g. ROM or RAM).
487    """
488    mapped_symbols = processed['mapped_symbols']
489    mapped_addresses = processed['mapped_addr']
490    unmapped_symbols = processed['unmapped_symbols']
491    newly_mapped_syms = set()
492
493    for ums in unmapped_symbols:
494        for one_sym in symbol_dict[ums]:
495            symbol = one_sym['symbol']
496            symaddr = symbol['st_value']
497
498            if is_symbol_in_ranges(symbol, addr_range):
499                if symaddr not in mapped_addresses:
500                    path = Path(':')
501                    one_sym['mapped_files'].add(path)
502                    mapped_addresses.add(symaddr)
503                    newly_mapped_syms.add(ums)
504
505    mapped_symbols = mapped_symbols.union(newly_mapped_syms)
506    unmapped_symbols = unmapped_symbols.difference(newly_mapped_syms)
507
508    processed['mapped_symbols'] = mapped_symbols
509    processed['mapped_addr'] = mapped_addresses
510    processed['unmapped_symbols'] = unmapped_symbols
511
512def find_common_path_prefix(symbol_dict):
513    """
514    Find the common path prefix of all mapped files.
515    Must be called before set_root_path_for_unmapped_symbols().
516    """
517    paths = list()
518
519    for _, sym in symbol_dict.items():
520        for symbol in sym:
521            for file in symbol['mapped_files']:
522                paths.append(file)
523
524    try:
525        return os.path.commonpath(paths)
526    except ValueError:
527        return None
528
529class TreeNode(NodeMixin):
530    """
531    A symbol node.
532    """
533
534    def __init__(self, name, identifier, size=0, parent=None, children=None):
535        super().__init__()
536        self._name = name
537        self._size = size
538        self.parent = parent
539        self._identifier = identifier
540        if children:
541            self.children = children
542
543    def __repr__(self):
544        return self._name
545
546
547def sum_node_children_size(node):
548    """
549    Calculate the sum of symbol size of all direct children.
550    """
551    size = 0
552
553    for child in node.children:
554        size += child._size
555
556    return size
557
558
559def generate_any_tree(symbol_dict, total_size, path_prefix):
560    """
561    Generate a symbol tree for output.
562    """
563    root = TreeNode('Root', "root")
564    node_no_paths = TreeNode('(no paths)', ":", parent=root)
565
566    if path_prefix and Path(path_prefix) == Path(args.zephyrbase):
567        # All source files are under ZEPHYR_BASE so there is
568        # no need for another level.
569        node_zephyr_base = root
570        node_output_dir = root
571        node_workspace = root
572        node_others = root
573    else:
574        node_zephyr_base = TreeNode('ZEPHYR_BASE', args.zephyrbase)
575        node_output_dir = TreeNode('OUTPUT_DIR', args.output)
576        node_others = TreeNode("/", "/")
577
578        if args.workspace:
579            node_workspace = TreeNode('WORKSPACE', args.workspace)
580        else:
581            node_workspace = node_others
582
583    # A set of helper function for building a simple tree with a path-like
584    # hierarchy.
585    def _insert_one_elem(root, path, size):
586        cur = None
587        node = None
588        parent = root
589        for part in path.parts:
590            if cur is None:
591                cur = part
592            else:
593                cur = str(Path(cur, part))
594
595            results = findall_by_attr(root, cur, name="_identifier")
596            if results:
597                item = results[0]
598                item._size += size
599                parent = item
600            else:
601                if node:
602                    parent = node
603                node = TreeNode(name=str(part), identifier=cur, size=size, parent=parent)
604
605    # Mapping paths to tree nodes
606    path_node_map = [
607        [Path(args.zephyrbase), node_zephyr_base],
608        [Path(args.output), node_output_dir],
609    ]
610
611    if args.workspace:
612        path_node_map.append(
613            [Path(args.workspace), node_workspace]
614        )
615
616    for name, sym in symbol_dict.items():
617        for symbol in sym:
618            size = get_symbol_size(symbol['symbol'])
619            for file in symbol['mapped_files']:
620                path = Path(file, name)
621                if path.is_absolute():
622                    has_node = False
623
624                    for one_path in path_node_map:
625                        if one_path[0] in path.parents:
626                            path = path.relative_to(one_path[0])
627                            dest_node = one_path[1]
628                            has_node = True
629                            break
630
631                    if not has_node:
632                        dest_node = node_others
633                else:
634                    dest_node = node_no_paths
635
636                _insert_one_elem(dest_node, path, size)
637
638
639    if node_zephyr_base is not root:
640        # ZEPHYR_BASE and OUTPUT_DIR nodes don't have sum of symbol size
641        # so calculate them here.
642        node_zephyr_base._size = sum_node_children_size(node_zephyr_base)
643        node_output_dir._size = sum_node_children_size(node_output_dir)
644
645        # Find out which nodes need to be in the tree.
646        # "(no path)", ZEPHYR_BASE nodes are essential.
647        children = [node_no_paths, node_zephyr_base]
648        if node_output_dir.height != 0:
649            # OUTPUT_DIR may be under ZEPHYR_BASE.
650            children.append(node_output_dir)
651        if node_others.height != 0:
652            # Only include "others" node if there is something.
653            children.append(node_others)
654
655        if args.workspace:
656            node_workspace._size = sum_node_children_size(node_workspace)
657            if node_workspace.height != 0:
658                children.append(node_workspace)
659
660        root.children = children
661
662    root._size = total_size
663
664    # Need to account for code and data where there are not emitted
665    # symbols associated with them.
666    node_hidden_syms = TreeNode('(hidden)', "(hidden)", parent=root)
667    node_hidden_syms._size = root._size - sum_node_children_size(root)
668
669    return root
670
671
672def node_sort(items):
673    """
674    Node sorting used with RenderTree.
675    """
676    return sorted(items, key=lambda item: item._name)
677
678
679def print_any_tree(root, total_size, depth):
680    """
681    Print the symbol tree.
682    """
683    print('{:101s} {:7s} {:8s}'.format(
684        Fore.YELLOW + "Path", "Size", "%" + Fore.RESET))
685    print('=' * 110)
686    for row in RenderTree(root, childiter=node_sort, maxlevel=depth):
687        f = len(row.pre) + len(row.node._name)
688        s = str(row.node._size).rjust(100-f)
689        percent = 100 * float(row.node._size) / float(total_size)
690
691        cc = cr = ""
692        if not row.node.children:
693            if row.node._name != "(hidden)":
694                cc = Fore.CYAN
695                cr = Fore.RESET
696        elif row.node._name.endswith(SRC_FILE_EXT):
697            cc = Fore.GREEN
698            cr = Fore.RESET
699
700        print(f"{row.pre}{cc}{row.node._name} {s} {cr}{Fore.BLUE}{percent:6.2f}%{Fore.RESET}")
701    print('=' * 110)
702    print(f'{total_size:>101}')
703
704
705def parse_args():
706    """
707    Parse command line arguments.
708    """
709    global args
710
711    parser = argparse.ArgumentParser(allow_abbrev=False)
712
713    parser.add_argument("-k", "--kernel", required=True,
714                        help="Zephyr ELF binary")
715    parser.add_argument("-z", "--zephyrbase", required=True,
716                        help="Zephyr base path")
717    parser.add_argument("-q", "--quiet", action="store_true",
718                        help="Do not output anything on the screen.")
719    parser.add_argument("-o", "--output", required=True,
720                        help="Output path")
721    parser.add_argument("-w", "--workspace", default=None,
722                        help="Workspace path (Usually the same as WEST_TOPDIR)")
723    parser.add_argument("target", choices=['rom', 'ram', 'all'])
724    parser.add_argument("-d", "--depth", dest="depth",
725                        type=int, default=None,
726                        help="How deep should we go into the tree",
727                        metavar="DEPTH")
728    parser.add_argument("-v", "--verbose", action="store_true",
729                        help="Print extra debugging information")
730    parser.add_argument("--json", help="store results in a JSON file.")
731    args = parser.parse_args()
732
733
734def main():
735    """
736    Main program.
737    """
738    parse_args()
739
740    sys.stdout.reconfigure(encoding='utf-8')
741
742    # Init colorama
743    init()
744
745    assert os.path.exists(args.kernel), "{0} does not exist.".format(args.kernel)
746    if args.target == 'ram':
747        targets = ['ram']
748    elif args.target == 'rom':
749        targets = ['rom']
750    elif args.target == 'all':
751        targets = ['rom', 'ram']
752
753    for t in targets:
754
755        elf = ELFFile(open(args.kernel, "rb"))
756
757        assert elf.has_dwarf_info(), "ELF file has no DWARF information"
758
759        set_global_machine_arch(elf.get_machine_arch())
760
761        addr_ranges = get_section_ranges(elf)
762
763        symbols = get_symbols(elf, addr_ranges)
764
765        for sym in symbols['unassigned'].values():
766            print("WARN: Symbol '{0}' is not in RAM or ROM".format(sym['name']))
767
768        symbol_dict = None
769
770        if args.json:
771            jsonout = args.json
772        else:
773            jsonout = os.path.join(args.output, f'{t}.json')
774
775        symbol_dict = symbols[t]
776        symsize = addr_ranges[f'{t}_total_size']
777        ranges = addr_ranges[t]
778
779        if symbol_dict is not None:
780            processed = {"mapped_symbols": set(),
781                         "mapped_addr": set(),
782                         "unmapped_symbols": set(symbol_dict.keys())}
783
784            do_simple_name_matching(elf, symbol_dict, processed)
785            mark_address_aliases(symbol_dict, processed)
786            do_address_range_matching(elf, symbol_dict, processed)
787            mark_address_aliases(symbol_dict, processed)
788            common_path_prefix = find_common_path_prefix(symbol_dict)
789            set_root_path_for_unmapped_symbols(symbol_dict, ranges, processed)
790
791            if args.verbose:
792                for sym in processed['unmapped_symbols']:
793                    print("INFO: Unmapped symbol: {0}".format(sym))
794
795            root = generate_any_tree(symbol_dict, symsize, common_path_prefix)
796            if not args.quiet:
797                print_any_tree(root, symsize, args.depth)
798
799            exporter = DictExporter(attriter=lambda attrs: [(k.lstrip('_'), v) for k, v in attrs])
800            data = dict()
801            data["symbols"] = exporter.export(root)
802            data["total_size"] = symsize
803            with open(jsonout, "w") as fp:
804                json.dump(data, fp, indent=4)
805
806
807if __name__ == "__main__":
808    main()
809