Skip to content

Instantly share code, notes, and snippets.

@courtc
Created March 30, 2024 18:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save courtc/1ac4221a428953df96c48da9b1cc640d to your computer and use it in GitHub Desktop.
Save courtc/1ac4221a428953df96c48da9b1cc640d to your computer and use it in GitHub Desktop.
#!/bin/bash
# 'ldd' alternative which reads ELF files (with readelf) for DT_NEEDED
# entries to collect dependencies. Supports list, tree and JSON output
# formats.
# requirements:
# - bash
# - readelf
# - sed
# read and include ld.so.conf files
# outputs colon separated directories
# param: <conf path>
read_conf() {
local conf="$1"
while read line; do
if [ "${line:0:7}" == "include" ]; then
local incl_wild="$(echo "${line:8}" | sed 's/^[ \t]*//')"
for f in $incl_wild; do
read_conf "$f"
done
else
echo -n "$line:"
fi
done < <(sed '/^\s*$/d;/^#/d;s/^[ \t]*//;s/[ \t]*$//' "$conf")
}
# read ld.so.conf
# outputs colon separated directories
read_ld_conf() {
read_conf "/etc/ld.so.conf"
}
# read ELF RPATH or RUNPATH
# outputs colon separated runpath
# param: <elf path>
read_runpath() {
local libname="$1"
# fixme: should mux rpath/runpath
while read line; do
echo -n "$line:"
done < <(readelf -d "$libname" | sed -n '/PATH/s/.*\[\(.*\)\]/\1/p')
}
# read all load paths for ELF
# Paths, in order:
# 1. RPATH/RUNPATH
# 2. LD_LIBRARY_PATH
# 3. /lib:/usr/lib
# 4. Those specified in /etc/ld.so.conf
# outputs possible search directories for ELF libraries, newline delimited
# param: <elf path>
load_paths() {
local libname="$1"
local lib_path="$LD_LIBRARY_PATH"
[ -n "$lib_path" ] && lib_path="$lib_path:"
local paths="$(read_runpath "$libname")$lib_path/lib:/usr/lib:$(read_ld_conf)"
IFS=: read -r -a patharr <<< "$paths"
for path in "${patharr[@]}"; do
echo "$path"
done
}
# read ELF DT_NEEDED entries
# outputs needed libraries, newline delimited
# param: <elf path>
read_needed() {
local libname="$1"
readelf -d "$libname" | sed -n '/NEEDED/s/.*\[\(.*\)\]/\1/p'
}
# locate needed libraries
# outputs needed library paths, newline delimited
# param: <elf path>
find_needed() {
local libname="$1"
local needed=( $(read_needed "$libname") )
local paths=( $(load_paths "$libname") )
for lib in "${needed[@]}"; do
local found=0
for path in "${paths[@]}"; do
if [ -e "$path/$lib" ]; then
echo "$path/$lib"
found=1
break
fi
done
[ $found -eq 0 ] && echo "$indent - MISSING: $lib" >&2
done
}
# recursive tree printer
# params: <elf path>, <indent string>
_print_needed_tree() {
local libname="$1"
local indent="$2"
local libs=( $(find_needed "$libname") )
echo "$indent- $libname"
for lib in "${libs[@]}"; do
if [ -n "${all_libs[$lib]}" ]; then
echo "$indent - $lib (cached)"
continue
fi
_print_needed_tree "$lib" "$indent "
done
all_libs[$libname]="found"
}
# print needed libraries as tree
# param: <elf path>
print_needed_tree() {
local libname="$1"
declare -A all_libs
_print_needed_tree "$libname"
}
# recursive list printer
# param: <elf path>
_print_needed_list() {
local libname="$1"
local libs=( $(find_needed "$libname") )
for lib in "${libs[@]}"; do
if [ -n "${all_libs[$lib]}" ]; then
continue
fi
echo "$lib"
_print_needed_list "$lib"
done
all_libs[$libname]="found"
}
# print needed libraries as list
# param: <elf path>
print_needed_list() {
local libname="$1"
declare -A all_libs
_print_needed_list "$libname"
}
# recursive JSON printer
# param: <elf path>, [comma/newline prefix]
_print_needed_json() {
local libname="$1"
local gpfx="$2"
local libs=( $(find_needed "$libname") )
[ ${#libs[@]} -eq 0 ] && return
echo -e "$gpfx \"$libname\": ["
all_libs[$libname]="found"
local pfx=""
for lib in "${libs[@]}"; do
echo -en "$pfx \"$lib\""
pfx=",\n"
done
echo -en "\n ]"
for lib in "${libs[@]}"; do
if [ -n "${all_libs[$lib]}" ]; then
continue
fi
_print_needed_json "$lib" ",\n"
done
}
# print needed libraries as JSON
# param: <elf path>
print_needed_json() {
local libname="$1"
declare -A all_libs
echo "{"
_print_needed_json "$libname"
echo -e "\n}"
}
# print usage message
# param: <script path>, [error message]
usage() {
[ -n "$2" ] && echo "$(basename $1): $2"
echo "Usage: $(basename $1) [-hjlt] [--json|--list|--tree] FILE..."
echo " -l, --list list format"
echo " -t, --tree tree format"
echo " -j, --json JSON format"
echo " -h, --help this help"
}
FILES=( )
run_mode="list"
while [[ $# -gt 0 ]]; do
while getopts ":hjlt-:" optchar; do
case "${optchar}" in
-)
case "${OPTARG}" in
json) run_mode="json" ;;
tree) run_mode="tree" ;;
list) run_mode="list" ;;
help)
usage "$0"
exit 0;;
*)
usage "$0" "Unknown option --${OPTARG}" >&2
exit 1;;
esac;;
j) run_mode="json" ;;
t) run_mode="tree" ;;
l) run_mode="list" ;;
h)
usage "$0"
exit 0;;
*)
usage "$0" "Invalid option: '-${optchar}'" >&2
exit 1;;
esac
done
shift $((OPTIND - 1))
OPTIND=1
if [[ ${1+set} = set ]]; then
FILES+=( "$1" )
shift
fi
done
if [ ${#FILES[@]} -eq 0 ]; then
usage "$0" "Missing file arguments"
exit 1
fi
for lib in "${FILES[@]}"; do
print_needed_$run_mode "$lib"
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment