Files
2026-04-25 09:44:02 +02:00

151 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Analyze ELF shared library dependencies and symbol versioning.
A cross-platform alternative to ldd that works without executing the binary.
Lists shared library dependencies, RPATH/RUNPATH search paths, and optionally
shows imported symbol versions (useful for checking glibc compatibility).
Inspired by conda-build's liefldd.py and Bitcoin's symbol-check.py.
Example:
$ python elf_deps.py /usr/bin/ls
Dependencies: /usr/bin/ls
========================================
Interpreter: /lib64/ld-linux-x86-64.so.2
Type: DYN (Position-Independent Executable)
Shared Libraries:
libselinux.so.1
libc.so.6
ld-linux-x86-64.so.2
$ python elf_deps.py --versions /usr/bin/ls
...
Symbol Versions:
GLIBC_2.2.5 (25 symbols)
GLIBC_2.3 (1 symbols)
GLIBC_2.17 (2 symbols)
"""
import argparse
from collections import defaultdict
import lief
HEADER_TYPES = {
lief.ELF.Header.FILE_TYPE.EXEC: "EXEC (Executable)",
lief.ELF.Header.FILE_TYPE.DYN: "DYN (Shared Object / PIE)",
lief.ELF.Header.FILE_TYPE.REL: "REL (Relocatable)",
lief.ELF.Header.FILE_TYPE.CORE: "CORE (Core Dump)",
}
def analyze_deps(filename, show_versions=False):
"""Analyze ELF dependencies and symbol version requirements."""
binary = lief.ELF.parse(filename)
if binary is None:
print(f"Error: failed to parse '{filename}' as ELF")
return False
file_type = HEADER_TYPES.get(binary.header.file_type, str(binary.header.file_type))
interpreter = binary.interpreter or "(none — statically linked)"
print(f"Dependencies: {filename}")
print("=" * 48)
print(f" Interpreter: {interpreter}")
print(f" Type: {file_type}")
print(f" Arch: {binary.header.machine_type.name}")
print()
# Shared libraries (DT_NEEDED entries)
libraries = list(binary.libraries)
if libraries:
print(" Shared Libraries:")
for lib in libraries:
print(f" {lib}")
else:
print(" Shared Libraries: (none — statically linked)")
print()
# RPATH and RUNPATH
rpath = None
runpath = None
for entry in binary.dynamic_entries:
if isinstance(entry, lief.ELF.DynamicEntryRpath):
rpath = entry.rpath
elif isinstance(entry, lief.ELF.DynamicEntryRunPath):
runpath = entry.runpath
if rpath or runpath:
print(" Search Paths:")
if rpath:
print(f" RPATH: {rpath}")
if runpath:
print(f" RUNPATH: {runpath}")
print()
# Symbol versions
if show_versions:
version_counts = defaultdict(int)
version_symbols = defaultdict(list)
for sym in binary.imported_symbols:
if not sym.has_version:
continue
version = sym.symbol_version
if not version.has_auxiliary_version:
continue
ver_name = version.symbol_version_auxiliary.name
version_counts[ver_name] += 1
version_symbols[ver_name].append(sym.name)
if version_counts:
print(" Symbol Versions:")
for ver_name in sorted(version_counts):
count = version_counts[ver_name]
print(f" {ver_name:<24} ({count} symbol{'s' if count != 1 else ''})")
print()
# Show highest required version per library prefix
lib_versions = defaultdict(list)
for ver_name in version_counts:
parts = ver_name.rsplit("_", 1)
if len(parts) == 2:
lib, ver_str = parts
try:
ver_tuple = tuple(int(x) for x in ver_str.split("."))
lib_versions[lib].append((ver_tuple, ver_name))
except ValueError:
pass
if lib_versions:
print(" Minimum Required Versions:")
for lib in sorted(lib_versions):
max_ver = max(lib_versions[lib], key=lambda x: x[0])
print(f" {lib}: {max_ver[1]}")
print()
return True
def main():
parser = argparse.ArgumentParser(
description="Analyze ELF shared library dependencies"
)
parser.add_argument("binaries", nargs="+", help="ELF binaries to analyze")
parser.add_argument(
"-v",
"--versions",
action="store_true",
help="Show imported symbol version requirements",
)
args = parser.parse_args()
for path in args.binaries:
analyze_deps(path, show_versions=args.versions)
if __name__ == "__main__":
main()