You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nuttx.apache.org by xi...@apache.org on 2021/12/03 14:17:03 UTC

[incubator-nuttx] branch master updated (06fc3eb -> 5ed85ef)

This is an automated email from the ASF dual-hosted git repository.

xiaoxiang pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-nuttx.git.


    from 06fc3eb  input/touchscreen: added touchscreen gesture
     new 8777279  tools: Add size report script
     new 5ed85ef  tools: Adapt Zephyr/zephyr to Nuttx/nuttx

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 LICENSE           |   1 +
 tools/size_report | 804 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 805 insertions(+)
 create mode 100755 tools/size_report

[incubator-nuttx] 02/02: tools: Adapt Zephyr/zephyr to Nuttx/nuttx

Posted by xi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

xiaoxiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-nuttx.git

commit 5ed85ef47696d7cb7f17a327522034552a84bd49
Author: Lingao Meng <me...@xiaomi.com>
AuthorDate: Fri Dec 3 10:31:06 2021 +0800

    tools: Adapt Zephyr/zephyr to Nuttx/nuttx
    
    Adapt Zephyr/zephyr to Nuttx/nuttx
    
    Signed-off-by: Lingao Meng <me...@xiaomi.com>
---
 tools/size_report | 31 ++++++++++++++++---------------
 1 file changed, 16 insertions(+), 15 deletions(-)

diff --git a/tools/size_report b/tools/size_report
index 7b4ae7d..de1464c 100755
--- a/tools/size_report
+++ b/tools/size_report
@@ -1,5 +1,6 @@
 #!/usr/bin/env python3
 #
+# Copyright (c) 2021 Xiaomi Corporation
 # Copyright (c) 2016, 2020 Intel Corporation
 #
 # SPDX-License-Identifier: Apache-2.0
@@ -236,7 +237,7 @@ def get_die_filename(die, lineprog):
         output = Path(args.output)
         path = output.joinpath(path)
 
-    # Change path to relative to Zephyr base
+    # Change path to relative to Nuttx base
     try:
         path = path.resolve()
     except OSError as e:
@@ -560,15 +561,15 @@ def generate_any_tree(symbol_dict, total_size, path_prefix):
     root = TreeNode('Root', "root")
     node_no_paths = TreeNode('(no paths)', ":", parent=root)
 
-    if Path(path_prefix) == Path(args.zephyrbase):
-        # All source files are under ZEPHYR_BASE so there is
+    if Path(path_prefix) == Path(args.nuttxbase):
+        # All source files are under nuttx_base so there is
         # no need for another level.
-        node_zephyr_base = root
+        node_nuttx_base = root
         node_output_dir = root
         node_workspace = root
         node_others = root
     else:
-        node_zephyr_base = TreeNode('ZEPHYR_BASE', args.zephyrbase)
+        node_nuttx_base = TreeNode('nuttx_base', args.nuttxbase)
         node_output_dir = TreeNode('OUTPUT_DIR', args.output)
         node_others = TreeNode("/", "/")
 
@@ -601,7 +602,7 @@ def generate_any_tree(symbol_dict, total_size, path_prefix):
 
     # Mapping paths to tree nodes
     path_node_map = [
-        [Path(args.zephyrbase), node_zephyr_base],
+        [Path(args.nuttxbase), node_nuttx_base],
         [Path(args.output), node_output_dir],
     ]
 
@@ -633,17 +634,17 @@ def generate_any_tree(symbol_dict, total_size, path_prefix):
                 _insert_one_elem(dest_node, path, size)
 
 
-    if node_zephyr_base is not root:
-        # ZEPHYR_BASE and OUTPUT_DIR nodes don't have sum of symbol size
+    if node_nuttx_base is not root:
+        # nuttx_base and OUTPUT_DIR nodes don't have sum of symbol size
         # so calculate them here.
-        node_zephyr_base.size = sum_node_children_size(node_zephyr_base)
+        node_nuttx_base.size = sum_node_children_size(node_nuttx_base)
         node_output_dir.size = sum_node_children_size(node_output_dir)
 
         # Find out which nodes need to be in the tree.
-        # "(no path)", ZEPHYR_BASE nodes are essential.
-        children = [node_no_paths, node_zephyr_base]
+        # "(no path)", nuttx_base nodes are essential.
+        children = [node_no_paths, node_nuttx_base]
         if node_output_dir.height != 0:
-            # OUTPUT_DIR may be under ZEPHYR_BASE.
+            # OUTPUT_DIR may be under nuttx_base.
             children.append(node_output_dir)
         if node_others.height != 0:
             # Only include "others" node if there is something.
@@ -708,9 +709,9 @@ def parse_args():
     parser = argparse.ArgumentParser()
 
     parser.add_argument("-k", "--kernel", required=True,
-                        help="Zephyr ELF binary")
-    parser.add_argument("-z", "--zephyrbase", required=True,
-                        help="Zephyr base path")
+                        help="Nuttx ELF binary")
+    parser.add_argument("-z", "--nuttxbase", required=True,
+                        help="Nuttx base path")
     parser.add_argument("-q", "--quiet", action="store_true",
                         help="Do not output anything on the screen.")
     parser.add_argument("-o", "--output", required=True,

[incubator-nuttx] 01/02: tools: Add size report script

Posted by xi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

xiaoxiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-nuttx.git

commit 87772796e3463472cd62f261e688f17cb8665fbd
Author: Lingao Meng <me...@xiaomi.com>
AuthorDate: Wed Dec 1 14:09:20 2021 +0800

    tools: Add size report script
    
    Add a resource statistics script, which can be used to
    analyze the resource occupation of ELF files, including
    BSS, data, ROM, etc.
    
    Signed-off-by: Lingao Meng <me...@xiaomi.com>
    
    LICENSE: Add size_report to license file
    
    Declare license for intel Corporation.
    
    Signed-off-by: Lingao Meng <me...@xiaomi.com>
---
 LICENSE           |   1 +
 tools/size_report | 803 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 804 insertions(+)

diff --git a/LICENSE b/LICENSE
index 6762630..3f095af 100644
--- a/LICENSE
+++ b/LICENSE
@@ -2397,6 +2397,7 @@ libs/libc/math/lib_copysignf.c
 drivers/wireless/bluetooth/bt_uart.c
 drivers/wireless/bluetooth/bt_uart.h
 wireless/bluetooth
+tools/size_report
 ===========================
    Copyright (c) 2016, Intel Corporation
    All rights reserved.
diff --git a/tools/size_report b/tools/size_report
new file mode 100755
index 0000000..7b4ae7d
--- /dev/null
+++ b/tools/size_report
@@ -0,0 +1,803 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2016, 2020 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+# Based on a script by:
+#       Chereau, Fabien <fa...@intel.com>
+
+"""
+Process an ELF file to generate size report on RAM and ROM.
+"""
+
+import argparse
+import os
+import sys
+import re
+from pathlib import Path
+import json
+
+from packaging import version
+
+from colorama import init, Fore
+
+from anytree import RenderTree, NodeMixin, findall_by_attr
+from anytree.exporter import DictExporter
+
+import elftools
+from elftools.elf.elffile import ELFFile
+from elftools.elf.sections import SymbolTableSection
+from elftools.dwarf.descriptions import describe_form_class
+from elftools.dwarf.descriptions import (
+    describe_DWARF_expr, set_global_machine_arch)
+from elftools.dwarf.locationlists import (
+    LocationExpr, LocationParser)
+
+if version.parse(elftools.__version__) < version.parse('0.24'):
+    sys.exit("pyelftools is out of date, need version 0.24 or later")
+
+
+# ELF section flags
+SHF_WRITE = 0x1
+SHF_ALLOC = 0x2
+SHF_EXEC = 0x4
+SHF_WRITE_ALLOC = SHF_WRITE | SHF_ALLOC
+SHF_ALLOC_EXEC = SHF_ALLOC | SHF_EXEC
+
+DT_LOCATION = re.compile(r"\(DW_OP_addr: ([0-9a-f]+)\)")
+
+SRC_FILE_EXT = ('.h', '.c', '.hpp', '.cpp', '.hxx', '.cxx', '.c++')
+
+
+def get_symbol_addr(sym):
+    """Get the address of a symbol"""
+    return sym['st_value']
+
+
+def get_symbol_size(sym):
+    """Get the size of a symbol"""
+    return sym['st_size']
+
+
+def is_symbol_in_ranges(sym, ranges):
+    """
+    Given a list of start/end addresses, test if the symbol
+    lies within any of these address ranges.
+    """
+    for bound in ranges:
+        if bound['start'] <= sym['st_value'] <= bound['end']:
+            return True
+
+    return False
+
+
+def get_die_mapped_address(die, parser, dwarfinfo):
+    """Get the bounding addresses from a DIE variable or subprogram"""
+    low = None
+    high = None
+
+    if die.tag == 'DW_TAG_variable':
+        if 'DW_AT_location' in die.attributes:
+            loc_attr = die.attributes['DW_AT_location']
+            if parser.attribute_has_location(loc_attr, die.cu['version']):
+                loc = parser.parse_from_attribute(loc_attr, die.cu['version'])
+                if isinstance(loc, LocationExpr):
+                    addr = describe_DWARF_expr(loc.loc_expr,
+                                               dwarfinfo.structs)
+
+                    matcher = DT_LOCATION.match(addr)
+                    if matcher:
+                        low = int(matcher.group(1), 16)
+                        high = low + 1
+
+    if die.tag == 'DW_TAG_subprogram':
+        if 'DW_AT_low_pc' in die.attributes:
+            low = die.attributes['DW_AT_low_pc'].value
+
+            high_pc = die.attributes['DW_AT_high_pc']
+            high_pc_class = describe_form_class(high_pc.form)
+            if high_pc_class == 'address':
+                high = high_pc.value
+            elif high_pc_class == 'constant':
+                high = low + high_pc.value
+
+    return low, high
+
+
+def match_symbol_address(symlist, die, parser, dwarfinfo):
+    """
+    Find the symbol from a symbol list
+    where it matches the address in DIE variable,
+    or within the range of a DIE subprogram.
+    """
+    low, high = get_die_mapped_address(die, parser, dwarfinfo)
+
+    if low is None:
+        return None
+
+    for sym in symlist:
+        if low <= sym['symbol']['st_value'] < high:
+            return sym
+
+    return None
+
+
+def get_symbols(elf, addr_ranges):
+    """
+    Fetch the symbols from the symbol table and put them
+    into ROM, RAM buckets.
+    """
+    rom_syms = dict()
+    ram_syms = dict()
+    unassigned_syms = dict()
+
+    rom_addr_ranges = addr_ranges['rom']
+    ram_addr_ranges = addr_ranges['ram']
+
+    for section in elf.iter_sections():
+        if isinstance(section, SymbolTableSection):
+            for sym in section.iter_symbols():
+                # Ignore symbols with size == 0
+                if get_symbol_size(sym) == 0:
+                    continue
+
+                found_sec = False
+                entry = {'name': sym.name,
+                         'symbol': sym,
+                         'mapped_files': set()}
+
+                # If symbol is in ROM area?
+                if is_symbol_in_ranges(sym, rom_addr_ranges):
+                    if sym.name not in rom_syms:
+                        rom_syms[sym.name] = list()
+                    rom_syms[sym.name].append(entry)
+                    found_sec = True
+
+                # If symbol is in RAM area?
+                if is_symbol_in_ranges(sym, ram_addr_ranges):
+                    if sym.name not in ram_syms:
+                        ram_syms[sym.name] = list()
+                    ram_syms[sym.name].append(entry)
+                    found_sec = True
+
+                if not found_sec:
+                    unassigned_syms['sym_name'] = entry
+
+    ret = {'rom': rom_syms,
+           'ram': ram_syms,
+           'unassigned': unassigned_syms}
+    return ret
+
+
+def get_section_ranges(elf):
+    """
+    Parse ELF header to find out the address ranges of ROM or RAM sections
+    and their total sizes.
+    """
+    rom_addr_ranges = list()
+    ram_addr_ranges = list()
+    rom_size = 0
+    ram_size = 0
+
+    for section in elf.iter_sections():
+        size = section['sh_size']
+        sec_start = section['sh_addr']
+        sec_end = sec_start + size - 1
+        bound = {'start': sec_start, 'end': sec_end}
+
+        if section['sh_type'] == 'SHT_NOBITS':
+            # BSS and noinit sections
+            ram_addr_ranges.append(bound)
+            ram_size += size
+        elif section['sh_type'] == 'SHT_PROGBITS':
+            # Sections to be in flash or memory
+            flags = section['sh_flags']
+            if (flags & SHF_ALLOC_EXEC) == SHF_ALLOC_EXEC:
+                # Text section
+                rom_addr_ranges.append(bound)
+                rom_size += size
+            elif (flags & SHF_WRITE_ALLOC) == SHF_WRITE_ALLOC:
+                # Data occupies both ROM and RAM
+                # since at boot, content is copied from ROM to RAM
+                rom_addr_ranges.append(bound)
+                rom_size += size
+
+                ram_addr_ranges.append(bound)
+                ram_size += size
+            elif (flags & SHF_ALLOC) == SHF_ALLOC:
+                # Read only data
+                rom_addr_ranges.append(bound)
+                rom_size += size
+
+    ret = {'rom': rom_addr_ranges,
+           'rom_total_size': rom_size,
+           'ram': ram_addr_ranges,
+           'ram_total_size': ram_size}
+    return ret
+
+
+def get_die_filename(die, lineprog):
+    """Get the source code filename associated with a DIE"""
+    file_index = die.attributes['DW_AT_decl_file'].value
+    file_entry = lineprog['file_entry'][file_index - 1]
+
+    dir_index = file_entry['dir_index']
+    if dir_index == 0:
+        filename = file_entry.name
+    else:
+        directory = lineprog.header['include_directory'][dir_index - 1]
+        filename = os.path.join(directory, file_entry.name)
+
+    path = Path(filename.decode())
+
+    # Prepend output path to relative path
+    if not path.is_absolute():
+        output = Path(args.output)
+        path = output.joinpath(path)
+
+    # Change path to relative to Zephyr base
+    try:
+        path = path.resolve()
+    except OSError as e:
+        # built-ins can't be resolved, so it's not an issue
+        if '<built-in>' not in str(path):
+            raise e
+
+    return path
+
+
+def do_simple_name_matching(elf, symbol_dict, processed):
+    """
+    Sequentially process DIEs in compiler units with direct file mappings
+    within the DIEs themselves, and do simply matching between DIE names
+    and symbol names.
+    """
+    mapped_symbols = processed['mapped_symbols']
+    mapped_addresses = processed['mapped_addr']
+    unmapped_symbols = processed['unmapped_symbols']
+    newly_mapped_syms = set()
+
+    dwarfinfo = elf.get_dwarf_info()
+    location_lists = dwarfinfo.location_lists()
+    location_parser = LocationParser(location_lists)
+
+    unmapped_dies = set()
+
+    # Loop through all compile units
+    for compile_unit in dwarfinfo.iter_CUs():
+        lineprog = dwarfinfo.line_program_for_CU(compile_unit)
+        if lineprog is None:
+            continue
+
+        # Loop through each DIE and find variables and
+        # subprograms (i.e. functions)
+        for die in compile_unit.iter_DIEs():
+            sym_name = None
+
+            # Process variables
+            if die.tag == 'DW_TAG_variable':
+                # DW_AT_declaration
+
+                # having 'DW_AT_location' means this maps
+                # to an actual address (e.g. not an extern)
+                if 'DW_AT_location' in die.attributes:
+                    sym_name = die.get_full_path()
+
+            # Process subprograms (i.e. functions) if they are valid
+            if die.tag == 'DW_TAG_subprogram':
+                # Refer to another DIE for name
+                if ('DW_AT_abstract_origin' in die.attributes) or (
+                        'DW_AT_specification' in die.attributes):
+                    unmapped_dies.add(die)
+
+                # having 'DW_AT_low_pc' means it maps to
+                # an actual address
+                elif 'DW_AT_low_pc' in die.attributes:
+                    # DW_AT_low_pc == 0 is a weak function
+                    # which has been overriden
+                    if die.attributes['DW_AT_low_pc'].value != 0:
+                        sym_name = die.get_full_path()
+
+                # For mangled function names, the linkage name
+                # is what appears in the symbol list
+                if 'DW_AT_linkage_name' in die.attributes:
+                    linkage = die.attributes['DW_AT_linkage_name']
+                    sym_name = linkage.value.decode()
+
+            if sym_name is not None:
+                # Skip DIE with no reference back to a file
+                if not 'DW_AT_decl_file' in die.attributes:
+                    continue
+
+                is_die_mapped = False
+                if sym_name in symbol_dict:
+                    mapped_symbols.add(sym_name)
+                    symlist = symbol_dict[sym_name]
+                    symbol = match_symbol_address(symlist, die,
+                                                  location_parser,
+                                                  dwarfinfo)
+
+                    if symbol is not None:
+                        symaddr = symbol['symbol']['st_value']
+                        if symaddr not in mapped_addresses:
+                            is_die_mapped = True
+                            path = get_die_filename(die, lineprog)
+                            symbol['mapped_files'].add(path)
+                            mapped_addresses.add(symaddr)
+                            newly_mapped_syms.add(sym_name)
+
+                if not is_die_mapped:
+                    unmapped_dies.add(die)
+
+    mapped_symbols = mapped_symbols.union(newly_mapped_syms)
+    unmapped_symbols = unmapped_symbols.difference(newly_mapped_syms)
+
+    processed['mapped_symbols'] = mapped_symbols
+    processed['mapped_addr'] = mapped_addresses
+    processed['unmapped_symbols'] = unmapped_symbols
+    processed['unmapped_dies'] = unmapped_dies
+
+
+def mark_address_aliases(symbol_dict, processed):
+    """
+    Mark symbol aliases as already mapped to prevent
+    double counting.
+
+    There are functions and variables which are aliases to
+    other functions/variables. So this marks them as mapped
+    so they will not get counted again when a tree is being
+    built for display.
+    """
+    mapped_symbols = processed['mapped_symbols']
+    mapped_addresses = processed['mapped_addr']
+    unmapped_symbols = processed['unmapped_symbols']
+    already_mapped_syms = set()
+
+    for ums in unmapped_symbols:
+        for one_sym in symbol_dict[ums]:
+            symbol = one_sym['symbol']
+            if symbol['st_value'] in mapped_addresses:
+                already_mapped_syms.add(ums)
+
+    mapped_symbols = mapped_symbols.union(already_mapped_syms)
+    unmapped_symbols = unmapped_symbols.difference(already_mapped_syms)
+
+    processed['mapped_symbols'] = mapped_symbols
+    processed['mapped_addr'] = mapped_addresses
+    processed['unmapped_symbols'] = unmapped_symbols
+
+
+def do_address_range_matching(elf, symbol_dict, processed):
+    """
+    Match symbols indirectly using address ranges.
+
+    This uses the address ranges of DIEs and map them to symbols
+    residing within those ranges, and works on DIEs that have not
+    been mapped in previous steps. This works on symbol names
+    that do not match the names in DIEs, e.g. "<func>" in DIE,
+    but "<func>.constprop.*" in symbol name list. This also
+    helps with mapping the mangled function names in C++,
+    since the names in DIE are actual function names in source
+    code and not mangled version of them.
+    """
+    if 'unmapped_dies' not in processed:
+        return
+
+    mapped_symbols = processed['mapped_symbols']
+    mapped_addresses = processed['mapped_addr']
+    unmapped_symbols = processed['unmapped_symbols']
+    newly_mapped_syms = set()
+
+    dwarfinfo = elf.get_dwarf_info()
+    location_lists = dwarfinfo.location_lists()
+    location_parser = LocationParser(location_lists)
+
+    unmapped_dies = processed['unmapped_dies']
+
+    # Group DIEs by compile units
+    cu_list = dict()
+
+    for die in unmapped_dies:
+        cu = die.cu
+        if cu not in cu_list:
+            cu_list[cu] = {'dies': set()}
+        cu_list[cu]['dies'].add(die)
+
+    # Loop through all compile units
+    for cu in cu_list:
+        lineprog = dwarfinfo.line_program_for_CU(cu)
+
+        # Map offsets from DIEs
+        offset_map = dict()
+        for die in cu.iter_DIEs():
+            offset_map[die.offset] = die
+
+        for die in cu_list[cu]['dies']:
+            if not die.tag == 'DW_TAG_subprogram':
+                continue
+
+            path = None
+
+            # Has direct reference to file, so use it
+            if 'DW_AT_decl_file' in die.attributes:
+                path = get_die_filename(die, lineprog)
+
+            # Loop through indirect reference until a direct
+            # reference to file is found
+            if ('DW_AT_abstract_origin' in die.attributes) or (
+                    'DW_AT_specification' in die.attributes):
+                die_ptr = die
+                while path is None:
+                    if not (die_ptr.tag == 'DW_TAG_subprogram') or not (
+                            ('DW_AT_abstract_origin' in die_ptr.attributes) or
+                            ('DW_AT_specification' in die_ptr.attributes)):
+                        break
+
+                    if 'DW_AT_abstract_origin' in die_ptr.attributes:
+                        ofname = 'DW_AT_abstract_origin'
+                    elif 'DW_AT_specification' in die_ptr.attributes:
+                        ofname = 'DW_AT_specification'
+
+                    offset = die_ptr.attributes[ofname].value
+                    offset += die_ptr.cu.cu_offset
+
+                    # There is nothing to reference so no need to continue
+                    if offset not in offset_map:
+                        break
+
+                    die_ptr = offset_map[offset]
+                    if 'DW_AT_decl_file' in die_ptr.attributes:
+                        path = get_die_filename(die_ptr, lineprog)
+
+            # Nothing to map
+            if path is not None:
+                low, high = get_die_mapped_address(die, location_parser,
+                                                   dwarfinfo)
+                if low is None:
+                    continue
+
+                for ums in unmapped_symbols:
+                    for one_sym in symbol_dict[ums]:
+                        symbol = one_sym['symbol']
+                        symaddr = symbol['st_value']
+
+                        if symaddr not in mapped_addresses:
+                            if low <= symaddr < high:
+                                one_sym['mapped_files'].add(path)
+                                mapped_addresses.add(symaddr)
+                                newly_mapped_syms.add(ums)
+
+    mapped_symbols = mapped_symbols.union(newly_mapped_syms)
+    unmapped_symbols = unmapped_symbols.difference(newly_mapped_syms)
+
+    processed['mapped_symbols'] = mapped_symbols
+    processed['mapped_addr'] = mapped_addresses
+    processed['unmapped_symbols'] = unmapped_symbols
+
+
+def set_root_path_for_unmapped_symbols(symbol_dict, addr_range, processed):
+    """
+    Set root path for unmapped symbols.
+
+    Any unmapped symbols are added under the root node if those
+    symbols reside within the desired memory address ranges
+    (e.g. ROM or RAM).
+    """
+    mapped_symbols = processed['mapped_symbols']
+    mapped_addresses = processed['mapped_addr']
+    unmapped_symbols = processed['unmapped_symbols']
+    newly_mapped_syms = set()
+
+    for ums in unmapped_symbols:
+        for one_sym in symbol_dict[ums]:
+            symbol = one_sym['symbol']
+            symaddr = symbol['st_value']
+
+            if is_symbol_in_ranges(symbol, addr_range):
+                if symaddr not in mapped_addresses:
+                    path = Path(':')
+                    one_sym['mapped_files'].add(path)
+                    mapped_addresses.add(symaddr)
+                    newly_mapped_syms.add(ums)
+
+    mapped_symbols = mapped_symbols.union(newly_mapped_syms)
+    unmapped_symbols = unmapped_symbols.difference(newly_mapped_syms)
+
+    processed['mapped_symbols'] = mapped_symbols
+    processed['mapped_addr'] = mapped_addresses
+    processed['unmapped_symbols'] = unmapped_symbols
+
+def find_common_path_prefix(symbol_dict):
+    """
+    Find the common path prefix of all mapped files.
+    Must be called before set_root_path_for_unmapped_symbols().
+    """
+    paths = list()
+
+    for _, sym in symbol_dict.items():
+        for symbol in sym:
+            for file in symbol['mapped_files']:
+                paths.append(file)
+
+    return os.path.commonpath(paths)
+
+
+class TreeNode(NodeMixin):
+    """
+    A symbol node.
+    """
+
+    def __init__(self, name, identifier, size=0, parent=None, children=None):
+        super().__init__()
+        self.name = name
+        self.size = size
+        self.parent = parent
+        self.identifier = identifier
+        if children:
+            self.children = children
+
+    def __repr__(self):
+        return self.name
+
+
+def sum_node_children_size(node):
+    """
+    Calculate the sum of symbol size of all direct children.
+    """
+    size = 0
+
+    for child in node.children:
+        size += child.size
+
+    return size
+
+
+def generate_any_tree(symbol_dict, total_size, path_prefix):
+    """
+    Generate a symbol tree for output.
+    """
+    root = TreeNode('Root', "root")
+    node_no_paths = TreeNode('(no paths)', ":", parent=root)
+
+    if Path(path_prefix) == Path(args.zephyrbase):
+        # All source files are under ZEPHYR_BASE so there is
+        # no need for another level.
+        node_zephyr_base = root
+        node_output_dir = root
+        node_workspace = root
+        node_others = root
+    else:
+        node_zephyr_base = TreeNode('ZEPHYR_BASE', args.zephyrbase)
+        node_output_dir = TreeNode('OUTPUT_DIR', args.output)
+        node_others = TreeNode("/", "/")
+
+        if args.workspace:
+            node_workspace = TreeNode('WORKSPACE', args.workspace)
+        else:
+            node_workspace = node_others
+
+    # A set of helper function for building a simple tree with a path-like
+    # hierarchy.
+    def _insert_one_elem(root, path, size):
+        cur = None
+        node = None
+        parent = root
+        for part in path.parts:
+            if cur is None:
+                cur = part
+            else:
+                cur = str(Path(cur, part))
+
+            results = findall_by_attr(root, cur, name="identifier")
+            if results:
+                item = results[0]
+                item.size += size
+                parent = item
+            else:
+                if node:
+                    parent = node
+                node = TreeNode(name=str(part), identifier=cur, size=size, parent=parent)
+
+    # Mapping paths to tree nodes
+    path_node_map = [
+        [Path(args.zephyrbase), node_zephyr_base],
+        [Path(args.output), node_output_dir],
+    ]
+
+    if args.workspace:
+        path_node_map.append(
+            [Path(args.workspace), node_workspace]
+        )
+
+    for name, sym in symbol_dict.items():
+        for symbol in sym:
+            size = get_symbol_size(symbol['symbol'])
+            for file in symbol['mapped_files']:
+                path = Path(file, name)
+                if path.is_absolute():
+                    has_node = False
+
+                    for one_path in path_node_map:
+                        if one_path[0] in path.parents:
+                            path = path.relative_to(one_path[0])
+                            dest_node = one_path[1]
+                            has_node = True
+                            break
+
+                    if not has_node:
+                        dest_node = node_others
+                else:
+                    dest_node = node_no_paths
+
+                _insert_one_elem(dest_node, path, size)
+
+
+    if node_zephyr_base is not root:
+        # ZEPHYR_BASE and OUTPUT_DIR nodes don't have sum of symbol size
+        # so calculate them here.
+        node_zephyr_base.size = sum_node_children_size(node_zephyr_base)
+        node_output_dir.size = sum_node_children_size(node_output_dir)
+
+        # Find out which nodes need to be in the tree.
+        # "(no path)", ZEPHYR_BASE nodes are essential.
+        children = [node_no_paths, node_zephyr_base]
+        if node_output_dir.height != 0:
+            # OUTPUT_DIR may be under ZEPHYR_BASE.
+            children.append(node_output_dir)
+        if node_others.height != 0:
+            # Only include "others" node if there is something.
+            children.append(node_others)
+
+        if args.workspace:
+            node_workspace.size = sum_node_children_size(node_workspace)
+            if node_workspace.height != 0:
+                children.append(node_workspace)
+
+        root.children = children
+
+    root.size = total_size
+
+    # Need to account for code and data where there are not emitted
+    # symbols associated with them.
+    node_hidden_syms = TreeNode('(hidden)', "(hidden)", parent=root)
+    node_hidden_syms.size = root.size - sum_node_children_size(root)
+
+    return root
+
+
+def node_sort(items):
+    """
+    Node sorting used with RenderTree.
+    """
+    return sorted(items, key=lambda item: item.name)
+
+
+def print_any_tree(root, total_size, depth):
+    """
+    Print the symbol tree.
+    """
+    print('{:101s} {:7s} {:8s}'.format(
+        Fore.YELLOW + "Path", "Size", "%" + Fore.RESET))
+    print('=' * 110)
+    for row in RenderTree(root, childiter=node_sort, maxlevel=depth):
+        f = len(row.pre) + len(row.node.name)
+        s = str(row.node.size).rjust(100-f)
+        percent = 100 * float(row.node.size) / float(total_size)
+
+        cc = cr = ""
+        if not row.node.children:
+            if row.node.name != "(hidden)":
+                cc = Fore.CYAN
+                cr = Fore.RESET
+        elif row.node.name.endswith(SRC_FILE_EXT):
+            cc = Fore.GREEN
+            cr = Fore.RESET
+
+        print(f"{row.pre}{cc}{row.node.name} {s} {cr}{Fore.BLUE}{percent:6.2f}%{Fore.RESET}")
+    print('=' * 110)
+    print(f'{total_size:>101}')
+
+
+def parse_args():
+    """
+    Parse command line arguments.
+    """
+    global args
+
+    parser = argparse.ArgumentParser()
+
+    parser.add_argument("-k", "--kernel", required=True,
+                        help="Zephyr ELF binary")
+    parser.add_argument("-z", "--zephyrbase", required=True,
+                        help="Zephyr base path")
+    parser.add_argument("-q", "--quiet", action="store_true",
+                        help="Do not output anything on the screen.")
+    parser.add_argument("-o", "--output", required=True,
+                        help="Output path")
+    parser.add_argument("-w", "--workspace", default=None,
+                        help="Workspace path (Usually the same as WEST_TOPDIR)")
+    parser.add_argument("target", choices=['rom', 'ram', 'all'])
+    parser.add_argument("-d", "--depth", dest="depth",
+                        type=int, default=None,
+                        help="How deep should we go into the tree",
+                        metavar="DEPTH")
+    parser.add_argument("-v", "--verbose", action="store_true",
+                        help="Print extra debugging information")
+    parser.add_argument("--json", help="store results in a JSON file.")
+    args = parser.parse_args()
+
+
+def main():
+    """
+    Main program.
+    """
+    parse_args()
+
+    # Init colorama
+    init()
+
+    assert os.path.exists(args.kernel), "{0} does not exist.".format(args.kernel)
+    if args.target == 'ram':
+        targets = ['ram']
+    elif args.target == 'rom':
+        targets = ['rom']
+    elif args.target == 'all':
+        targets = ['rom', 'ram']
+
+    for t in targets:
+
+        elf = ELFFile(open(args.kernel, "rb"))
+
+        assert elf.has_dwarf_info(), "ELF file has no DWARF information"
+
+        set_global_machine_arch(elf.get_machine_arch())
+
+        addr_ranges = get_section_ranges(elf)
+
+        symbols = get_symbols(elf, addr_ranges)
+
+        for sym in symbols['unassigned'].values():
+            print("WARN: Symbol '{0}' is not in RAM or ROM".format(sym['name']))
+
+        symbol_dict = None
+
+        if args.json:
+            jsonout = args.json
+        else:
+            jsonout = os.path.join(args.output, f'{t}.json')
+
+        symbol_dict = symbols[t]
+        symsize = addr_ranges[f'{t}_total_size']
+        ranges = addr_ranges[t]
+
+        if symbol_dict is not None:
+            processed = {"mapped_symbols": set(),
+                         "mapped_addr": set(),
+                         "unmapped_symbols": set(symbol_dict.keys())}
+
+            do_simple_name_matching(elf, symbol_dict, processed)
+            mark_address_aliases(symbol_dict, processed)
+            do_address_range_matching(elf, symbol_dict, processed)
+            mark_address_aliases(symbol_dict, processed)
+            common_path_prefix = find_common_path_prefix(symbol_dict)
+            set_root_path_for_unmapped_symbols(symbol_dict, ranges, processed)
+
+            if args.verbose:
+                for sym in processed['unmapped_symbols']:
+                    print("INFO: Unmapped symbol: {0}".format(sym))
+
+            root = generate_any_tree(symbol_dict, symsize, common_path_prefix)
+            if not args.quiet:
+                print_any_tree(root, symsize, args.depth)
+
+            exporter = DictExporter()
+            data = dict()
+            data["symbols"] = exporter.export(root)
+            data["total_size"] = symsize
+            with open(jsonout, "w") as fp:
+                json.dump(data, fp, indent=4)
+
+
+if __name__ == "__main__":
+    main()