Skip to content

Commit b765133

Browse files
committed
[ci/scripts] Add run nm scripts
Prints useful tables about binary size. TODO: expand it to parse linker map and also dump module, objects, and section names with a given symbol.
1 parent 7b9205d commit b765133

File tree

1 file changed

+167
-0
lines changed

1 file changed

+167
-0
lines changed

.github/scripts/run_nm.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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 = ["--print-size", "--size-sort", "--reverse-sort", "--demangle"]
51+
output = Parser.run_nm(
52+
self.elf,
53+
args,
54+
nm=self.toolchain_prefix + "nm" if self.toolchain_prefix else "nm",
55+
)
56+
lines = output.splitlines()
57+
symbols = []
58+
59+
def parse_line(line: str) -> Optional[Symbol]:
60+
import re
61+
62+
symbol_pattern = re.compile(
63+
r"(?P<addr>[0-9a-fA-F]+)\s+(?P<size>[0-9a-fA-F]+)\s+(?P<type>\w)\s+(?P<name>.+)"
64+
)
65+
match = symbol_pattern.match(line)
66+
if match:
67+
addr = int(match.group("addr"), 16)
68+
size = int(match.group("size"), 16)
69+
type_ = match.group("type").strip().strip("\n")
70+
name = match.group("name").strip().strip("\n")
71+
return Symbol(name=name, addr=addr, size=size, symbol_type=type_)
72+
return None
73+
74+
for line in lines:
75+
symbol = parse_line(line)
76+
if symbol:
77+
symbols.append(symbol)
78+
79+
assert len(symbols) > 0, "No symbols found in nm output"
80+
if len(symbols) != len(lines):
81+
print(
82+
"** Warning: Not all lines were parsed, check the output of nm. Parsed {len(symbols)} lines, given {len(lines)}"
83+
)
84+
if any(symbol.size == 0 for symbol in symbols):
85+
print("** Warning: Some symbols have zero size, check the output of nm.")
86+
87+
# TODO: Populate the section and module fields from the linker map if available (-Wl,-Map=linker.map)
88+
return {symbol.name: symbol for symbol in symbols}
89+
90+
def print(self):
91+
print(f"Elf: {self.elf}")
92+
93+
def print_table(filter=None, filter_name=None):
94+
print("\nAddress\t\tSize\tType\tName")
95+
# Apply filter and sort symbols
96+
symbols_to_print = {
97+
name: sym
98+
for name, sym in self.symbols.items()
99+
if not filter or filter(sym)
100+
}
101+
sorted_symbols = sorted(
102+
symbols_to_print.items(), key=lambda x: x[1].size, reverse=True
103+
)
104+
105+
# Print symbols and calculate total size
106+
size_total = 0
107+
for name, sym in sorted_symbols:
108+
print(f"{hex(sym.addr)}\t\t{sym.size}\t{sym.symbol_type}\t{sym.name}")
109+
size_total += sym.size
110+
111+
# Print summary
112+
symbol_percent = len(symbols_to_print) / len(self.symbols) * 100
113+
print("-----")
114+
print(f"> Total bytes: {size_total}")
115+
print(
116+
f"Counted: {len(symbols_to_print)}/{len(self.symbols)}, {symbol_percent:0.2f}% (filter: '{filter_name}')"
117+
)
118+
print("=====\n")
119+
120+
# Print tables with different filters
121+
if (
122+
self.filter is None
123+
or self.filter not in ["all", "executorch", "executorch_text"]
124+
or self.filter == "all"
125+
):
126+
print_table(None, "All")
127+
elif self.filter == "executorch":
128+
print_table(
129+
lambda s: "executorch" in s.name or s.name.startswith("et"),
130+
"Executorch",
131+
)
132+
elif self.filter == "executorch_text":
133+
print_table(
134+
lambda s: ("executorch" in s.name or s.name.startswith("et"))
135+
and s.symbol_type in ["t", "T"],
136+
"Executorch .text",
137+
)
138+
139+
140+
if __name__ == "__main__":
141+
import argparse
142+
143+
parser = argparse.ArgumentParser(
144+
description="Process ELF file and linker map file."
145+
)
146+
parser.add_argument(
147+
"-e", "--elf-file-path", required=True, help="Path to the ELF file"
148+
)
149+
parser.add_argument(
150+
"-f",
151+
"--filter",
152+
required=False,
153+
default="all",
154+
help="Filter symbols by pre-defined filters",
155+
choices=["all", "executorch", "executorch_text"],
156+
)
157+
parser.add_argument(
158+
"-p",
159+
"--toolchain-prefix",
160+
required=False,
161+
default="",
162+
help="Optional toolchain prefix for nm",
163+
)
164+
165+
args = parser.parse_args()
166+
p = Parser(args.elf_file_path, args.toolchain_prefix, filter=args.filter)
167+
p.print()

0 commit comments

Comments
 (0)