899 lines
33 KiB
Python
899 lines
33 KiB
Python
#!/usr/bin/env python3
|
|
# pyright: reportUnknownMemberType=false, reportOperatorIssue=false, reportArgumentType=false
|
|
"""
|
|
Enhanced Symbol Analysis Tool with Function Call Graph Analysis
|
|
Analyzes ELF files to identify symbols and function call relationships.
|
|
Shows which functions call other functions (call graph analysis).
|
|
Works with any platform (ESP32S3, UNO, etc.) that has build_info.json.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from collections import defaultdict
|
|
from dataclasses import asdict, dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
|
|
|
# Import board mapping system
|
|
from ci.boards import create_board
|
|
|
|
|
|
@dataclass
|
|
class SymbolInfo:
|
|
"""Represents a symbol in the binary"""
|
|
|
|
address: str
|
|
size: int
|
|
type: str
|
|
name: str
|
|
demangled_name: str
|
|
source: str # STRICT: NO defaults - all callers must provide explicit source
|
|
|
|
|
|
@dataclass
|
|
class TypeBreakdown:
|
|
"""Breakdown of symbols by type"""
|
|
|
|
type: str
|
|
count: int
|
|
total_size: int
|
|
|
|
|
|
@dataclass
|
|
class CallStats:
|
|
"""Statistics about function calls"""
|
|
|
|
functions_with_calls: int
|
|
functions_called_by_others: int
|
|
most_called: List[Tuple[str, int]] = field(default_factory=lambda: [])
|
|
most_calling: List[Tuple[str, int]] = field(default_factory=lambda: [])
|
|
|
|
|
|
@dataclass
|
|
class AnalysisReport:
|
|
"""Complete symbol analysis report"""
|
|
|
|
board: str
|
|
total_symbols: int
|
|
total_size: int
|
|
largest_symbols: List[SymbolInfo] = field(default_factory=lambda: [])
|
|
type_breakdown: List[TypeBreakdown] = field(default_factory=lambda: [])
|
|
dependencies: Dict[str, List[str]] = field(default_factory=lambda: {})
|
|
call_graph: Optional[Dict[str, List[str]]] = None
|
|
reverse_call_graph: Optional[Dict[str, List[str]]] = None
|
|
call_stats: Optional[CallStats] = None
|
|
|
|
|
|
@dataclass
|
|
class DetailedAnalysisData:
|
|
"""Complete detailed analysis data structure for JSON output"""
|
|
|
|
summary: AnalysisReport
|
|
all_symbols_sorted_by_size: List[SymbolInfo]
|
|
dependencies: Dict[str, List[str]]
|
|
call_graph: Optional[Dict[str, List[str]]] = None
|
|
reverse_call_graph: Optional[Dict[str, List[str]]] = None
|
|
|
|
|
|
@dataclass
|
|
class TypeStats:
|
|
"""Statistics for symbol types with dictionary-like functionality"""
|
|
|
|
stats: Dict[str, TypeBreakdown] = field(default_factory=lambda: {})
|
|
|
|
def add_symbol(self, symbol: SymbolInfo) -> None:
|
|
"""Add a symbol to the type statistics"""
|
|
sym_type = symbol.type
|
|
if sym_type not in self.stats:
|
|
self.stats[sym_type] = TypeBreakdown(type=sym_type, count=0, total_size=0)
|
|
self.stats[sym_type].count += 1
|
|
self.stats[sym_type].total_size += symbol.size
|
|
|
|
def items(self) -> List[Tuple[str, TypeBreakdown]]:
|
|
"""Return items for iteration, sorted by total_size descending"""
|
|
return sorted(self.stats.items(), key=lambda x: x[1].total_size, reverse=True)
|
|
|
|
def values(self) -> List[TypeBreakdown]:
|
|
"""Return values for iteration"""
|
|
return list(self.stats.values())
|
|
|
|
def __getitem__(self, key: str) -> TypeBreakdown:
|
|
"""Allow dictionary-style access"""
|
|
return self.stats[key]
|
|
|
|
def __contains__(self, key: str) -> bool:
|
|
"""Allow 'in' operator"""
|
|
return key in self.stats
|
|
|
|
|
|
def run_command(cmd: str) -> str:
|
|
"""Run a command and return stdout"""
|
|
try:
|
|
result = subprocess.run(
|
|
cmd, shell=True, capture_output=True, text=True, check=True
|
|
)
|
|
return result.stdout
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Error running command: {cmd}")
|
|
print(f"Error: {e.stderr}")
|
|
return ""
|
|
|
|
|
|
def demangle_symbol(mangled_name: str, cppfilt_path: str) -> str:
|
|
"""Demangle a C++ symbol using c++filt"""
|
|
try:
|
|
cmd = f'echo "{mangled_name}" | "{cppfilt_path}"'
|
|
result = subprocess.run(
|
|
cmd, shell=True, capture_output=True, text=True, check=True
|
|
)
|
|
demangled = result.stdout.strip()
|
|
# If demangling failed, c++filt returns the original name
|
|
return demangled if demangled != mangled_name else mangled_name
|
|
except Exception as e:
|
|
print(f"Error demangling symbol: {mangled_name}")
|
|
print(f"Error: {e}")
|
|
return mangled_name
|
|
|
|
|
|
def analyze_symbols(
|
|
elf_file: str, nm_path: str, cppfilt_path: str, readelf_path: Optional[str] = None
|
|
) -> List[SymbolInfo]:
|
|
"""Analyze ALL symbols in ELF file using both nm and readelf for comprehensive coverage"""
|
|
print("Analyzing symbols with enhanced coverage...")
|
|
|
|
symbols: List[SymbolInfo] = []
|
|
symbols_dict: Dict[str, SymbolInfo] = {} # To deduplicate by address+name
|
|
|
|
# Method 1: Use readelf to get ALL symbols (including those without size)
|
|
if readelf_path:
|
|
print("Getting all symbols using readelf...")
|
|
readelf_cmd = f'"{readelf_path}" -s "{elf_file}"'
|
|
output = run_command(readelf_cmd)
|
|
|
|
if output:
|
|
for line in output.strip().split("\n"):
|
|
line = line.strip()
|
|
# Skip header and empty lines
|
|
if (
|
|
not line
|
|
or "Num:" in line
|
|
or "Symbol table" in line
|
|
or line.startswith("--")
|
|
):
|
|
continue
|
|
|
|
# Parse readelf output format: Num: Value Size Type Bind Vis Ndx Name
|
|
parts = line.split()
|
|
if len(parts) >= 8:
|
|
try:
|
|
# Skip num (parts[0]) - not needed
|
|
addr = parts[1]
|
|
size = int(parts[2]) if parts[2].isdigit() else 0
|
|
symbol_type = parts[3]
|
|
bind = parts[4]
|
|
# Skip vis and ndx (parts[5], parts[6]) - not needed
|
|
name = " ".join(parts[7:]) if len(parts) > 7 else ""
|
|
|
|
# Skip empty names and section symbols
|
|
if not name or name.startswith(".") or symbol_type == "SECTION":
|
|
continue
|
|
|
|
# Create a unique key for deduplication (use name as primary key since addresses can vary)
|
|
key = name.strip()
|
|
|
|
if key not in symbols_dict:
|
|
# Demangle the symbol name
|
|
demangled_name = demangle_symbol(name, cppfilt_path)
|
|
|
|
symbol_info = SymbolInfo(
|
|
address=addr,
|
|
size=size,
|
|
type=symbol_type[0].upper(), # T, D, B, etc.
|
|
name=name,
|
|
demangled_name=demangled_name,
|
|
source="readelf",
|
|
)
|
|
|
|
symbols_dict[key] = symbol_info
|
|
|
|
except (ValueError, IndexError):
|
|
continue # Skip malformed lines
|
|
|
|
# Method 2: Use nm with --print-size to get symbols with sizes (for accurate size info)
|
|
print("Getting sized symbols using nm...")
|
|
nm_cmd = f'"{nm_path}" --print-size --size-sort --radix=d "{elf_file}"'
|
|
output = run_command(nm_cmd)
|
|
|
|
if output:
|
|
for line in output.strip().split("\n"):
|
|
if not line.strip():
|
|
continue
|
|
|
|
parts = line.split()
|
|
if len(parts) >= 4:
|
|
try:
|
|
addr = parts[0]
|
|
size = int(parts[1])
|
|
symbol_type = parts[2]
|
|
name = " ".join(parts[3:])
|
|
|
|
# Create a unique key for deduplication (use name as primary key)
|
|
key = name.strip()
|
|
|
|
# If we already have this symbol from readelf, update with accurate size
|
|
if key in symbols_dict:
|
|
symbols_dict[key].size = size
|
|
symbols_dict[key].type = symbol_type
|
|
symbols_dict[key].source = "nm+readelf"
|
|
else:
|
|
# New symbol not found by readelf
|
|
demangled_name = demangle_symbol(name, cppfilt_path)
|
|
|
|
symbol_info = SymbolInfo(
|
|
address=addr,
|
|
size=size,
|
|
type=symbol_type,
|
|
name=name,
|
|
demangled_name=demangled_name,
|
|
source="nm",
|
|
)
|
|
|
|
symbols_dict[key] = symbol_info
|
|
|
|
except (ValueError, IndexError):
|
|
continue # Skip malformed lines
|
|
|
|
# Method 3: Use nm with -a to get all symbols including debugger-only
|
|
print("Getting additional symbols using nm -a...")
|
|
nm_all_cmd = f'"{nm_path}" -a --radix=d "{elf_file}"'
|
|
output = run_command(nm_all_cmd)
|
|
|
|
if output:
|
|
for line in output.strip().split("\n"):
|
|
if not line.strip():
|
|
continue
|
|
|
|
parts = line.split()
|
|
if len(parts) >= 3:
|
|
try:
|
|
addr = parts[0]
|
|
symbol_type = parts[1]
|
|
name = " ".join(parts[2:])
|
|
|
|
# Skip empty names
|
|
if not name:
|
|
continue
|
|
|
|
# Create a unique key for deduplication (use name as primary key)
|
|
key = name.strip()
|
|
|
|
if key not in symbols_dict:
|
|
# New symbol not found by other methods
|
|
demangled_name = demangle_symbol(name, cppfilt_path)
|
|
|
|
symbol_info = SymbolInfo(
|
|
address=addr,
|
|
size=0, # nm -a doesn't provide size
|
|
type=symbol_type,
|
|
name=name,
|
|
demangled_name=demangled_name,
|
|
source="nm-a",
|
|
)
|
|
|
|
symbols_dict[key] = symbol_info
|
|
|
|
except (ValueError, IndexError):
|
|
continue # Skip malformed lines
|
|
|
|
# Convert dict to list
|
|
symbols: List[SymbolInfo] = list(symbols_dict.values())
|
|
|
|
print(f"Found {len(symbols)} total symbols using enhanced analysis")
|
|
print(f" - Symbols with size info: {len([s for s in symbols if s.size > 0])}")
|
|
print(f" - Symbols without size: {len([s for s in symbols if s.size == 0])}")
|
|
|
|
return symbols
|
|
|
|
|
|
def analyze_function_calls(
|
|
elf_file: str, objdump_path: str, cppfilt_path: str
|
|
) -> Dict[str, List[str]]:
|
|
"""Analyze function calls using objdump to build call graph"""
|
|
print("Analyzing function calls using objdump...")
|
|
|
|
# Use objdump to disassemble the binary
|
|
cmd = f'"{objdump_path}" -t "{elf_file}"'
|
|
print(f"Running: {cmd}")
|
|
symbol_output = run_command(cmd)
|
|
|
|
# Build symbol address map for function symbols
|
|
symbol_map: Dict[str, str] = {} # address -> symbol_name
|
|
function_symbols: set[str] = set() # set of function names
|
|
|
|
for line in symbol_output.strip().split("\n"):
|
|
if not line.strip():
|
|
continue
|
|
|
|
# Parse objdump symbol table output
|
|
# Format: address flags section size name
|
|
parts = line.split()
|
|
if len(parts) >= 5 and ("F" in parts[1] or "f" in parts[1]): # Function symbol
|
|
try:
|
|
address = parts[0]
|
|
symbol_name = " ".join(parts[4:])
|
|
|
|
# Demangle the symbol name
|
|
demangled_name = demangle_symbol(symbol_name, cppfilt_path)
|
|
|
|
symbol_map[address] = {"name": symbol_name, "demangled": demangled_name}
|
|
function_symbols.add(demangled_name)
|
|
except (ValueError, IndexError):
|
|
continue
|
|
|
|
print(f"Found {len(function_symbols)} function symbols")
|
|
|
|
# Now disassemble text sections to find function calls
|
|
cmd = f'"{objdump_path}" -d "{elf_file}"'
|
|
print(f"Running: {cmd}")
|
|
disasm_output = run_command(cmd)
|
|
|
|
if not disasm_output:
|
|
print("Warning: No disassembly output received")
|
|
return {}
|
|
|
|
# Parse disassembly to find function calls
|
|
call_graph: defaultdict[str, set[str]] = defaultdict(
|
|
set
|
|
) # caller -> set of callees
|
|
current_function = None
|
|
|
|
# Common call instruction patterns for different architectures
|
|
call_patterns = [
|
|
r"call\s+(\w+)", # x86/x64 call
|
|
r"bl\s+(\w+)", # ARM branch with link
|
|
r"jal\s+(\w+)", # RISC-V jump and link
|
|
r"callx?\d*\s+(\w+)", # Xtensa call variations
|
|
]
|
|
|
|
call_regex = re.compile(
|
|
"|".join(f"(?:{pattern})" for pattern in call_patterns), re.IGNORECASE
|
|
)
|
|
|
|
function_start_pattern = re.compile(r"^([0-9a-f]+)\s+<([^>]+)>:")
|
|
|
|
lines = disasm_output.split("\n")
|
|
for i, line in enumerate(lines):
|
|
line = line.strip()
|
|
|
|
# Check for function start
|
|
func_match = function_start_pattern.match(line)
|
|
if func_match:
|
|
func_name = func_match.group(2)
|
|
|
|
# Demangle function name
|
|
current_function = demangle_symbol(func_name, cppfilt_path)
|
|
continue
|
|
|
|
# Look for call instructions
|
|
if current_function and (
|
|
"call" in line.lower() or "bl " in line.lower() or "jal" in line.lower()
|
|
):
|
|
call_match = call_regex.search(line)
|
|
if call_match:
|
|
# Extract the target function name
|
|
for group in call_match.groups():
|
|
if group:
|
|
target_func = demangle_symbol(group, cppfilt_path)
|
|
call_graph[current_function].add(target_func)
|
|
break
|
|
|
|
print(f"Built call graph with {len(call_graph)} calling functions")
|
|
|
|
# Convert sets to lists for JSON serialization
|
|
return {caller: list(callees) for caller, callees in call_graph.items()}
|
|
|
|
|
|
def build_reverse_call_graph(call_graph: Dict[str, List[str]]) -> Dict[str, List[str]]:
|
|
"""Build reverse call graph: function -> list of functions that call it"""
|
|
reverse_graph: defaultdict[str, List[str]] = defaultdict(list)
|
|
|
|
for caller, callees in call_graph.items():
|
|
for callee in callees:
|
|
reverse_graph[callee].append(caller)
|
|
|
|
return dict(reverse_graph)
|
|
|
|
|
|
def analyze_map_file(map_file: Path) -> Dict[str, List[str]]:
|
|
"""Analyze the map file to understand module dependencies"""
|
|
print(f"Analyzing map file: {map_file}")
|
|
|
|
dependencies: Dict[str, List[str]] = {}
|
|
current_archive: Optional[str] = None
|
|
|
|
if not map_file.exists():
|
|
print(f"Map file not found: {map_file}")
|
|
return {}
|
|
|
|
try:
|
|
with open(map_file, "r") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
|
|
# Look for archive member includes - handle both ESP32 and UNO formats
|
|
if ".a(" in line and ")" in line:
|
|
# Extract module name
|
|
start = line.find("(") + 1
|
|
end = line.find(")")
|
|
if start > 0 and end > start:
|
|
current_archive = line[start:end]
|
|
dependencies[current_archive] = []
|
|
|
|
elif current_archive and line and not line.startswith((".", "/", "*")):
|
|
# This line shows what pulled in the module
|
|
if "(" in line and ")" in line:
|
|
# Extract the symbol that caused the inclusion
|
|
symbol_start = line.find("(") + 1
|
|
symbol_end = line.find(")")
|
|
if symbol_start > 0 and symbol_end > symbol_start:
|
|
symbol = line[symbol_start:symbol_end]
|
|
dependencies[current_archive].append(symbol)
|
|
current_archive = None
|
|
except Exception as e:
|
|
print(f"Error reading map file: {e}")
|
|
return {}
|
|
|
|
return dependencies
|
|
|
|
|
|
def generate_report(
|
|
board_name: str,
|
|
symbols: List[SymbolInfo],
|
|
dependencies: Dict[str, List[str]],
|
|
call_graph: Optional[Dict[str, List[str]]] = None,
|
|
reverse_call_graph: Optional[Dict[str, List[str]]] = None,
|
|
enhanced_mode: bool = False,
|
|
) -> AnalysisReport:
|
|
"""Generate a comprehensive report with optional call graph analysis"""
|
|
print("\n" + "=" * 80)
|
|
if enhanced_mode and call_graph and reverse_call_graph:
|
|
print(f"{board_name.upper()} ENHANCED SYMBOL ANALYSIS REPORT")
|
|
else:
|
|
print(f"{board_name.upper()} SYMBOL ANALYSIS REPORT")
|
|
print("=" * 80)
|
|
|
|
# Summary statistics
|
|
total_symbols = len(symbols)
|
|
total_size = sum(s.size for s in symbols)
|
|
symbols_with_size = [s for s in symbols if s.size > 0]
|
|
symbols_without_size = [s for s in symbols if s.size == 0]
|
|
|
|
print("\nSUMMARY:")
|
|
print(f" Total symbols: {total_symbols}")
|
|
print(f" Symbols with size info: {len(symbols_with_size)}")
|
|
print(f" Symbols without size info: {len(symbols_without_size)}")
|
|
print(
|
|
f" Total symbol size: {total_size} bytes ({total_size / 1024:.1f} KB) [sized symbols only]"
|
|
)
|
|
|
|
if enhanced_mode and call_graph and reverse_call_graph:
|
|
print(f" Functions with calls: {len(call_graph)}")
|
|
print(f" Functions called by others: {len(reverse_call_graph)}")
|
|
|
|
# Show source breakdown
|
|
source_stats: Dict[str, int] = {}
|
|
for sym in symbols:
|
|
source = sym.source
|
|
if source not in source_stats:
|
|
source_stats[source] = 0
|
|
source_stats[source] += 1
|
|
|
|
print("\nSYMBOL SOURCES:")
|
|
for source, count in sorted(source_stats.items()):
|
|
print(f" {source}: {count} symbols")
|
|
|
|
# Largest symbols overall
|
|
print("\nLARGEST SYMBOLS (all symbols, sorted by size):")
|
|
symbols_sorted = sorted(symbols, key=lambda x: x.size, reverse=True)
|
|
|
|
display_count = 30 if enhanced_mode else 50
|
|
for i, sym in enumerate(symbols_sorted[:display_count]):
|
|
display_name = sym.demangled_name
|
|
print(f" {i + 1:2d}. {sym.size:6d} bytes - {display_name}")
|
|
|
|
# Show what functions call this symbol (if enhanced mode and it's a function)
|
|
if enhanced_mode and reverse_call_graph and display_name in reverse_call_graph:
|
|
callers = reverse_call_graph[display_name]
|
|
if callers:
|
|
caller_names = [
|
|
name[:40] + "..." if len(name) > 40 else name
|
|
for name in callers[:3]
|
|
]
|
|
print(f" Called by: {', '.join(caller_names)}")
|
|
if len(callers) > 3:
|
|
print(f" ... and {len(callers) - 3} more")
|
|
|
|
# Show mangled name if different (non-enhanced mode)
|
|
elif (
|
|
not enhanced_mode
|
|
and hasattr(sym, "demangled_name")
|
|
and sym.demangled_name
|
|
and sym.demangled_name != sym.name
|
|
):
|
|
print(
|
|
f" (mangled: {sym.name[:80]}{'...' if len(sym.name) > 80 else ''})"
|
|
)
|
|
|
|
# Initialize variables for enhanced mode data
|
|
most_called = []
|
|
most_calling = []
|
|
|
|
# Enhanced function call analysis
|
|
if enhanced_mode and call_graph and reverse_call_graph:
|
|
print("\n" + "=" * 80)
|
|
print("FUNCTION CALL ANALYSIS")
|
|
print("=" * 80)
|
|
|
|
# Most called functions
|
|
most_called = sorted(
|
|
reverse_call_graph.items(), key=lambda x: len(x[1]), reverse=True
|
|
)
|
|
print("\nMOST CALLED FUNCTIONS (functions called by many others):")
|
|
for i, (func_name, callers) in enumerate(most_called[:15]):
|
|
short_name = func_name[:60] + "..." if len(func_name) > 60 else func_name
|
|
print(f" {i + 1:2d}. {short_name}")
|
|
print(f" Called by {len(callers)} functions")
|
|
if len(callers) <= 5:
|
|
for caller in callers:
|
|
caller_short = caller[:50] + "..." if len(caller) > 50 else caller
|
|
print(f" - {caller_short}")
|
|
else:
|
|
for caller in callers[:3]:
|
|
caller_short = caller[:50] + "..." if len(caller) > 50 else caller
|
|
print(f" - {caller_short}")
|
|
print(f" ... and {len(callers) - 3} more")
|
|
print()
|
|
|
|
# Functions that call many others
|
|
most_calling = sorted(call_graph.items(), key=lambda x: len(x[1]), reverse=True)
|
|
print("\nFUNCTIONS THAT CALL MANY OTHERS:")
|
|
for i, (func_name, callees) in enumerate(most_calling[:10]):
|
|
short_name = func_name[:60] + "..." if len(func_name) > 60 else func_name
|
|
print(f" {i + 1:2d}. {short_name}")
|
|
print(f" Calls {len(callees)} functions")
|
|
if len(callees) <= 5:
|
|
for callee in callees:
|
|
callee_short = callee[:50] + "..." if len(callee) > 50 else callee
|
|
print(f" -> {callee_short}")
|
|
else:
|
|
for callee in callees[:3]:
|
|
callee_short = callee[:50] + "..." if len(callee) > 50 else callee
|
|
print(f" -> {callee_short}")
|
|
print(f" ... and {len(callees) - 3} more")
|
|
print()
|
|
|
|
# Symbol type breakdown
|
|
section_title = "\n" + "=" * 80 + "\n" if enhanced_mode else "\n"
|
|
print(section_title + "SYMBOL TYPE BREAKDOWN:")
|
|
type_stats = TypeStats()
|
|
for sym in symbols:
|
|
type_stats.add_symbol(sym)
|
|
|
|
for sym_type, stats in type_stats.items():
|
|
print(
|
|
f" {sym_type}: {stats.count} symbols, {stats.total_size} bytes ({stats.total_size / 1024:.1f} KB)"
|
|
)
|
|
|
|
# Dependencies analysis
|
|
if dependencies:
|
|
print("\nMODULE DEPENDENCIES:")
|
|
for module in sorted(dependencies.keys()):
|
|
if dependencies[module]: # Only show modules with dependencies
|
|
print(f" {module}:")
|
|
for symbol in dependencies[module][:5]: # Show first 5 symbols
|
|
print(f" - {symbol}")
|
|
if len(dependencies[module]) > 5:
|
|
print(f" ... and {len(dependencies[module]) - 5} more")
|
|
|
|
# Build return data
|
|
report_data = AnalysisReport(
|
|
board=board_name,
|
|
total_symbols=total_symbols,
|
|
total_size=total_size,
|
|
largest_symbols=symbols_sorted[:20],
|
|
type_breakdown=list(type_stats.values()),
|
|
dependencies=dependencies,
|
|
)
|
|
|
|
# Add enhanced data if available
|
|
if enhanced_mode and call_graph and reverse_call_graph:
|
|
report_data.call_graph = call_graph
|
|
report_data.reverse_call_graph = reverse_call_graph
|
|
report_data.call_stats = CallStats(
|
|
functions_with_calls=len(call_graph),
|
|
functions_called_by_others=len(reverse_call_graph),
|
|
most_called=most_called[:10],
|
|
most_calling=most_calling[:10],
|
|
)
|
|
|
|
return report_data
|
|
|
|
|
|
def find_board_build_info(board_name: Optional[str] = None) -> Tuple[Path, str]:
|
|
"""Find build info for a specific board or detect available boards"""
|
|
# Detect build directory
|
|
current = Path.cwd()
|
|
build_dir: Optional[Path] = None
|
|
while current != current.parent:
|
|
candidate = current / ".build"
|
|
if candidate.exists():
|
|
build_dir = candidate
|
|
break
|
|
current = current.parent
|
|
|
|
if not build_dir:
|
|
print("Error: Could not find build directory (.build)")
|
|
sys.exit(1)
|
|
|
|
# If specific board requested, look for it
|
|
if board_name:
|
|
# 1) Direct board directory: .build/<board>/build_info.json
|
|
board_dir = build_dir / board_name
|
|
if board_dir.exists() and (board_dir / "build_info.json").exists():
|
|
return board_dir / "build_info.json", board_name
|
|
|
|
# 2) PlatformIO nested directory: .build/pio/<board>/build_info.json
|
|
pio_board_dir = build_dir / "pio" / board_name
|
|
if pio_board_dir.exists() and (pio_board_dir / "build_info.json").exists():
|
|
return pio_board_dir / "build_info.json", board_name
|
|
|
|
print(f"Error: Board '{board_name}' not found or missing build_info.json")
|
|
sys.exit(1)
|
|
|
|
# Otherwise, find any available board
|
|
available_boards: List[Tuple[Path, str]] = []
|
|
|
|
# 1) Direct children of .build
|
|
for item in build_dir.iterdir():
|
|
if item.is_dir():
|
|
build_info_file = item / "build_info.json"
|
|
if build_info_file.exists():
|
|
available_boards.append((build_info_file, item.name))
|
|
|
|
# 2) Nested PlatformIO structure .build/pio/*
|
|
pio_dir = build_dir / "pio"
|
|
if pio_dir.exists() and pio_dir.is_dir():
|
|
for item in pio_dir.iterdir():
|
|
if item.is_dir():
|
|
build_info_file = item / "build_info.json"
|
|
if build_info_file.exists():
|
|
available_boards.append((build_info_file, item.name))
|
|
|
|
if not available_boards:
|
|
print(f"Error: No boards with build_info.json found in {build_dir}")
|
|
sys.exit(1)
|
|
|
|
# Return the first available board
|
|
return available_boards[0]
|
|
|
|
|
|
def main():
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Enhanced symbol analysis with optional function call analysis for any platform"
|
|
)
|
|
parser.add_argument(
|
|
"--board", help="Board name to analyze (e.g., uno, esp32dev, esp32s3)"
|
|
)
|
|
parser.add_argument(
|
|
"--no-enhanced",
|
|
action="store_false",
|
|
dest="enhanced",
|
|
default=True,
|
|
help="Disable enhanced analysis with function call graph (enhanced is default)",
|
|
)
|
|
parser.add_argument(
|
|
"--show-calls-to",
|
|
help="Show what functions call a specific function (enables enhanced mode)",
|
|
)
|
|
parser.add_argument(
|
|
"--basic",
|
|
action="store_true",
|
|
help="Use basic nm-only symbol analysis (for backward compatibility). Default is comprehensive analysis with readelf + nm",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
# Enable enhanced mode if show-calls-to is specified
|
|
enhanced_mode = args.enhanced or args.show_calls_to
|
|
|
|
# Use comprehensive symbol analysis by default, basic only if requested
|
|
comprehensive_symbols = not args.basic
|
|
|
|
# Find build info
|
|
build_info_path, board_name = find_board_build_info(args.board)
|
|
print(f"Found build info for {board_name}: {build_info_path}")
|
|
|
|
with open(build_info_path) as f:
|
|
build_info = json.load(f)
|
|
|
|
# Get board info using proper board mapping
|
|
board = create_board(board_name)
|
|
real_board_name = board.get_real_board_name()
|
|
|
|
# Try the real board name first, then fall back to directory name
|
|
if real_board_name in build_info:
|
|
board_info = build_info[real_board_name]
|
|
actual_board_key = real_board_name
|
|
if real_board_name != board_name:
|
|
print(
|
|
f"Note: Using board key '{real_board_name}' from board mapping (directory was '{board_name}')"
|
|
)
|
|
elif board_name in build_info:
|
|
board_info = build_info[board_name]
|
|
actual_board_key = board_name
|
|
else:
|
|
# Try to find the actual board key in the JSON as fallback
|
|
board_keys = list(build_info.keys())
|
|
if len(board_keys) == 1:
|
|
actual_board_key = board_keys[0]
|
|
board_info = build_info[actual_board_key]
|
|
print(
|
|
f"Note: Using only available board key '{actual_board_key}' from build_info.json (expected '{real_board_name}' or '{board_name}')"
|
|
)
|
|
else:
|
|
print(
|
|
f"Error: Could not find board '{real_board_name}' or '{board_name}' in build_info.json"
|
|
)
|
|
print(f"Available board keys: {board_keys}")
|
|
sys.exit(1)
|
|
|
|
nm_path = board_info["aliases"]["nm"]
|
|
cppfilt_path = board_info["aliases"]["c++filt"]
|
|
elf_file = board_info["prog_path"]
|
|
|
|
# Get readelf path (derive from nm path if not in aliases)
|
|
if "readelf" in board_info["aliases"]:
|
|
readelf_path = board_info["aliases"]["readelf"]
|
|
else:
|
|
# Derive readelf path from nm path (replace 'nm' with 'readelf')
|
|
readelf_path = nm_path.replace("-nm", "-readelf")
|
|
|
|
# Find map file
|
|
map_file = Path(elf_file).with_suffix(".map")
|
|
|
|
print(f"Analyzing ELF file: {elf_file}")
|
|
print(f"Using nm tool: {nm_path}")
|
|
print(f"Using c++filt tool: {cppfilt_path}")
|
|
print(f"Using readelf tool: {readelf_path}")
|
|
if enhanced_mode:
|
|
objdump_path = board_info["aliases"]["objdump"]
|
|
print(f"Using objdump tool: {objdump_path}")
|
|
print(f"Map file: {map_file}")
|
|
|
|
# Analyze symbols
|
|
if comprehensive_symbols:
|
|
symbols = analyze_symbols(elf_file, nm_path, cppfilt_path, readelf_path)
|
|
else:
|
|
symbols = analyze_symbols(elf_file, nm_path, cppfilt_path)
|
|
|
|
# Analyze function calls if enhanced mode
|
|
call_graph = {}
|
|
reverse_call_graph = {}
|
|
if enhanced_mode:
|
|
objdump_path = board_info["aliases"]["objdump"]
|
|
call_graph = analyze_function_calls(elf_file, objdump_path, cppfilt_path)
|
|
reverse_call_graph = build_reverse_call_graph(call_graph)
|
|
|
|
# Analyze dependencies
|
|
dependencies = analyze_map_file(map_file)
|
|
|
|
# Handle specific function query
|
|
if args.show_calls_to:
|
|
target_function = args.show_calls_to
|
|
print("\n" + "=" * 80)
|
|
print(f"FUNCTIONS THAT CALL: {target_function}")
|
|
print("=" * 80)
|
|
|
|
# Find functions that call the target (exact match first)
|
|
exact_callers = reverse_call_graph.get(target_function, [])
|
|
|
|
# Also search for partial matches
|
|
partial_matches: Dict[str, List[str]] = {}
|
|
for func_name, callers in reverse_call_graph.items():
|
|
if (
|
|
target_function.lower() in func_name.lower()
|
|
and func_name != target_function
|
|
):
|
|
partial_matches[func_name] = callers
|
|
|
|
if exact_callers:
|
|
print(f"\nExact match - Functions calling '{target_function}':")
|
|
for i, caller in enumerate(exact_callers, 1):
|
|
print(f" {i}. {caller}")
|
|
|
|
if partial_matches:
|
|
print(f"\nPartial matches - Functions containing '{target_function}':")
|
|
for func_name, callers in partial_matches.items():
|
|
print(f"\n Function: {func_name}")
|
|
print(f" Called by {len(callers)} functions:")
|
|
for caller in callers[:5]:
|
|
print(f" - {caller}")
|
|
if len(callers) > 5:
|
|
print(f" ... and {len(callers) - 5} more")
|
|
|
|
if not exact_callers and not partial_matches:
|
|
print(f"No functions found that call '{target_function}'")
|
|
print("Available functions (first 20):")
|
|
available_funcs = sorted(reverse_call_graph.keys())[:20]
|
|
for func in available_funcs:
|
|
print(f" - {func}")
|
|
|
|
return # Exit early for specific query
|
|
|
|
# Generate report using user-friendly board name
|
|
report = generate_report(
|
|
board_name.upper(),
|
|
symbols,
|
|
dependencies,
|
|
call_graph,
|
|
reverse_call_graph,
|
|
enhanced_mode,
|
|
)
|
|
|
|
# Save detailed data to JSON (sorted by size, largest first)
|
|
# Find the build directory (go up from wherever we are to find .build)
|
|
current = Path.cwd()
|
|
while current != current.parent:
|
|
build_dir = current / ".build"
|
|
if build_dir.exists():
|
|
filename_suffix = (
|
|
"_enhanced_symbol_analysis.json"
|
|
if enhanced_mode
|
|
else "_symbol_analysis.json"
|
|
)
|
|
output_file = build_dir / f"{board_name}{filename_suffix}"
|
|
break
|
|
current = current.parent
|
|
else:
|
|
# Fallback to current directory if .build not found
|
|
filename_suffix = (
|
|
"_enhanced_symbol_analysis.json"
|
|
if enhanced_mode
|
|
else "_symbol_analysis.json"
|
|
)
|
|
output_file = Path(f"{board_name}{filename_suffix}")
|
|
|
|
# Create detailed analysis data structure
|
|
detailed_data = DetailedAnalysisData(
|
|
summary=report,
|
|
all_symbols_sorted_by_size=sorted(symbols, key=lambda x: x.size, reverse=True),
|
|
dependencies=dependencies,
|
|
call_graph=call_graph if enhanced_mode else None,
|
|
reverse_call_graph=reverse_call_graph if enhanced_mode else None,
|
|
)
|
|
|
|
with open(output_file, "w") as f:
|
|
json.dump(asdict(detailed_data), f, indent=2)
|
|
|
|
description = (
|
|
"enhanced analysis with complete call graph"
|
|
if enhanced_mode
|
|
else "basic symbol analysis"
|
|
)
|
|
print(f"\nDetailed {description} saved to: {output_file}")
|
|
|
|
if not enhanced_mode:
|
|
print("This file contains ALL symbols without any filtering or classification.")
|
|
print(
|
|
"Enhanced mode with function call graph analysis is enabled by default. Use --no-enhanced to disable."
|
|
)
|
|
else:
|
|
print("This file contains ALL symbols and complete call graph analysis.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|