322 lines
7.9 KiB
Bash
Executable File
322 lines
7.9 KiB
Bash
Executable File
#!/bin/ksh
|
|
# $OpenBSD: check_sym,v 1.11 2022/01/03 03:40:48 guenther Exp $
|
|
#
|
|
# Copyright (c) 2016,2019,2022 Philip Guenther <guenther@openbsd.org>
|
|
#
|
|
# Permission to use, copy, modify, and distribute this software for any
|
|
# purpose with or without fee is hereby granted, provided that the above
|
|
# copyright notice and this permission notice appear in all copies.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
#
|
|
#
|
|
# check_sym -- compare the symbols and external function references in two
|
|
# versions of a shared library
|
|
#
|
|
# SYNOPSIS
|
|
# check_sym [-chkv] [old [new]]
|
|
#
|
|
# DESCRIPTION
|
|
# Library developers need to be aware when they have changed the
|
|
# ABI of a library. To assist them, check_sym examines two versions
|
|
# of a shared library and reports changes to the following:
|
|
# * the set of exported symbols and their strengths
|
|
# * the set of undefined symbols referenced
|
|
# * the set of lazily-resolved functions (PLT)
|
|
#
|
|
# In each case, additions and removals are reported; for exported
|
|
# symbols it also reports when a symbol is weakened or strengthened.
|
|
#
|
|
# The shared libraries to compare can be specified on the
|
|
# command-line. Otherwise, check_sym expects to be run from the
|
|
# source directory of a library with a shlib_version file specifying
|
|
# the version being built and the new library in the obj subdirectory.
|
|
# If the old library to compare against wasn't specified either then
|
|
# check_sym will take the highest version of that library in the
|
|
# *current* directory, or the highest version of that library in
|
|
# /usr/lib if it wasn't present in the current directory.
|
|
#
|
|
# By default, check_sym places all its intermediate files in a
|
|
# temporary directory and removed it on exit. They contain useful
|
|
# details for understanding what changed, so if the -k option is used
|
|
# they will instead be placed in /tmp/ and left behind. If any of
|
|
# them cannot be created by the user, the command will fail. The
|
|
# files left behind by the -k option can be cleaned up by invoking
|
|
# check_syms with the -c option.
|
|
#
|
|
# The -v option enables verbose output.
|
|
#
|
|
# The *basic* rules of thumb for library versions are: if you
|
|
# * stop exporting a symbol, or
|
|
# * change the size of a data symbol
|
|
# * start exporting a symbol that an inter-dependent library needs
|
|
# then you need to bump the MAJOR version of the library.
|
|
#
|
|
# Otherwise, if you:
|
|
# * start exporting a symbol
|
|
# then you need to bump the MINOR version of the library.
|
|
#
|
|
# SEE ALSO
|
|
# readelf(1), elf(5)
|
|
#
|
|
# AUTHORS
|
|
# Philip Guenther <guenther@openbsd.org>
|
|
#
|
|
# CAVEATS
|
|
# The elf format is infinitely extendable, but check_sym only
|
|
# handles a few weirdnesses. Running it on or against new archs
|
|
# may result in meaningless results.
|
|
#
|
|
# BUGS
|
|
# While the author stills find the intermediate files useful,
|
|
# most people won't. By default they should be placed in a
|
|
# temp directory and removed.
|
|
#
|
|
|
|
get_lib_name()
|
|
{
|
|
sed -n 's/^[ ]*LIB[ ]*=[ ]*\([^ ]*\).*/\1/p' "$@"
|
|
}
|
|
|
|
pick_highest()
|
|
{
|
|
old=
|
|
omaj=-1
|
|
omin=0
|
|
for i
|
|
do
|
|
[[ -f $i ]] || continue
|
|
maj=${i%.*}; maj=${maj##*.}
|
|
min=${i##*.}
|
|
if [[ $maj -gt $omaj || ( $maj -eq $omaj && $min -gt $omin ) ]]
|
|
then
|
|
old=$i
|
|
omaj=$maj
|
|
omin=$min
|
|
fi
|
|
done
|
|
[[ $old != "" ]]
|
|
}
|
|
|
|
usage()
|
|
{
|
|
usage="usage: check_sym [-chkv] [old [new]]"
|
|
if [[ $# -gt 0 ]]
|
|
then
|
|
echo "check_sym: $@
|
|
$usage" >&2
|
|
exit 1
|
|
fi
|
|
echo "$usage"
|
|
exit 0
|
|
}
|
|
|
|
unset odir
|
|
file_list={D{,S,W,O},J,S,U,d,j,r,s}{1,2}
|
|
|
|
keep_temp=false
|
|
verbose=false
|
|
while getopts :chkv opt "$@"
|
|
do
|
|
case $opt in
|
|
c) rm -f /tmp/$file_list
|
|
exit 0;;
|
|
h) usage;;
|
|
k) keep_temp=true;;
|
|
v) verbose=true;;
|
|
\?) usage "unknown option -- $OPTARG";;
|
|
esac
|
|
done
|
|
shift $((OPTIND - 1))
|
|
[[ $# -gt 2 ]] && usage "too many arguments"
|
|
|
|
# Old library?
|
|
if [[ $1 = ?(*/)lib*.so* ]]
|
|
then
|
|
if [[ ! -f $1 ]]
|
|
then
|
|
echo "$1 doesn't exist" >&2
|
|
exit 1
|
|
fi
|
|
old=$1
|
|
lib=${old##*/}
|
|
lib=${lib%%.so.*}
|
|
shift
|
|
else
|
|
# try determining it from the current directory
|
|
if [[ -f Makefile ]] && lib=$(get_lib_name Makefile) &&
|
|
[[ $lib != "" ]]
|
|
then
|
|
lib=lib$lib
|
|
else
|
|
lib=libc
|
|
fi
|
|
|
|
# Is there a copy of that lib in the current directory?
|
|
# If so, use the highest numbered one
|
|
if ! pick_highest $lib.so.* && ! pick_highest /usr/lib/$lib.so.*
|
|
then
|
|
echo "unable to find $lib.so.*" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# New library?
|
|
if [[ $1 = ?(*/)lib*.so* ]]
|
|
then
|
|
new=$1
|
|
shift
|
|
else
|
|
# Dig info out of the just built library
|
|
. ./shlib_version
|
|
new=obj/${lib}.so.${major}.${minor}
|
|
fi
|
|
if [[ ! -f $new ]]
|
|
then
|
|
echo "$new doesn't exist" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Filter the output of readelf -s to be easier to parse by removing a
|
|
# field that only appears on some symbols: [<other>: 88]
|
|
# Not really arch-specific, but I've only seen it on alpha
|
|
filt_symtab() {
|
|
sed 's/\[<other>: [0-9a-f]*\]//'
|
|
}
|
|
|
|
if $keep_temp
|
|
then
|
|
# precreate all the files we'll use, but with noclobber set to avoid
|
|
# symlink attacks
|
|
odir=/tmp
|
|
files=
|
|
trap 'ret=$?; rm -f $files; exit $ret' 1 2 15 ERR
|
|
else
|
|
trap 'ret=$?; rm -rf "$odir"; exit $ret' 0 1 2 15 ERR
|
|
odir=$(mktemp -dt check_sym.XXXXXXXXXX)
|
|
fi
|
|
set -C
|
|
for i in $odir/$file_list
|
|
do
|
|
rm -f $i
|
|
3>$i
|
|
files="$files $i"
|
|
done
|
|
set +C
|
|
|
|
readelf -rW $old > $odir/r1
|
|
readelf -rW $new > $odir/r2
|
|
|
|
readelf -sW $old | filt_symtab > $odir/s1
|
|
readelf -sW $new | filt_symtab > $odir/s2
|
|
|
|
|
|
case $(readelf -h $new | grep '^ *Machine:') in
|
|
*MIPS*) cpu=mips64;;
|
|
*HPPA*) cpu=hppa;;
|
|
*) cpu=dontcare;;
|
|
esac
|
|
|
|
if [[ $cpu = mips64 ]]
|
|
then
|
|
gotsym1=$(readelf -d $old | awk '$2 ~ /MIPS_GOTSYM/{print $3}')
|
|
gotsym2=$(readelf -d $new | awk '$2 ~ /MIPS_GOTSYM/{print $3}')
|
|
fi
|
|
|
|
# Now that we're done accessing $old and $new (which could be
|
|
# relative paths), chdir into our work directory, whatever it is
|
|
cd $odir
|
|
|
|
jump_slots() {
|
|
case $cpu in
|
|
hppa) awk '/IPLT/ && $5 != ""{print $5}' r$1
|
|
;;
|
|
mips64) # the $((gotsym$1)) converts hex to decimal
|
|
awk -v g=$((gotsym$1)) \
|
|
'/^Symbol table ..symtab/{exit}
|
|
$6 == "PROTECTED" { next }
|
|
$1+0 >= g && $4 == "FUNC" {print $8}' s$1
|
|
;;
|
|
*) awk '/JU*MP_SL/ && $5 != ""{print $5}' r$1
|
|
;;
|
|
esac | sort -o j$1
|
|
}
|
|
|
|
dynamic_sym() {
|
|
awk -v s=$1 '/^Symbol table ..symtab/{exit}
|
|
! /^ *[1-9]/ {next}
|
|
$7 == "UND" {print $8 | ("sort -o U" s); next }
|
|
$5 == "GLOBAL" {print $8 | ("sort -o DS" s) }
|
|
$5 == "WEAK" {print $8 | ("sort -o DW" s) }
|
|
$5 != "LOCAL" {print $8 | ("sort -o D" s) }
|
|
$5 != "LOCAL" && $4 == "OBJECT" {
|
|
print $8, $3 | ("sort -o DO" s) }
|
|
{print $4, $5, $6, $8}' s$1 | sort -o d$1
|
|
}
|
|
|
|
static_sym() {
|
|
awk '/^Symbol table ..symtab/{s=1}
|
|
/LOCAL/{next}
|
|
s&&/^ *[1-9]/{print $4, $5, $6, $8}' s$1 | sort -o S$1
|
|
}
|
|
|
|
data_sym_changes() {
|
|
join "$@" | awk '$2 != $3 { print $1 " " $2 " --> " $3 }'
|
|
}
|
|
|
|
output_if_not_empty() {
|
|
leader=$1
|
|
shift
|
|
if "$@" | grep -q .
|
|
then
|
|
echo "$leader"
|
|
"$@" | sed 's:^: :'
|
|
echo
|
|
fi
|
|
}
|
|
|
|
|
|
for i in 1 2
|
|
do
|
|
jump_slots $i
|
|
dynamic_sym $i
|
|
static_sym $i
|
|
comm -23 j$i U$i >J$i
|
|
done
|
|
|
|
echo "$old --> $new"
|
|
if cmp -s d[12] && cmp -s DO[12]
|
|
then
|
|
printf "No dynamic export changes\n"
|
|
else
|
|
printf "Dynamic export changes:\n"
|
|
output_if_not_empty "added:" comm -13 D[12]
|
|
output_if_not_empty "removed:" comm -23 D[12]
|
|
output_if_not_empty "weakened:" comm -12 DS1 DW2
|
|
output_if_not_empty "strengthened:" comm -12 DW1 DS2
|
|
output_if_not_empty "data object sizes changes:" \
|
|
data_sym_changes DO[12]
|
|
fi
|
|
if ! cmp -s U[12]
|
|
then
|
|
printf "External reference changes:\n"
|
|
output_if_not_empty "added:" comm -13 U[12]
|
|
output_if_not_empty "removed:" comm -23 U[12]
|
|
fi
|
|
|
|
if $verbose; then
|
|
printf "\nReloc counts:\nbefore:\n"
|
|
grep ^R r1
|
|
printf "\nafter:\n"
|
|
grep ^R r2
|
|
fi
|
|
|
|
output_if_not_empty "PLT added:" comm -13 J1 J2
|
|
output_if_not_empty "PLT removed:" comm -23 J1 J2
|