159abad11Sguenther#!/bin/ksh 2*d7922f91Stb# $OpenBSD: check_sym,v 1.14 2024/12/24 18:14:49 tb Exp $ 3588fe94fSguenther# 43ea1dcecSguenther# Copyright (c) 2016,2019,2022 Philip Guenther <guenther@openbsd.org> 5588fe94fSguenther# 6588fe94fSguenther# Permission to use, copy, modify, and distribute this software for any 7588fe94fSguenther# purpose with or without fee is hereby granted, provided that the above 8588fe94fSguenther# copyright notice and this permission notice appear in all copies. 9588fe94fSguenther# 10588fe94fSguenther# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11588fe94fSguenther# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12588fe94fSguenther# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13588fe94fSguenther# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14588fe94fSguenther# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15588fe94fSguenther# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16588fe94fSguenther# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17588fe94fSguenther# 1859abad11Sguenther# 1959abad11Sguenther# check_sym -- compare the symbols and external function references in two 2045cee0b3Sguenther# versions of a library 2159abad11Sguenther# 2259abad11Sguenther# SYNOPSIS 2345cee0b3Sguenther# check_sym [-chkSv] [old [new]] 2459abad11Sguenther# 2559abad11Sguenther# DESCRIPTION 2659abad11Sguenther# Library developers need to be aware when they have changed the 2759abad11Sguenther# ABI of a library. To assist them, check_sym examines two versions 2859abad11Sguenther# of a shared library and reports changes to the following: 2959abad11Sguenther# * the set of exported symbols and their strengths 3059abad11Sguenther# * the set of undefined symbols referenced 3159abad11Sguenther# * the set of lazily-resolved functions (PLT) 3259abad11Sguenther# 3359abad11Sguenther# In each case, additions and removals are reported; for exported 3459abad11Sguenther# symbols it also reports when a symbol is weakened or strengthened. 3559abad11Sguenther# 3645cee0b3Sguenther# With the -S option, a similar analysis is done but for the static lib. 3745cee0b3Sguenther# 3859abad11Sguenther# The shared libraries to compare can be specified on the 3959abad11Sguenther# command-line. Otherwise, check_sym expects to be run from the 40b26f28fbSguenther# source directory of a library with a shlib_version file specifying 4159abad11Sguenther# the version being built and the new library in the obj subdirectory. 42b26f28fbSguenther# If the old library to compare against wasn't specified either then 4359abad11Sguenther# check_sym will take the highest version of that library in the 4459abad11Sguenther# *current* directory, or the highest version of that library in 4559abad11Sguenther# /usr/lib if it wasn't present in the current directory. 4659abad11Sguenther# 473ea1dcecSguenther# By default, check_sym places all its intermediate files in a 48*d7922f91Stb# temporary directory and removes it on exit. They contain useful 493ea1dcecSguenther# details for understanding what changed, so if the -k option is used 503ea1dcecSguenther# they will instead be placed in /tmp/ and left behind. If any of 513ea1dcecSguenther# them cannot be created by the user, the command will fail. The 523ea1dcecSguenther# files left behind by the -k option can be cleaned up by invoking 533ea1dcecSguenther# check_syms with the -c option. 5459abad11Sguenther# 5545cee0b3Sguenther# The -v option enables verbose output, showing relocation counts. 5659abad11Sguenther# 5759abad11Sguenther# The *basic* rules of thumb for library versions are: if you 5859abad11Sguenther# * stop exporting a symbol, or 59c9fb8097Sguenther# * change the size of a data symbol 6059abad11Sguenther# * start exporting a symbol that an inter-dependent library needs 6159abad11Sguenther# then you need to bump the MAJOR version of the library. 6259abad11Sguenther# 6359abad11Sguenther# Otherwise, if you: 6459abad11Sguenther# * start exporting a symbol 6559abad11Sguenther# then you need to bump the MINOR version of the library. 6659abad11Sguenther# 6759abad11Sguenther# SEE ALSO 6859abad11Sguenther# readelf(1), elf(5) 6959abad11Sguenther# 7059abad11Sguenther# AUTHORS 7159abad11Sguenther# Philip Guenther <guenther@openbsd.org> 7259abad11Sguenther# 7359abad11Sguenther# CAVEATS 7459abad11Sguenther# The elf format is infinitely extendable, but check_sym only 7559abad11Sguenther# handles a few weirdnesses. Running it on or against new archs 7659abad11Sguenther# may result in meaningless results. 7759abad11Sguenther# 7859abad11Sguenther# BUGS 797ea1ffd3Sguenther# While the author stills find the intermediate files useful, 807ea1ffd3Sguenther# most people won't. By default they should be placed in a 817ea1ffd3Sguenther# temp directory and removed. 827ea1ffd3Sguenther# 8359abad11Sguenther 8459abad11Sguentherget_lib_name() 8559abad11Sguenther{ 8645cee0b3Sguenther sed -n '/^[ ]*LIB[ ]*=/{ s/^[^=]*=[ ]*\([^ ]*\).*/\1/p; q;}' "$@" 8759abad11Sguenther} 8859abad11Sguenther 8959abad11Sguentherpick_highest() 9059abad11Sguenther{ 9159abad11Sguenther old= 9259abad11Sguenther omaj=-1 9359abad11Sguenther omin=0 9459abad11Sguenther for i 9559abad11Sguenther do 9659abad11Sguenther [[ -f $i ]] || continue 9759abad11Sguenther maj=${i%.*}; maj=${maj##*.} 9859abad11Sguenther min=${i##*.} 9959abad11Sguenther if [[ $maj -gt $omaj || ( $maj -eq $omaj && $min -gt $omin ) ]] 10059abad11Sguenther then 10159abad11Sguenther old=$i 10259abad11Sguenther omaj=$maj 10359abad11Sguenther omin=$min 10459abad11Sguenther fi 10559abad11Sguenther done 10659abad11Sguenther [[ $old != "" ]] 10759abad11Sguenther} 10859abad11Sguenther 10945cee0b3Sguentherfail() { echo "$*" >&2; exit 1; } 11045cee0b3Sguenther 1117ea1ffd3Sguentherusage() 1127ea1ffd3Sguenther{ 11345cee0b3Sguenther usage="usage: check_sym [-chkSv] [old [new]]" 11445cee0b3Sguenther [[ $# -eq 0 ]] || fail "check_sym: $* 11545cee0b3Sguenther$usage" 1167ea1ffd3Sguenther echo "$usage" 1177ea1ffd3Sguenther exit 0 1187ea1ffd3Sguenther} 1197ea1ffd3Sguenther 12045cee0b3Sguenther 12145cee0b3Sguenther# 12245cee0b3Sguenther# Output helpers 12345cee0b3Sguenther# 12445cee0b3Sguentherdata_sym_changes() 12545cee0b3Sguenther{ 12645cee0b3Sguenther join "$@" | awk '$2 != $3 { print $1 " " $2 " --> " $3 }' 12745cee0b3Sguenther} 12845cee0b3Sguenther 12945cee0b3Sguentheroutput_if_not_empty() 13045cee0b3Sguenther{ 13145cee0b3Sguenther leader=$1 13245cee0b3Sguenther shift 13345cee0b3Sguenther if "$@" | grep -q . 13445cee0b3Sguenther then 13545cee0b3Sguenther echo "$leader" 13645cee0b3Sguenther "$@" | sed 's:^: :' 13745cee0b3Sguenther echo 13845cee0b3Sguenther fi 13945cee0b3Sguenther} 14045cee0b3Sguenther 14145cee0b3Sguenther 14245cee0b3Sguenther# 14345cee0b3Sguenther# Dynamic library routines 14445cee0b3Sguenther# 14545cee0b3Sguenther 14645cee0b3Sguentherdynamic_collect() 14745cee0b3Sguenther{ 14845cee0b3Sguenther readelf -sW $old | filt_symtab > $odir/Ds1 14945cee0b3Sguenther readelf -sW $new | filt_symtab > $odir/Ds2 15045cee0b3Sguenther 15145cee0b3Sguenther readelf -rW $old > $odir/r1 15245cee0b3Sguenther readelf -rW $new > $odir/r2 15345cee0b3Sguenther 15445cee0b3Sguenther case $(readelf -h $new | grep '^ *Machine:') in 15545cee0b3Sguenther *MIPS*) cpu=mips64 15645cee0b3Sguenther gotsym1=$(readelf -d $old | awk '$2 ~ /MIPS_GOTSYM/{print $3}') 15745cee0b3Sguenther gotsym2=$(readelf -d $new | awk '$2 ~ /MIPS_GOTSYM/{print $3}') 15845cee0b3Sguenther ;; 15945cee0b3Sguenther *HPPA*) cpu=hppa;; 16045cee0b3Sguenther *) cpu=dontcare;; 16145cee0b3Sguenther esac 16245cee0b3Sguenther} 16345cee0b3Sguenther 16445cee0b3Sguentherjump_slots() 16545cee0b3Sguenther{ 16645cee0b3Sguenther case $cpu in 16745cee0b3Sguenther hppa) awk '/IPLT/ && $5 != ""{print $5}' r$1 16845cee0b3Sguenther ;; 16945cee0b3Sguenther mips64) # the $((gotsym$1)) converts hex to decimal 17045cee0b3Sguenther awk -v g=$((gotsym$1)) \ 17145cee0b3Sguenther '/^Symbol table ..symtab/{exit} 17245cee0b3Sguenther $6 == "PROTECTED" { next } 17345cee0b3Sguenther $1+0 >= g && $4 == "FUNC" {print $8}' Ds$1 17445cee0b3Sguenther ;; 17545cee0b3Sguenther *) awk '/JU*MP_SL/ && $5 != ""{print $5}' r$1 17645cee0b3Sguenther ;; 17745cee0b3Sguenther esac | sort -o j$1 17845cee0b3Sguenther} 17945cee0b3Sguenther 18045cee0b3Sguentherdynamic_sym() 18145cee0b3Sguenther{ 18245cee0b3Sguenther awk -v s=$1 '/^Symbol table ..symtab/{exit} 18345cee0b3Sguenther ! /^ *[1-9]/ {next} 18445cee0b3Sguenther $5 == "LOCAL" {next} 18545cee0b3Sguenther $7 == "UND" {print $8 | ("sort -o DU" s); next } 18645cee0b3Sguenther $5 == "GLOBAL" {print $8 | ("sort -o DS" s) } 18745cee0b3Sguenther $5 == "WEAK" {print $8 | ("sort -o DW" s) } 18845cee0b3Sguenther $4 == "OBJECT" {print $8, $3 | ("sort -o DO" s) } 18945cee0b3Sguenther {print $8 | ("sort -o D" s) 19045cee0b3Sguenther print $4, $5, $6, $8}' Ds$1 | sort -o d$1 19145cee0b3Sguenther} 19245cee0b3Sguenther 19345cee0b3Sguentherstatic_sym() 19445cee0b3Sguenther{ 19545cee0b3Sguenther awk '/^Symbol table ..symtab/{s=1} 19645cee0b3Sguenther /LOCAL/{next} 19745cee0b3Sguenther s&&/^ *[1-9]/{print $4, $5, $6, $8}' Ds$1 | sort -o s$1 19845cee0b3Sguenther} 19945cee0b3Sguenther 20045cee0b3Sguentherdynamic_analysis() 20145cee0b3Sguenther{ 20245cee0b3Sguenther jump_slots $1 20345cee0b3Sguenther dynamic_sym $1 20445cee0b3Sguenther #static_sym $1 20545cee0b3Sguenther comm -23 j$1 DU$1 >J$1 20645cee0b3Sguenther return 0 20745cee0b3Sguenther} 20845cee0b3Sguenther 20945cee0b3Sguentherdynamic_output() 21045cee0b3Sguenther{ 21145cee0b3Sguenther if cmp -s d[12] && cmp -s DO[12] 21245cee0b3Sguenther then 21345cee0b3Sguenther printf "No dynamic export changes\n" 21445cee0b3Sguenther else 21545cee0b3Sguenther printf "Dynamic export changes:\n" 21645cee0b3Sguenther output_if_not_empty "added:" comm -13 D[12] 21745cee0b3Sguenther output_if_not_empty "removed:" comm -23 D[12] 21845cee0b3Sguenther output_if_not_empty "weakened:" comm -12 DS1 DW2 21945cee0b3Sguenther output_if_not_empty "strengthened:" comm -12 DW1 DS2 22045cee0b3Sguenther output_if_not_empty "data object sizes changes:" \ 22145cee0b3Sguenther data_sym_changes DO[12] 22245cee0b3Sguenther fi 22345cee0b3Sguenther if ! cmp -s DU[12] 22445cee0b3Sguenther then 22545cee0b3Sguenther printf "External reference changes:\n" 22645cee0b3Sguenther output_if_not_empty "added:" comm -13 DU[12] 22745cee0b3Sguenther output_if_not_empty "removed:" comm -23 DU[12] 22845cee0b3Sguenther fi 22945cee0b3Sguenther 23045cee0b3Sguenther if $verbose; then 23145cee0b3Sguenther printf "\nReloc counts:\nbefore:\n" 23245cee0b3Sguenther grep ^R r1 23345cee0b3Sguenther printf "\nafter:\n" 23445cee0b3Sguenther grep ^R r2 23545cee0b3Sguenther fi 23645cee0b3Sguenther 23745cee0b3Sguenther output_if_not_empty "PLT added:" comm -13 J[12] 23845cee0b3Sguenther output_if_not_empty "PLT removed:" comm -23 J[12] 23945cee0b3Sguenther} 24045cee0b3Sguenther 24145cee0b3Sguenther 24245cee0b3Sguenther# 24345cee0b3Sguenther# Static library routines 24445cee0b3Sguenther# 24545cee0b3Sguentherstatic_collect() 24645cee0b3Sguenther{ 24745cee0b3Sguenther readelf -sW $old | filt_ret | filt_symtab > $odir/Ss1 24845cee0b3Sguenther readelf -sW $new | filt_ret | filt_symtab > $odir/Ss2 24945cee0b3Sguenther} 25045cee0b3Sguenther 25145cee0b3Sguentherstatic_analysis() 25245cee0b3Sguenther{ 25345cee0b3Sguenther awk -v s=$1 '!/^ *[1-9]/{next} 25445cee0b3Sguenther $5 == "LOCAL" {next} 25545cee0b3Sguenther $7 == "UND" {print $8 | ("sort -uo SU" s); next } 25645cee0b3Sguenther $6 == "HIDDEN" {print $8 | ("sort -uo SH" s) } 25745cee0b3Sguenther $5 == "GLOBAL" {print $8 | ("sort -o SS" s) } 25845cee0b3Sguenther $5 == "WEAK" {print $8 | ("sort -o SW" s) } 25945cee0b3Sguenther $4 == "OBJECT" {print $8, $3 | ("sort -o SO" s) } 26045cee0b3Sguenther {print $8 | ("sort -o S" s) 26145cee0b3Sguenther print $4, $5, $6, $8}' Ss$1 | sort -o s$1 26245cee0b3Sguenther grep -v '^_' SH$1 >Sh$1 || : 26345cee0b3Sguenther} 26445cee0b3Sguenther 26545cee0b3Sguentherstatic_output() 26645cee0b3Sguenther{ 26745cee0b3Sguenther output_if_not_empty "hidden but not reserved:" comm -13 Sh[12] 26845cee0b3Sguenther if cmp -s s[12] && cmp -s SO[12] 26945cee0b3Sguenther then 27045cee0b3Sguenther printf "No static export changes\n" 27145cee0b3Sguenther else 27245cee0b3Sguenther printf "Static export changes:\n" 27345cee0b3Sguenther output_if_not_empty "added:" comm -13 S[12] 27445cee0b3Sguenther output_if_not_empty "removed:" comm -23 S[12] 27545cee0b3Sguenther output_if_not_empty "weakened:" comm -12 SS1 SW2 27645cee0b3Sguenther output_if_not_empty "strengthened:" comm -12 SW1 SS2 27745cee0b3Sguenther output_if_not_empty "data object sizes changes:" \ 27845cee0b3Sguenther data_sym_changes SO[12] 27945cee0b3Sguenther fi 28045cee0b3Sguenther if ! cmp -s SU[12] 28145cee0b3Sguenther then 28245cee0b3Sguenther printf "External reference changes:\n" 28345cee0b3Sguenther output_if_not_empty "added:" comm -13 SU[12] 28445cee0b3Sguenther output_if_not_empty "removed:" comm -23 SU[12] 28545cee0b3Sguenther fi 28645cee0b3Sguenther} 28745cee0b3Sguenther 28845cee0b3Sguenther 2893ea1dcecSguentherunset odir 29045cee0b3Sguentherfile_list={D{,O,S,s,W,U},J,d,j,r}{1,2} 29145cee0b3Sguentherstatic_file_list={S{,H,h,O,S,U,W},U,s}{1,2} 29259abad11Sguenther 2933ea1dcecSguentherkeep_temp=false 29445cee0b3Sguentherdynamic=true 29545cee0b3Sguentherstatic=false 2967ea1ffd3Sguentherverbose=false 29745cee0b3Sguenther 29845cee0b3Sguentherdo_static() { static=true dynamic=false file_list=$static_file_list; } 29945cee0b3Sguenther 30045cee0b3Sguentherwhile getopts :chkSv opt "$@" 3017ea1ffd3Sguentherdo 3027ea1ffd3Sguenther case $opt in 3033ea1dcecSguenther c) rm -f /tmp/$file_list 3047ea1ffd3Sguenther exit 0;; 3053ea1dcecSguenther h) usage;; 3063ea1dcecSguenther k) keep_temp=true;; 30745cee0b3Sguenther S) do_static;; 3087ea1ffd3Sguenther v) verbose=true;; 3097ea1ffd3Sguenther \?) usage "unknown option -- $OPTARG";; 3107ea1ffd3Sguenther esac 3117ea1ffd3Sguentherdone 3127ea1ffd3Sguenthershift $((OPTIND - 1)) 3137ea1ffd3Sguenther[[ $# -gt 2 ]] && usage "too many arguments" 31459abad11Sguenther 31559abad11Sguenther# Old library? 31645cee0b3Sguentherif ! $static && [[ $1 = ?(*/)lib*.so* ]] 31759abad11Sguentherthen 31845cee0b3Sguenther [[ -f $1 ]] || fail "$1 doesn't exist" 31959abad11Sguenther old=$1 32059abad11Sguenther lib=${old##*/} 32159abad11Sguenther lib=${lib%%.so.*} 32259abad11Sguenther shift 32345cee0b3Sguentherelif [[ $1 = ?(*/)lib*.a ]] 32445cee0b3Sguentherthen 32545cee0b3Sguenther # woo hoo, static library mode 32645cee0b3Sguenther do_static 32745cee0b3Sguenther if [[ -f $1 ]] 32845cee0b3Sguenther then 32945cee0b3Sguenther old=$1 33045cee0b3Sguenther lib=${old##*/} 33145cee0b3Sguenther elif [[ $1 = lib*.a && -f /usr/lib/$1 ]] 33245cee0b3Sguenther then 33345cee0b3Sguenther old=/usr/lib/$1 33445cee0b3Sguenther lib=$1 33545cee0b3Sguenther else 33645cee0b3Sguenther fail "$1 doesn't exist" 33745cee0b3Sguenther fi 33845cee0b3Sguenther lib=${lib%%.a} 33945cee0b3Sguenther shift 34059abad11Sguentherelse 34159abad11Sguenther # try determining it from the current directory 34259abad11Sguenther if [[ -f Makefile ]] && lib=$(get_lib_name Makefile) && 34359abad11Sguenther [[ $lib != "" ]] 34459abad11Sguenther then 34559abad11Sguenther lib=lib$lib 34659abad11Sguenther else 34759abad11Sguenther lib=libc 34859abad11Sguenther fi 34959abad11Sguenther 35059abad11Sguenther # Is there a copy of that lib in the current directory? 35159abad11Sguenther # If so, use the highest numbered one 35245cee0b3Sguenther if ! $static && 35345cee0b3Sguenther ! pick_highest $lib.so.* && 35445cee0b3Sguenther ! pick_highest /usr/lib/$lib.so.* 35559abad11Sguenther then 35645cee0b3Sguenther fail "unable to find $lib.so.*" 35745cee0b3Sguenther elif $static 35845cee0b3Sguenther then 35945cee0b3Sguenther old=/usr/lib/${lib}.a 36045cee0b3Sguenther [[ -f $old ]] || fail "$old doesn't exist" 36159abad11Sguenther fi 36259abad11Sguentherfi 36359abad11Sguenther 36459abad11Sguenther# New library? 36545cee0b3Sguentherif [[ $1 = ?(*/)lib*.so* ]] || 36645cee0b3Sguenther { $static && [[ $1 = ?(*/)lib*.a ]]; } 36759abad11Sguentherthen 36859abad11Sguenther new=$1 36959abad11Sguenther shift 37045cee0b3Sguentherelif $static 37145cee0b3Sguentherthen 37245cee0b3Sguenther new=obj/${lib}.a 37359abad11Sguentherelse 37459abad11Sguenther # Dig info out of the just built library 37559abad11Sguenther . ./shlib_version 37659abad11Sguenther new=obj/${lib}.so.${major}.${minor} 37759abad11Sguentherfi 37845cee0b3Sguenther[[ -f $new ]] || fail "$new doesn't exist" 37959abad11Sguenther 38059abad11Sguenther# Filter the output of readelf -s to be easier to parse by removing a 38159abad11Sguenther# field that only appears on some symbols: [<other>: 88] 38259abad11Sguenther# Not really arch-specific, but I've only seen it on alpha 38345cee0b3Sguentherfilt_symtab() { sed 's/\[<other>: [0-9a-f]*\]//'; } 38445cee0b3Sguentherfilt_ret() { egrep -v ' (__retguard_[0-9]+|__llvm_retpoline_[a-z]+[0-9]*)$'; } 38559abad11Sguenther 3863ea1dcecSguentherif $keep_temp 3873ea1dcecSguentherthen 38859abad11Sguenther # precreate all the files we'll use, but with noclobber set to avoid 38959abad11Sguenther # symlink attacks 3903ea1dcecSguenther odir=/tmp 39159abad11Sguenther files= 3923ea1dcecSguenther trap 'ret=$?; rm -f $files; exit $ret' 1 2 15 ERR 3933ea1dcecSguentherelse 3943ea1dcecSguenther trap 'ret=$?; rm -rf "$odir"; exit $ret' 0 1 2 15 ERR 3953ea1dcecSguenther odir=$(mktemp -dt check_sym.XXXXXXXXXX) 3963ea1dcecSguentherfi 3973ea1dcecSguentherset -C 3983ea1dcecSguentherfor i in $odir/$file_list 39959abad11Sguentherdo 40059abad11Sguenther rm -f $i 40159abad11Sguenther 3>$i 40259abad11Sguenther files="$files $i" 40359abad11Sguentherdone 40459abad11Sguentherset +C 40559abad11Sguenther 40659abad11Sguenther 40745cee0b3Sguenther# 40845cee0b3Sguenther# Collect data 40945cee0b3Sguenther# 41045cee0b3Sguenther$dynamic && dynamic_collect 41145cee0b3Sguenther$static && static_collect 41259abad11Sguenther 4133ea1dcecSguenther# Now that we're done accessing $old and $new (which could be 4143ea1dcecSguenther# relative paths), chdir into our work directory, whatever it is 4153ea1dcecSguenthercd $odir 4163ea1dcecSguenther 41745cee0b3Sguenther# 41845cee0b3Sguenther# Do The Job 41945cee0b3Sguenther# 42059abad11Sguentherfor i in 1 2 42159abad11Sguentherdo 42245cee0b3Sguenther $dynamic && dynamic_analysis $i 42345cee0b3Sguenther $static && static_analysis $i 42459abad11Sguentherdone 42559abad11Sguenther 42645cee0b3Sguenther{ 42759abad11Sguenther echo "$old --> $new" 42842dc5753Stb ! $dynamic || dynamic_output 42942dc5753Stb ! $static || static_output 43045cee0b3Sguenther} 43159abad11Sguenther 432