xref: /openbsd-src/lib/check_sym (revision b8851fcc53cbe24fd20b090f26dd149e353f6174)
1#!/bin/ksh
2#  $OpenBSD: check_sym,v 1.4 2016/09/22 21:37:02 guenther Exp $
3#
4# Copyright (c) 2016 Philip Guenther <guenther@openbsd.org>
5#
6# Permission to use, copy, modify, and distribute this software for any
7# purpose with or without fee is hereby granted, provided that the above
8# copyright notice and this permission notice appear in all copies.
9#
10# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17#
18#
19#  check_sym -- compare the symbols and external function references in two
20#	versions of a shared library
21#
22#  SYNOPSIS
23#	check_sym [-ch] [old [new]]
24#
25#  DESCRIPTION
26#	Library developers need to be aware when they have changed the
27#	ABI of a library.  To assist them, check_sym examines two versions
28#	of a shared library and reports changes to the following:
29#	 * the set of exported symbols and their strengths
30#	 * the set of undefined symbols referenced
31#	 * the set of lazily-resolved functions (PLT)
32#
33#	In each case, additions and removals are reported; for exported
34#	symbols it also reports when a symbol is weakened or strengthened.
35#
36#	The shared libraries to compare can be specified on the
37#	command-line.  Otherwise, check_sym expects to be run from the
38#	source directory of a library, with a shlib_version file specifying
39#	the version being built and the new library in the obj subdirectory.
40#	If the old library to compare against, wasn't specified either then
41#	check_sym will take the highest version of that library in the
42#	*current* directory, or the highest version of that library in
43#	/usr/lib if it wasn't present in the current directory.
44#
45#	check_sym uses fixed names in /tmp for its intermediate files,
46#	as they contain useful details for those trying to understand
47#	what changed.  If any of them cannot be created by the user,
48#	the command will fail.  The files can be cleaned up using
49#	the -c option.
50#
51#
52#	The *basic* rules of thumb for library versions are: if you
53#	 * stop exporting a symbol, or
54#	 * change the size of a data symbol (not reported by check_sym)
55#	 * start exporting a symbol that an inter-dependent library needs
56#	then you need to bump the MAJOR version of the library.
57#
58#	Otherwise, if you:
59#	 * start exporting a symbol
60#	then you need to bump the MINOR version of the library.
61#
62#  SEE ALSO
63#	readelf(1), elf(5)
64#
65#  AUTHORS
66#	Philip Guenther <guenther@openbsd.org>
67#
68#  CAVEATS
69#	The elf format is infinitely extendable, but check_sym only
70#	handles a few weirdnesses.  Running it on or against new archs
71#	may result in meaningless results.
72#
73#  BUGS
74#	Should report changes in the size of exported data objects.
75#
76
77get_lib_name()
78{
79	sed -n 's/^[ 	]*LIB[ 	]*=[ 	]*\([^ 	]*\).*/\1/p' "$@"
80}
81
82pick_highest()
83{
84	old=
85	omaj=-1
86	omin=0
87	for i
88	do
89		[[ -f $i ]] || continue
90		maj=${i%.*}; maj=${maj##*.}
91		min=${i##*.}
92		if [[ $maj -gt $omaj || ( $maj -eq $omaj && $min -gt $omin ) ]]
93		then
94			old=$i
95			omaj=$maj
96			omin=$min
97		fi
98	done
99	[[ $old != "" ]]
100}
101
102file_list=/tmp/{D{,S,W},J,S,U,d,j,r,s}{1,2}
103
104if [[ $1 = "-h" ]]
105then
106	echo "usage: $0 [-ch] [old [new]]"
107	exit 0
108elif [[ $1 = "-c" ]]
109then
110	rm -f $file_list
111	exit 0
112fi
113
114# Old library?
115if [[ $1 = ?(*/)lib*.so* ]]
116then
117	if [[ ! -f $1 ]]
118	then
119		echo "$1 doesn't exist" >&2
120		exit 1
121	fi
122	old=$1
123	lib=${old##*/}
124	lib=${lib%%.so.*}
125	shift
126else
127	# try determining it from the current directory
128	if [[ -f Makefile ]] && lib=$(get_lib_name Makefile) &&
129	   [[ $lib != "" ]]
130	then
131		lib=lib$lib
132	else
133		lib=libc
134	fi
135
136	# Is there a copy of that lib in the current directory?
137	# If so, use the highest numbered one
138	if ! pick_highest $lib.so.* && ! pick_highest /usr/lib/$lib.so.*
139	then
140		echo "unable to find $lib.so.*" >&2
141		exit 1
142	fi
143fi
144
145# New library?
146if [[ $1 = ?(*/)lib*.so* ]]
147then
148	if [[ ! -f $1 ]]
149	then
150		echo "$1 doesn't exist" >&2
151		exit 1
152	fi
153	new=$1
154	shift
155else
156	# Dig info out of the just built library
157	. ./shlib_version
158	new=obj/${lib}.so.${major}.${minor}
159fi
160
161# Filter the output of readelf -s to be easier to parse by removing a
162# field that only appears on some symbols: [<other>: 88]
163# Not really arch-specific, but I've only seen it on alpha
164filt_symtab() {
165	sed 's/\[<other>: [0-9a-f]*\]//'
166}
167
168# precreate all the files we'll use, but with noclobber set to avoid
169# symlink attacks
170set -C
171files=
172trap 'rm -f $files' 1 2 15 ERR
173for i in $file_list
174do
175	rm -f $i
176	3>$i
177	files="$files $i"
178done
179set +C
180
181readelf -rW $old > /tmp/r1
182readelf -rW $new > /tmp/r2
183
184readelf -sW $old | filt_symtab > /tmp/s1
185readelf -sW $new | filt_symtab > /tmp/s2
186
187
188cpu=$(uname -p)
189if [[ $cpu = mips64* ]]
190then
191	gotsym1=$(readelf -d $old | awk '$2 ~ /MIPS_GOTSYM/{print $3}')
192	gotsym2=$(readelf -d $new | awk '$2 ~ /MIPS_GOTSYM/{print $3}')
193fi
194
195jump_slots() {
196	case $cpu in
197	hppa*)	awk '/IPLT/ && $5 != ""{print $5}' /tmp/r$1
198		;;
199	mips*)	# the $((gotsym$1)) converts hex to decimal
200		awk -v g=$((gotsym$1)) \
201			'/^Symbol table ..symtab/{exit}
202			$1+0 >= g && $4 == "FUNC" {print $8}' /tmp/s$1
203		;;
204	*)	awk '/JU*MP_SL/ && $5 != ""{print $5}' /tmp/r$1
205		;;
206	esac | sort -o /tmp/j$1
207}
208
209dynamic_sym() {
210	awk -v s=$1 '/^Symbol table ..symtab/{exit}
211		! /^ *[1-9]/   {next}
212		$7 == "UND"    {print $8 | ("sort -o /tmp/U" s); next }
213		$5 == "GLOBAL" {print $8 | ("sort -o /tmp/DS" s) }
214		$5 == "WEAK"   {print $8 | ("sort -o /tmp/DW" s) }
215		$5 != "LOCAL"  {print $8 | ("sort -o /tmp/D" s) }
216		{print $4, $5, $6, $8}' /tmp/s$1 | sort -o /tmp/d$1
217#	awk -v s=$1 '$2 == "GLOBAL" {print $4 | ("sort -o /tmp/DS" s) }
218#		     $2 == "WEAK"   {print $4 | ("sort -o /tmp/DW" s) }
219#		     $1 != "SECTION"{print $4}' /tmp/d$1 | sort -o /tmp/D$1
220}
221
222static_sym() {
223	awk '/^Symbol table ..symtab/{s=1}
224	     /LOCAL/{next}
225	     s&&/^ *[1-9]/{print $4, $5, $6, $8}' /tmp/s$1 | sort -o /tmp/S$1
226}
227
228output_if_not_empty() {
229	leader=$1
230	shift
231	if "$@" | grep -q .
232	then
233		echo "$leader"
234		"$@" | sed 's:^:	:'
235		echo
236	fi
237}
238
239
240for i in 1 2
241do
242	jump_slots $i
243	dynamic_sym $i
244	static_sym $i
245	comm -23 /tmp/j$i /tmp/U$i >/tmp/J$i
246done
247
248echo "$old --> $new"
249if cmp -s /tmp/d[12]
250then
251	printf "No dynamic export changes\n"
252else
253	printf "Dynamic export changes:\n"
254	output_if_not_empty "added:" comm -13 /tmp/D[12]
255	output_if_not_empty "removed:" comm -23 /tmp/D[12]
256	output_if_not_empty "weakened:" comm -12 /tmp/DS1 /tmp/DW2
257	output_if_not_empty "strengthened:" comm -12 /tmp/DW1 /tmp/DS2
258fi
259if ! cmp -s /tmp/U[12]
260then
261	printf "External reference changes:\n"
262	output_if_not_empty "added:" comm -13 /tmp/U[12]
263	output_if_not_empty "removed:" comm -23 /tmp/U[12]
264fi
265
266if false; then
267	printf "\nReloc counts:\nbefore:\n"
268	grep ^R /tmp/r1
269	printf "\nafter:\n"
270	grep ^R /tmp/r2
271fi
272
273output_if_not_empty "PLT added:" comm -13 /tmp/J1 /tmp/J2
274output_if_not_empty "PLT removed:" comm -23 /tmp/J1 /tmp/J2
275