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