"""Symbol demangling utilities for memory analysis. This module provides functions for demangling C++ symbol names using c++filt. """ from __future__ import annotations import logging import re import subprocess from .toolchain import find_tool _LOGGER = logging.getLogger(__name__) # GCC global constructor/destructor prefix annotations GCC_PREFIX_ANNOTATIONS = { "_GLOBAL__sub_I_": "global constructor for", "_GLOBAL__sub_D_": "global destructor for", } # GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2) GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)") def _strip_gcc_annotations(symbol: str) -> tuple[str, str]: """Strip GCC optimization suffixes and prefixes from a symbol. Args: symbol: The mangled symbol name Returns: Tuple of (stripped_symbol, removed_prefix) """ # Remove GCC optimization markers stripped = GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol) # Handle GCC global constructor/initializer prefixes prefix = "" for gcc_prefix in GCC_PREFIX_ANNOTATIONS: if stripped.startswith(gcc_prefix): prefix = gcc_prefix stripped = stripped[len(prefix) :] break return stripped, prefix def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str: """Restore prefix that was removed before demangling. Args: prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_") stripped: Stripped symbol name demangled: Demangled symbol name Returns: Demangled name with prefix restored/annotated """ if not prefix: return demangled # Successfully demangled - add descriptive prefix if demangled != stripped and (annotation := GCC_PREFIX_ANNOTATIONS.get(prefix)): return f"[{annotation}: {demangled}]" # Failed to demangle - restore original prefix return prefix + demangled def _restore_symbol_suffix(original: str, demangled: str) -> str: """Restore GCC optimization suffix that was removed before demangling. Args: original: Original symbol name with suffix demangled: Demangled symbol name without suffix Returns: Demangled name with suffix annotation """ if suffix_match := GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original): return f"{demangled} [{suffix_match.group(1)}]" return demangled def batch_demangle( symbols: list[str], cppfilt_path: str | None = None, objdump_path: str | None = None, ) -> dict[str, str]: """Batch demangle C++ symbol names. Args: symbols: List of symbol names to demangle cppfilt_path: Path to c++filt binary (auto-detected if not provided) objdump_path: Path to objdump binary to derive c++filt path from Returns: Dictionary mapping original symbol names to demangled names """ cache: dict[str, str] = {} if not symbols: return cache # Find c++filt tool cppfilt_cmd = cppfilt_path or find_tool("c++filt", objdump_path) if not cppfilt_cmd: _LOGGER.warning("Could not find c++filt, symbols will not be demangled") return {s: s for s in symbols} _LOGGER.debug("Demangling %d symbols using %s", len(symbols), cppfilt_cmd) # Strip GCC optimization suffixes and prefixes before demangling symbols_stripped: list[str] = [] symbols_prefixes: list[str] = [] for symbol in symbols: stripped, prefix = _strip_gcc_annotations(symbol) symbols_stripped.append(stripped) symbols_prefixes.append(prefix) try: result = subprocess.run( [cppfilt_cmd], input="\n".join(symbols_stripped), capture_output=True, text=True, check=False, ) except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e: _LOGGER.warning("Failed to batch demangle symbols: %s", e) return {s: s for s in symbols} if result.returncode != 0: _LOGGER.warning( "c++filt exited with code %d: %s", result.returncode, result.stderr[:200] if result.stderr else "(no error output)", ) return {s: s for s in symbols} # Process demangled output demangled_lines = result.stdout.strip().split("\n") # Check for output length mismatch if len(demangled_lines) != len(symbols): _LOGGER.warning( "c++filt output mismatch: expected %d lines, got %d", len(symbols), len(demangled_lines), ) return {s: s for s in symbols} failed_count = 0 for original, stripped, prefix, demangled in zip( symbols, symbols_stripped, symbols_prefixes, demangled_lines, strict=True ): # Add back any prefix that was removed demangled = _restore_symbol_prefix(prefix, stripped, demangled) # If we stripped a suffix, add it back to the demangled name for clarity if original != stripped and not prefix: demangled = _restore_symbol_suffix(original, demangled) cache[original] = demangled # Count symbols that failed to demangle if stripped == demangled and stripped.startswith("_Z"): failed_count += 1 if failed_count <= 5: _LOGGER.debug("Failed to demangle: %s", original) if failed_count > 0: _LOGGER.debug( "Failed to demangle %d/%d symbols using %s", failed_count, len(symbols), cppfilt_cmd, ) return cache