xref: /linux/tools/objtool/tests/lib.sh (revision d76312b8a1f69be71ec38d04d69fbcdd0520ff89)
1# SPDX-License-Identifier: GPL-2.0
2#
3# Helpers for the objtool klp tests.  A test builds a fixture twice, as the
4# original and (with -DPATCHED) the patched object, runs both through
5# "klp checksum" and diffs them, then asserts on the result.
6#
7# Assertions check properties rather than compare against recorded output:
8# codegen varies between compilers and golden files would report churn instead
9# of regressions.
10
11TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
13# Tests live in generic/ or in an architecture directory beside it, and each
14# carries its own fixtures.
15FIXTURES_DIR="$(cd "$(dirname "$0")/fixtures" 2>/dev/null && pwd)"
16
17# The kernel's convention: CROSS_COMPILE is the one knob, with per-tool
18# overrides for what it does not cover.  objtool itself is always a host binary
19# -- it is built with HOSTCC and only reads ELF -- so an arm64 machine can run
20# the x86 tests against x86 objects given a compiler that emits them.
21#
22# readelf reads any target, so it rarely needs overriding, and either GNU
23# readelf or llvm-readelf will do: the assertions match on fields rather than
24# on columns, and where the two spell something differently -- "OS [0xff20]"
25# against "OS[0xff20]" for SHN_LIVEPATCH -- they accept both.  BFD's objcopy is
26# usually built for the host's target alone, and llvm-objcopy is the
27# target-agnostic replacement.
28CROSS_COMPILE="${CROSS_COMPILE:-}"
29CC="${CC:-${CROSS_COMPILE}gcc}"
30LD="${LD:-${CROSS_COMPILE}ld}"
31READELF="${READELF:-${CROSS_COMPILE}readelf}"
32OBJCOPY="${OBJCOPY:-${CROSS_COMPILE}objcopy}"
33
34OBJTOOL="${OBJTOOL:-$TESTS_DIR/../objtool}"
35
36# klp_preflight
37#
38# Check the environment once, before any test runs, and report what was found.
39#
40klp_preflight()
41{
42	local tmp tool cc_version arch host cc_arch
43
44	bail() { echo "Bail out! $*" >&2; exit 1; }
45
46	# A relative $OBJTOOL is relative to the objtool directory, not tests/.
47	[ -x "$OBJTOOL" ] || [ ! -x "$TESTS_DIR/../$OBJTOOL" ] ||
48		OBJTOOL="$TESTS_DIR/../$OBJTOOL"
49
50	[ -x "$OBJTOOL" ] ||
51		bail "objtool not found at '$OBJTOOL' -- build it first"
52
53	# run_diff() runs objtool from inside the test's working directory, so
54	# a relative path would resolve against that instead.
55	OBJTOOL="$(realpath "$OBJTOOL")"
56
57	"$OBJTOOL" klp 2>&1 | grep -q checksum ||
58		bail "objtool was built without klp support; install libxxhash (>= 0.8) and rebuild"
59
60	command -v "${CC%% *}" >/dev/null || bail "compiler not found: $CC"
61
62	for tool in "$READELF" "$OBJCOPY" "$LD"; do
63		command -v "${tool%% *}" >/dev/null || bail "$tool not found"
64	done
65
66	tmp="$(mktemp -d)" || bail "mktemp failed"
67	echo 'int probe(void) { return 0; }' > "$tmp/probe.c"
68	$CC -c -o "$tmp/probe.o" "$tmp/probe.c" 2>/dev/null ||
69		{ rm -rf "$tmp"; bail "$CC cannot compile a trivial object"; }
70
71	# $CC, $ARCH and objtool have to agree about the target, and cross runs
72	# are where they stop agreeing: plain "CC=clang ARCH=x86_64" on an arm64
73	# box selects the x86 tests and then builds arm64 objects, because clang
74	# needs --target= to emit anything but the host's.
75	#
76	# Ask objtool rather than comparing machine names.  It rejects an object
77	# it was not built for -- "unexpected ELF machine type" -- so one check
78	# covers every way the three can disagree, and says so once instead of
79	# failing every test for the same reason.
80	"$OBJTOOL" klp checksum "$tmp/probe.o" >/dev/null 2>&1 ||
81		{ rm -rf "$tmp"
82		  bail "objtool rejects an object built by '$CC'; they target" \
83		       "different architectures (set CROSS_COMPILE, or" \
84		       "--target= for clang)"; }
85
86	# BFD objcopy is usually built for the host's target alone, and
87	# checksum_of() needs it to read the object under test.
88	$OBJCOPY -O binary --only-section=.text "$tmp/probe.o" "$tmp/probe.bin" 2>/dev/null ||
89		{ rm -rf "$tmp"
90		  bail "$OBJCOPY cannot read objects built by '$CC'; install" \
91		       "binutils-multiarch or set OBJCOPY=llvm-objcopy"; }
92	# $ARCH only chooses which directory of tests runs, so it can disagree
93	# with what $CC builds without objtool noticing -- and the result is the
94	# wrong set of tests, quietly.
95	case "$($READELF -hW "$tmp/probe.o" | sed -n 's/.*Machine: *//p')" in
96	*X86-64*|*Intel*80386*)	cc_arch=x86 ;;
97	*AArch64*)		cc_arch=arm64 ;;
98	*)			cc_arch= ;;
99	esac
100	rm -rf "$tmp"
101
102	# Normalize to the kernel's SRCARCH.
103	case "${ARCH:-$(uname -m)}" in
104	x86_64|i?86)	arch=x86 ;;
105	aarch64*)	arch=arm64 ;;
106	*)		arch="${ARCH:-$(uname -m)}" ;;
107	esac
108
109	case "$(uname -m)" in
110	x86_64|i?86)	host=x86 ;;
111	aarch64*)	host=arm64 ;;
112	*)		host="$(uname -m)" ;;
113	esac
114
115	[ -z "$cc_arch" ] || [ "$cc_arch" = "$arch" ] ||
116		bail "ARCH says $arch but '$CC' builds $cc_arch objects;" \
117		     "the $arch tests would run against the wrong architecture"
118
119	KLP_TEST_ARCH="$arch"
120	KLP_TEST_PREFLIGHT=done
121	export OBJTOOL CC KLP_TEST_ARCH KLP_TEST_PREFLIGHT
122
123	cc_version="$($CC --version 2>/dev/null | head -1)"
124	cat <<EOF
125# preflight
126#   objtool   $OBJTOOL (klp: yes)
127#   compiler  $cc_version
128#   arch      $KLP_TEST_ARCH$([ "$arch" = "$host" ] || echo "  (host $host, cross)")
129EOF
130}
131
132[ -n "${KLP_TEST_PREFLIGHT:-}" ] || klp_preflight
133
134# klp-build compiles the kernel this way; klp diff needs per-symbol sections to
135# extract individual functions.
136FIXTURE_CFLAGS="-c -O2 -ffunction-sections -fdata-sections -fno-asynchronous-unwind-tables"
137
138test_name="$(basename "$0" .sh)"
139workdir=
140
141pass() { echo "ok - $test_name${1:+: $1}"; exit 0; }
142fail() { echo "not ok - $test_name: $1" >&2; exit 1; }
143skip() { echo "ok - $test_name # SKIP $1"; exit 0; }
144
145cleanup() { [ -n "$workdir" ] && rm -rf "$workdir"; }
146
147# setup [exported symbol...]
148setup()
149{
150	# The environment was checked once when this file was sourced, so there
151	# is nothing to verify here: objtool exists at the resolved path, has
152	# klp support, and $CC works.
153	workdir="$(mktemp -d)" || fail "mktemp failed"
154	trap cleanup EXIT
155
156	export_syms "$@"
157}
158
159# export_syms [symbol...]
160#
161# Rewrite Module.symvers so exactly these symbols are exported by vmlinux.
162# Whether a symbol is listed decides between an ordinary relocation and a klp
163# relocation, so tests flip it to cover both.
164export_syms()
165{
166	: > "$workdir/Module.symvers"
167	for sym in "$@"; do
168		printf '0x00000000\t%s\tvmlinux\tEXPORT_SYMBOL\t\n' \
169			"$sym" >> "$workdir/Module.symvers"
170	done
171}
172
173# build_pair <fixture.c> [cflags...]
174build_pair()
175{
176	local fixture="$FIXTURES_DIR/$1"; shift
177
178	[ -f "$fixture" ] || fail "missing fixture $fixture"
179
180	$CC $FIXTURE_CFLAGS "$@" -o "$workdir/orig.o" "$fixture" 2>"$workdir/cc.log" ||
181		skip "fixture does not build here: $(tail -1 "$workdir/cc.log")"
182	$CC $FIXTURE_CFLAGS "$@" -DPATCHED -o "$workdir/patched.o" "$fixture" 2>"$workdir/cc.log" ||
183		skip "fixture does not build here: $(tail -1 "$workdir/cc.log")"
184}
185
186# run_diff [expected exit status]
187run_diff()
188{
189	local expect="${1:-0}" rc=0
190
191	# Checksums live in the objects, so only generate them once even when a
192	# test diffs the same pair again with a different Module.symvers.
193	if [ ! -e "$workdir/.checksummed" ]; then
194		"$OBJTOOL" klp checksum "$workdir/orig.o" ||
195			fail "klp checksum orig.o failed"
196		"$OBJTOOL" klp checksum "$workdir/patched.o" ||
197			fail "klp checksum patched.o failed"
198		touch "$workdir/.checksummed"
199	fi
200
201	# klp diff looks for Module.symvers relative to the working directory.
202	( cd "$workdir" && "$OBJTOOL" klp diff orig.o patched.o out.o ) \
203		> "$workdir/diff.log" 2>&1 || rc=$?
204
205	[ "$rc" = "$expect" ] ||
206		fail "klp diff exited $rc, expected $expect: $(tail -2 "$workdir/diff.log")"
207}
208
209cc_supports()
210{
211	echo 'int f(void) { return 0; }' > "$workdir/flagtest.c"
212	$CC $1 -c "$workdir/flagtest.c" -o "$workdir/flagtest.o" 2>/dev/null
213}
214
215# partial_link <output> <object...>
216#
217# "ld -r" through the compiler driver so the link targets the same
218# architecture as the objects.
219partial_link()
220{
221	local out="$1"; shift
222
223	$CC -r -nostdlib -o "$out" "$@" 2>/dev/null ||
224		$CC -r -nostdlib -fuse-ld=lld -o "$out" "$@" 2>/dev/null
225}
226
227# find_thinlto_toolchain
228#
229# Set $THIN_CC and $THIN_LD to a clang and lld from the same LLVM release.  A
230# mismatched pair fails with "Invalid summary version", which reads like a
231# broken test rather than a broken environment.
232find_thinlto_toolchain()
233{
234	local cc ld ver
235
236	for cc in "${THIN_CC:-}" "$CC" clang; do
237		[ -n "$cc" ] || continue
238		command -v "${cc%% *}" >/dev/null 2>&1 || continue
239
240		ver=$($cc -dumpversion 2>/dev/null | cut -d. -f1)
241
242		for ld in "${THIN_LD:-}" "ld.lld-$ver" ld.lld; do
243			[ -n "$ld" ] || continue
244			command -v "$ld" >/dev/null 2>&1 || continue
245
246			echo 'int probe(void) { return 0; }' > "$workdir/probe.c"
247			$cc -flto=thin -O2 -c "$workdir/probe.c" \
248				-o "$workdir/probe.o" 2>/dev/null || continue
249			"$ld" -r "$workdir/probe.o" -o "$workdir/probe.elf" \
250				2>/dev/null || continue
251
252			THIN_CC="$cc"
253			THIN_LD="$ld"
254			return 0
255		done
256	done
257
258	return 1
259}
260
261out_sections() { $READELF -S -W "$workdir/out.o" 2>/dev/null; }
262out_relocs()   { $READELF -r -W "$workdir/out.o" 2>/dev/null; }
263out_symbols()  { $READELF -s -W "$workdir/out.o" 2>/dev/null; }
264diff_log()     { cat "$workdir/diff.log"; }
265
266assert_section()
267{
268	out_sections | grep -q "[[:space:]]$1[[:space:]]" ||
269		fail "expected section '$1' in output"
270}
271
272assert_patched()
273{
274	assert_section ".text.$1"
275}
276
277assert_not_patched()
278{
279	out_sections | grep -q "[[:space:]].text.$1[[:space:]]" &&
280		fail "function '$1' should not have been cloned"
281	return 0
282}
283