| 
 | 1 | +# Copyright (c) Meta Platforms, Inc. and affiliates.  | 
 | 2 | +# All rights reserved.  | 
 | 3 | +#  | 
 | 4 | +# This source code is licensed under the BSD-style license found in the  | 
 | 5 | +# LICENSE file in the root directory of this source tree.  | 
 | 6 | + | 
 | 7 | +import re  | 
 | 8 | +import subprocess  | 
 | 9 | +import sys  | 
 | 10 | +from dataclasses import dataclass  | 
 | 11 | +from typing import Dict, List, Optional, Union  | 
 | 12 | + | 
 | 13 | + | 
 | 14 | +@dataclass  | 
 | 15 | +class Symbol:  | 
 | 16 | +    name: str  | 
 | 17 | +    addr: int  | 
 | 18 | +    size: int  | 
 | 19 | +    symbol_type: str  | 
 | 20 | + | 
 | 21 | + | 
 | 22 | +class Parser:  | 
 | 23 | +    def __init__(self, elf: str, toolchain_prefix: str = "", filter=None):  | 
 | 24 | +        self.elf = elf  | 
 | 25 | +        self.toolchain_prefix = toolchain_prefix  | 
 | 26 | +        self.symbols: Dict[str, Symbol] = self._get_nm_output()  | 
 | 27 | +        self.filter = filter  | 
 | 28 | + | 
 | 29 | +    @staticmethod  | 
 | 30 | +    def run_nm(  | 
 | 31 | +        elf_file_path: str, args: Optional[List[str]] = None, nm: str = "nm"  | 
 | 32 | +    ) -> str:  | 
 | 33 | +        """  | 
 | 34 | +        Run the nm command on the specified ELF file.  | 
 | 35 | +        """  | 
 | 36 | +        args = [] if args is None else args  | 
 | 37 | +        cmd = [nm] + args + [elf_file_path]  | 
 | 38 | +        try:  | 
 | 39 | +            result = subprocess.run(cmd, check=True, capture_output=True, text=True)  | 
 | 40 | +            return result.stdout  | 
 | 41 | +        except FileNotFoundError:  | 
 | 42 | +            print(f"Error: 'nm' command not found. Please ensure it's installed.")  | 
 | 43 | +            sys.exit(1)  | 
 | 44 | +        except subprocess.CalledProcessError as e:  | 
 | 45 | +            print(f"Error running nm on {elf_file_path}: {e}")  | 
 | 46 | +            print(f"stderr: {e.stderr}")  | 
 | 47 | +            sys.exit(1)  | 
 | 48 | + | 
 | 49 | +    def _get_nm_output(self) -> Dict[str, Symbol]:  | 
 | 50 | +        args = [  | 
 | 51 | +            "--print-size",  | 
 | 52 | +            "--size-sort",  | 
 | 53 | +            "--reverse-sort",  | 
 | 54 | +            "--demangle",  | 
 | 55 | +            "--format=bsd",  | 
 | 56 | +        ]  | 
 | 57 | +        output = Parser.run_nm(  | 
 | 58 | +            self.elf,  | 
 | 59 | +            args,  | 
 | 60 | +            nm=self.toolchain_prefix + "nm" if self.toolchain_prefix else "nm",  | 
 | 61 | +        )  | 
 | 62 | +        lines = output.splitlines()  | 
 | 63 | +        symbols = []  | 
 | 64 | +        symbol_pattern = re.compile(  | 
 | 65 | +            r"(?P<addr>[0-9a-fA-F]+)\s+(?P<size>[0-9a-fA-F]+)\s+(?P<type>\w)\s+(?P<name>.+)"  | 
 | 66 | +        )  | 
 | 67 | + | 
 | 68 | +        def parse_line(line: str) -> Optional[Symbol]:  | 
 | 69 | + | 
 | 70 | +            match = symbol_pattern.match(line)  | 
 | 71 | +            if match:  | 
 | 72 | +                addr = int(match.group("addr"), 16)  | 
 | 73 | +                size = int(match.group("size"), 16)  | 
 | 74 | +                type_ = match.group("type").strip().strip("\n")  | 
 | 75 | +                name = match.group("name").strip().strip("\n")  | 
 | 76 | +                return Symbol(name=name, addr=addr, size=size, symbol_type=type_)  | 
 | 77 | +            return None  | 
 | 78 | + | 
 | 79 | +        for line in lines:  | 
 | 80 | +            symbol = parse_line(line)  | 
 | 81 | +            if symbol:  | 
 | 82 | +                symbols.append(symbol)  | 
 | 83 | + | 
 | 84 | +        assert len(symbols) > 0, "No symbols found in nm output"  | 
 | 85 | +        if len(symbols) != len(lines):  | 
 | 86 | +            print(  | 
 | 87 | +                "** Warning: Not all lines were parsed, check the output of nm. Parsed {len(symbols)} lines, given {len(lines)}"  | 
 | 88 | +            )  | 
 | 89 | +        if any(symbol.size == 0 for symbol in symbols):  | 
 | 90 | +            print("** Warning: Some symbols have zero size, check the output of nm.")  | 
 | 91 | + | 
 | 92 | +        # TODO: Populate the section and module fields from the linker map if available (-Wl,-Map=linker.map)  | 
 | 93 | +        return {symbol.name: symbol for symbol in symbols}  | 
 | 94 | + | 
 | 95 | +    def print(self):  | 
 | 96 | +        print(f"Elf: {self.elf}")  | 
 | 97 | + | 
 | 98 | +        def print_table(filter=None, filter_name=None):  | 
 | 99 | +            print("\nAddress\t\tSize\tType\tName")  | 
 | 100 | +            # Apply filter and sort symbols  | 
 | 101 | +            symbols_to_print = {  | 
 | 102 | +                name: sym  | 
 | 103 | +                for name, sym in self.symbols.items()  | 
 | 104 | +                if not filter or filter(sym)  | 
 | 105 | +            }  | 
 | 106 | +            sorted_symbols = sorted(  | 
 | 107 | +                symbols_to_print.items(), key=lambda x: x[1].size, reverse=True  | 
 | 108 | +            )  | 
 | 109 | + | 
 | 110 | +            # Print symbols and calculate total size  | 
 | 111 | +            size_total = 0  | 
 | 112 | +            for name, sym in sorted_symbols:  | 
 | 113 | +                print(f"{hex(sym.addr)}\t\t{sym.size}\t{sym.symbol_type}\t{sym.name}")  | 
 | 114 | +                size_total += sym.size  | 
 | 115 | + | 
 | 116 | +            # Print summary  | 
 | 117 | +            symbol_percent = len(symbols_to_print) / len(self.symbols) * 100  | 
 | 118 | +            print("-----")  | 
 | 119 | +            print(f"> Total bytes: {size_total}")  | 
 | 120 | +            print(  | 
 | 121 | +                f"Counted: {len(symbols_to_print)}/{len(self.symbols)}, {symbol_percent:0.2f}% (filter: '{filter_name}')"  | 
 | 122 | +            )  | 
 | 123 | +            print("=====\n")  | 
 | 124 | + | 
 | 125 | +        # Print tables with different filters  | 
 | 126 | +        def is_executorch_symbol(s):  | 
 | 127 | +            return "executorch" in s.name or s.name.startswith("et")  | 
 | 128 | + | 
 | 129 | +        FILTER_NAME_TO_FILTER_AND_LABEL = {  | 
 | 130 | +            "all": (None, "All"),  | 
 | 131 | +            "executorch": (is_executorch_symbol, "ExecuTorch"),  | 
 | 132 | +            "executorch_text": (  | 
 | 133 | +                lambda s: is_executorch_symbol(s) and s.symbol_type.lower() == "t",  | 
 | 134 | +                "ExecuTorch .text",  | 
 | 135 | +            ),  | 
 | 136 | +        }  | 
 | 137 | + | 
 | 138 | +        filter_func, label = FILTER_NAME_TO_FILTER_AND_LABEL.get(  | 
 | 139 | +            self.filter, FILTER_NAME_TO_FILTER_AND_LABEL["all"]  | 
 | 140 | +        )  | 
 | 141 | +        print_table(filter_func, label)  | 
 | 142 | + | 
 | 143 | + | 
 | 144 | +if __name__ == "__main__":  | 
 | 145 | +    import argparse  | 
 | 146 | + | 
 | 147 | +    parser = argparse.ArgumentParser(  | 
 | 148 | +        description="Process ELF file and linker map file."  | 
 | 149 | +    )  | 
 | 150 | +    parser.add_argument(  | 
 | 151 | +        "-e", "--elf-file-path", required=True, help="Path to the ELF file"  | 
 | 152 | +    )  | 
 | 153 | +    parser.add_argument(  | 
 | 154 | +        "-f",  | 
 | 155 | +        "--filter",  | 
 | 156 | +        required=False,  | 
 | 157 | +        default="all",  | 
 | 158 | +        help="Filter symbols by pre-defined filters",  | 
 | 159 | +        choices=["all", "executorch", "executorch_text"],  | 
 | 160 | +    )  | 
 | 161 | +    parser.add_argument(  | 
 | 162 | +        "-p",  | 
 | 163 | +        "--toolchain-prefix",  | 
 | 164 | +        required=False,  | 
 | 165 | +        default="",  | 
 | 166 | +        help="Optional toolchain prefix for nm",  | 
 | 167 | +    )  | 
 | 168 | + | 
 | 169 | +    args = parser.parse_args()  | 
 | 170 | +    p = Parser(args.elf_file_path, args.toolchain_prefix, filter=args.filter)  | 
 | 171 | +    p.print()  | 
0 commit comments