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