Skip to content

Commit d42246c

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 5446b5e commit d42246c

File tree

1 file changed

+166
-0
lines changed

1 file changed

+166
-0
lines changed

.github/scripts/run_nm.py

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

0 commit comments

Comments
 (0)