300 lines
9.8 KiB
Python
Executable File
300 lines
9.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
|
|
"""
|
|
Find unused symbols in a shared library.
|
|
|
|
This script analyzes a shared library and a list of executables that link
|
|
against it to determine which publicly exported symbols from the library
|
|
are not used by any of the executables or by the library itself internally.
|
|
|
|
The script checks for symbol usage in three ways:
|
|
1. Internal library references: Uses objdump -R to find relocations within
|
|
the library that reference its own exported symbols
|
|
2. Executable dependencies: Uses nm to find undefined symbols in executables
|
|
that match the library's exported symbols
|
|
3. Cross-references: Identifies symbols used across all provided binaries
|
|
|
|
This comprehensive approach ensures that symbols used internally by the
|
|
library are not incorrectly marked as unused.
|
|
"""
|
|
|
|
import argparse
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def get_exported_symbols(library_path):
|
|
"""
|
|
Extract all exported (public) symbols from a shared library.
|
|
|
|
Public API symbols (those starting with 'sd_') are excluded from the analysis
|
|
since they cannot be removed or made private due to API compatibility requirements.
|
|
|
|
Returns a set of symbol names that are defined and exported by the library.
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
['nm', '--dynamic', '--defined-only', '--extern-only', library_path],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f'Error: Failed to run nm on {library_path}: {e}', file=sys.stderr)
|
|
sys.exit(1)
|
|
except FileNotFoundError:
|
|
print("Error: 'nm' command not found. Please install binutils.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
symbols = set()
|
|
for line in result.stdout.splitlines():
|
|
parts = line.split()
|
|
if len(parts) >= 3:
|
|
# Format: address type name
|
|
symbol_type = parts[1]
|
|
symbol_name = parts[2]
|
|
# Include text (T) and data (D, B, R) symbols
|
|
if symbol_type in ('T', 'D', 'B', 'R', 'W'):
|
|
# Strip version information (e.g., @@SD_SHARED or @SD_SHARED)
|
|
symbol_name = symbol_name.split('@')[0]
|
|
# Skip public API symbols (those starting with sd_)
|
|
if symbol_name.startswith('sd_'):
|
|
continue
|
|
symbols.add(symbol_name)
|
|
|
|
return symbols
|
|
|
|
|
|
def get_undefined_symbols(executable_path):
|
|
"""
|
|
Extract all undefined symbols from an executable.
|
|
|
|
These are symbols that the executable expects to be provided by
|
|
shared libraries it links against.
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
['nm', '--dynamic', '--undefined-only', executable_path],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f'Warning: Failed to run nm on {executable_path}: {e}', file=sys.stderr)
|
|
return set()
|
|
except FileNotFoundError:
|
|
print("Error: 'nm' command not found. Please install binutils.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
symbols = set()
|
|
for line in result.stdout.splitlines():
|
|
parts = line.split()
|
|
if len(parts) >= 2:
|
|
# Format: type name (no address for undefined symbols)
|
|
symbol_name = parts[1]
|
|
# Strip version information (e.g., @SD_SHARED)
|
|
symbol_name = symbol_name.split('@')[0]
|
|
symbols.add(symbol_name)
|
|
|
|
return symbols
|
|
|
|
|
|
def verify_executable_links_library(executable_path, library_name):
|
|
"""
|
|
Verify that an executable actually links against the given library.
|
|
|
|
Returns True if the executable links against a library with the given name.
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
['ldd', executable_path],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
# If ldd fails or doesn't exist, we'll skip the verification
|
|
return True
|
|
|
|
# Check if library_name appears in the ldd output
|
|
for line in result.stdout.splitlines():
|
|
if library_name in line:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def get_library_internal_references(library_path, exported_symbols):
|
|
"""
|
|
Find which exported symbols are referenced internally within the library itself.
|
|
|
|
This uses objdump to look for relocations that reference the exported symbols.
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
['objdump', '-R', library_path],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f'Warning: Failed to run objdump on {library_path}: {e}', file=sys.stderr)
|
|
return set()
|
|
except FileNotFoundError:
|
|
print(
|
|
"Warning: 'objdump' command not found. Internal references won't be detected.",
|
|
file=sys.stderr,
|
|
)
|
|
return set()
|
|
|
|
internal_refs = set()
|
|
for line in result.stdout.splitlines():
|
|
parts = line.split()
|
|
if len(parts) >= 3:
|
|
# objdump -R format: offset type symbol
|
|
# The symbol is typically the last field
|
|
symbol_name = parts[-1]
|
|
# Strip version information
|
|
symbol_name = symbol_name.split('@')[0]
|
|
# Only include if it's one of our exported symbols
|
|
if symbol_name in exported_symbols:
|
|
internal_refs.add(symbol_name)
|
|
|
|
return internal_refs
|
|
|
|
|
|
def find_unused_symbols(library_path, executable_paths, verify_linkage=True):
|
|
"""
|
|
Find symbols exported by the library that are not used by any executable.
|
|
|
|
Args:
|
|
library_path: Path to the shared library
|
|
executable_paths: List of paths to executables
|
|
verify_linkage: Whether to verify executables link against the library
|
|
|
|
Returns:
|
|
Tuple of (unused_symbols, exported_symbols, used_symbols)
|
|
"""
|
|
library_name = Path(library_path).name
|
|
|
|
# Get all exported symbols from the library (excluding public API symbols)
|
|
exported_symbols = get_exported_symbols(library_path)
|
|
|
|
if not exported_symbols:
|
|
print(f'Warning: No exported symbols found in {library_path}', file=sys.stderr)
|
|
return set(), set(), set()
|
|
|
|
# Collect all symbols used by the executables
|
|
used_symbols = set()
|
|
|
|
# First, check if the library references its own exported symbols internally
|
|
internal_refs = get_library_internal_references(library_path, exported_symbols)
|
|
used_symbols.update(internal_refs)
|
|
|
|
for exe_path in executable_paths:
|
|
# Optionally verify linkage
|
|
if verify_linkage and not verify_executable_links_library(exe_path, library_name):
|
|
print(f'Warning: {exe_path} does not appear to link against {library_name}', file=sys.stderr)
|
|
|
|
undefined_symbols = get_undefined_symbols(exe_path)
|
|
# Only count symbols that are actually exported by our library
|
|
used_symbols.update(undefined_symbols & exported_symbols)
|
|
|
|
# Find unused symbols
|
|
unused_symbols = exported_symbols - used_symbols
|
|
|
|
return unused_symbols, exported_symbols, used_symbols
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='Find unused exported symbols in a shared library',
|
|
)
|
|
parser.add_argument(
|
|
'library',
|
|
help='Path to the shared library to analyze',
|
|
)
|
|
parser.add_argument(
|
|
'executables',
|
|
nargs='+',
|
|
help='Paths to executables that link against the library',
|
|
)
|
|
parser.add_argument(
|
|
'--no-verify-linkage',
|
|
action='store_true',
|
|
help='Skip verification that executables actually link against the library',
|
|
)
|
|
parser.add_argument(
|
|
'--show-used',
|
|
action='store_true',
|
|
help='Also show used symbols',
|
|
)
|
|
parser.add_argument(
|
|
'--stats-only',
|
|
action='store_true',
|
|
help='Only show statistics, not individual symbols',
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Verify library exists
|
|
library_path = Path(args.library)
|
|
if not library_path.exists():
|
|
print(f'Error: Library not found: {library_path}', file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Verify executables exist
|
|
executable_paths = []
|
|
for exe in args.executables:
|
|
exe_path = Path(exe)
|
|
if not exe_path.exists():
|
|
print(f'Warning: Executable not found: {exe_path}', file=sys.stderr)
|
|
else:
|
|
executable_paths.append(str(exe_path))
|
|
|
|
if not executable_paths:
|
|
print('Error: No valid executables provided', file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Analyze symbols
|
|
unused, exported, used = find_unused_symbols(
|
|
str(library_path), executable_paths, verify_linkage=not args.no_verify_linkage
|
|
)
|
|
|
|
# Print results
|
|
print(f'Analysis of {library_path.name}')
|
|
print('=' * 70)
|
|
print(f'Total exported symbols: {len(exported)}')
|
|
print(" (excluding public API symbols starting with 'sd_')")
|
|
print(f'Used symbols: {len(used)}')
|
|
print(f'Unused symbols: {len(unused)}')
|
|
print(f'Usage rate: {len(used) / len(exported) * 100:.1f}%' if exported else 'N/A')
|
|
print()
|
|
|
|
if not args.stats_only:
|
|
if unused:
|
|
print('Unused symbols:')
|
|
print('-' * 70)
|
|
for symbol in sorted(unused):
|
|
print(f' {symbol}')
|
|
print()
|
|
else:
|
|
print('All exported symbols are used!')
|
|
print()
|
|
|
|
if args.show_used and used:
|
|
print('Used symbols:')
|
|
print('-' * 70)
|
|
for symbol in sorted(used):
|
|
print(f' {symbol}')
|
|
print()
|
|
|
|
# Exit with non-zero if there are unused symbols (useful for CI)
|
|
sys.exit(0 if not unused else 1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|