[tip: objtool/core] objtool/klp: Build klp test fixtures through the harness

From: tip-bot2 for Song Liu

Date: Fri Sep 18 2026 - 06:33:29 EST


The following commit has been merged into the objtool/core branch of tip:

Commit-ID: 18289c2240be1f7122a7e696c05475c09b64f904
Gitweb: https://git.kernel.org/tip/18289c2240be1f7122a7e696c05475c09b64f904
Author: Song Liu <song@xxxxxxxxxx>
AuthorDate: Wed, 16 Sep 2026 11:42:58 -07:00
Committer: Josh Poimboeuf <jpoimboe@xxxxxxxxxx>
CommitterDate: Wed, 16 Sep 2026 17:13:26 -07:00

objtool/klp: Build klp test fixtures through the harness

Compiling a fixture is the one thing every test does, and it has more
invariants than it looks.

- The flags describe the kernel a fixture stands in for, and each is
written down with its reason. That substitution is the whole mechanism
by which these tests cover configurations without building a kernel --
objtool reads no .config, so a configuration reaches it only as
compiler flags and the bytes they produce -- and a flag with no stated
motive is indistinguishable from a mistake.

- -c is not one of them. Producing an object rather than a program is
the helper's contract, not something a test may reconsider, so it lives
at the compile and an override cannot drop it.

- A fixture which will not compile is a failure, not a skip. It is far
more often a mistake in the fixture than a compiler which cannot
express the construct, and the two are indistinguishable once reported
as a skip.

- A newly produced object has no checksums in it, so producing one
invalidates the record that checksumming has already been done. Left
to itself that invariant ends up in each test which rebuilds.

build_one handles a single object for the tests which need more than two or
different names; build_pair and build_module_pair are expressed in terms of
it, so none of the above can be lost by going around them.

run_checksum comes out of run_diff for the same reason: a test which wants
checksums without a diff should not have to run one.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@xxxxxxxxxx>
Link: https://patch.msgid.link/20260916184351.2720310-6-song@xxxxxxxxxx
Signed-off-by: Josh Poimboeuf <jpoimboe@xxxxxxxxxx>
---
tools/objtool/tests/lib.sh | 205 ++++++++++++++++++++++++++++++++----
1 file changed, 186 insertions(+), 19 deletions(-)

diff --git a/tools/objtool/tests/lib.sh b/tools/objtool/tests/lib.sh
index 1531caa..a4513ae 100644
--- a/tools/objtool/tests/lib.sh
+++ b/tools/objtool/tests/lib.sh
@@ -131,13 +131,35 @@ EOF

[ -n "${KLP_TEST_PREFLIGHT:-}" ] || klp_preflight

-# klp-build compiles the kernel this way; klp diff needs per-symbol sections to
-# extract individual functions.
-FIXTURE_CFLAGS="-c -O2 -ffunction-sections -fdata-sections -fno-asynchronous-unwind-tables"
+# What every fixture is built with. These describe the kernel a fixture stands
+# in for; -c is build_one's contract rather than a property of that kernel, so
+# it lives at the compile where an override cannot drop it.
+#
+# -O2 the kernel's default
+# -ffunction-sections -fdata-sections klp-build passes these itself, through
+# KCFLAGS, whatever the configuration
+# -fno-asynchronous-unwind-tables arch/x86/Makefile sets this always, so
+# kernel objects carry no .eh_frame
+# -fno-common the kernel's Makefile sets it, so an
+# uninitialised global there lands in
+# .bss rather than being SHN_COMMON,
+# which has no section and so no
+# checksum
+#
+# A test overrides it; see tools/objtool/Documentation/klp-write-tests.txt.
+FIXTURE_CFLAGS="-O2 -ffunction-sections -fdata-sections -fno-common \
+ -fno-asynchronous-unwind-tables"

test_name="$(basename "$0" .sh)"
workdir=

+# The pair run_diff() and the checksum helpers work on. build_pair() names
+# them again and make_vmlinux_pair() repoints orig_obj at the image it links,
+# but a test which builds its objects itself with build_one() sets neither, so
+# the default belongs here.
+orig_obj=orig.o
+patched_obj=patched.o
+
pass() { echo "ok - $test_name${1:+: $1}"; exit 0; }
fail() { echo "not ok - $test_name: $1"; exit 1; }

@@ -217,17 +239,78 @@ clang_only()
declared_skip "clang only${1:+: $1}"
}

+# build_one <fixture.c> <output object> [cflags...]
+build_one()
+{
+ local fixture out
+ fixture="$FIXTURES_DIR/$1"
+ out="$workdir/$2"
+ shift 2
+
+ [ -f "$fixture" ] || fail "missing fixture $fixture"
+
+ # run_checksum only runs once per workdir. A fresh object has no
+ # checksums in it, so anything built now needs that to happen again.
+ rm -f "$workdir/.checksummed"
+
+ $CC -c $FIXTURE_CFLAGS "$@" -o "$out" "$fixture" 2>"$workdir/cc.log" ||
+ fail "$(basename "$fixture") does not build: $(tail -1 "$workdir/cc.log")"
+}
+
# build_pair <fixture.c> [cflags...]
build_pair()
{
- local fixture="$FIXTURES_DIR/$1"; shift
+ local fixture="$1"; shift

- [ -f "$fixture" ] || fail "missing fixture $fixture"
+ # Name what this builds. A test may run several segments, and
+ # make_vmlinux_pair() repoints orig_obj at the image it links, so
+ # without this the next run_diff() would still be reading that.
+ orig_obj=orig.o
+ patched_obj=patched.o
+
+ build_one "$fixture" orig.o "$@"
+ build_one "$fixture" patched.o "$@" -DPATCHED
+}

- $CC $FIXTURE_CFLAGS "$@" -o "$workdir/orig.o" "$fixture" 2>"$workdir/cc.log" ||
- probe_skip "fixture does not build here: $(tail -1 "$workdir/cc.log")"
- $CC $FIXTURE_CFLAGS "$@" -DPATCHED -o "$workdir/patched.o" "$fixture" 2>"$workdir/cc.log" ||
- probe_skip "fixture does not build here: $(tail -1 "$workdir/cc.log")"
+# run_objtool_check <objtool arguments...>
+#
+# Run objtool's ordinary check pass over the pair, as the kernel build does.
+#
+# Some of what klp diff consumes is produced by this pass rather than by the
+# compiler: .static_call_sites, .mcount_loc, .ibt_endbr_seal, ORC.
+#
+# Only module objects see it before klp-build -- with CONFIG_KLP_BUILD the
+# per-object pass is deferred, so built-in objects reach klp diff exactly as
+# the compiler left them.
+run_objtool_check()
+{
+ local obj
+
+ # This rewrites both objects, so checksums taken before it describe
+ # something that no longer exists. As in build_one(), drop the marker
+ # so run_checksum() takes them again.
+ rm -f "$workdir/.checksummed"
+
+ for obj in "$orig_obj" "$patched_obj"; do
+ "$OBJTOOL" "$@" "$workdir/$obj" ||
+ fail "objtool $* failed on $obj"
+ done
+}
+
+run_checksum()
+{
+ # Checksums live in the objects, and a test may ask for them more than
+ # once -- diffing the same pair again with a different Module.symvers,
+ # say. objtool does the right thing when asked twice, leaving the
+ # object alone, but it says so, and that warning would be most of what
+ # a passing run prints. Remember instead, and keep quiet.
+ [ -e "$workdir/.checksummed" ] && return 0
+
+ "$OBJTOOL" klp checksum "$workdir/$orig_obj" ||
+ fail "klp checksum $orig_obj failed"
+ "$OBJTOOL" klp checksum "$workdir/$patched_obj" ||
+ fail "klp checksum $patched_obj failed"
+ touch "$workdir/.checksummed"
}

# run_diff [expected exit status]
@@ -235,18 +318,10 @@ run_diff()
{
local expect="${1:-0}" rc=0

- # Checksums live in the objects, so only generate them once even when a
- # test diffs the same pair again with a different Module.symvers.
- if [ ! -e "$workdir/.checksummed" ]; then
- "$OBJTOOL" klp checksum "$workdir/orig.o" ||
- fail "klp checksum orig.o failed"
- "$OBJTOOL" klp checksum "$workdir/patched.o" ||
- fail "klp checksum patched.o failed"
- touch "$workdir/.checksummed"
- fi
+ run_checksum

# klp diff looks for Module.symvers relative to the working directory.
- ( cd "$workdir" && "$OBJTOOL" klp diff orig.o patched.o out.o ) \
+ ( cd "$workdir" && "$OBJTOOL" klp diff "$orig_obj" "$patched_obj" out.o ) \
> "$workdir/diff.log" 2>&1 || rc=$?

[ "$rc" = "$expect" ] ||
@@ -267,10 +342,102 @@ partial_link()
{
local out="$1"; shift

+ rm -f "$workdir/.checksummed"
+
$CC -r -nostdlib -o "$out" "$@" 2>/dev/null ||
$CC -r -nostdlib -fuse-ld=lld -o "$out" "$@" 2>/dev/null
}

+# link_vmlinux <output> <object...>
+#
+# Link objects into an executable, the way the kernel's final link produces
+# vmlinux from vmlinux.o. Entry point 0 and no libc: nothing runs it, it only
+# has to be a linked image with resolved addresses.
+#
+# The sub-sections have to come out in name order rather than object order,
+# the way the kernel's linker script gathers .text.unlikely and .data.. apart
+# from the rest. That reordering is the entire reason .klp.symid exists: a
+# link which preserves order cannot tell a correct sympos from one that merely
+# counted, and the caller checks the two orders really did diverge.
+#
+# A linker script rather than --sort-section=name, because lld accepts that
+# option and ignores it -- so on a host where only lld can link the target, the
+# test would quietly stop testing the thing it is named for.
+#
+# Three attempts because a cross run has neither $LD nor the compiler's default
+# linker able to touch the target: on an arm64 host linking x86 objects, only
+# lld will do it.
+link_vmlinux()
+{
+ local out="$1" lds="$workdir/sort.lds"; shift
+
+ echo 'SECTIONS { .data : { *(SORT_BY_NAME(.data.*)) } }' > "$lds"
+
+ $LD -e 0 -T "$lds" -o "$out" "$@" 2>/dev/null ||
+ $CC -nostdlib -Wl,-e,0 -Wl,-T,"$lds" \
+ -o "$out" "$@" 2>/dev/null ||
+ $CC -nostdlib -fuse-ld=lld -Wl,-e,0 -Wl,-T,"$lds" \
+ -o "$out" "$@" 2>/dev/null
+}
+
+# make_vmlinux_pair <orig object...> -- <patched object...>
+#
+# Build the vmlinux.o / vmlinux pair klp diff needs to resolve sympos the way
+# it does for built-in code, and point the diff at it.
+#
+# For a module, sympos is a count in symbol table order, which klp diff can do
+# from the object alone. vmlinux is different: the final link reorders
+# sub-sections, so the position comes from the linked image, bridged by
+# .klp.symid. klp diff only looks for that when the object it was handed is
+# called vmlinux.o and a vmlinux sits beside it -- so both the name and the
+# linked image matter.
+make_vmlinux_pair()
+{
+ local orig=() patched=() seen= arg
+
+ for arg in "$@"; do
+ if [ "$arg" = -- ]; then seen=y; continue; fi
+ if [ -n "$seen" ]; then patched+=( "$arg" ); else orig+=( "$arg" ); fi
+ done
+
+ # Both sides have to have been named. Without this, forgetting the --
+ # leaves one list empty, the link of nothing fails, and the test skips
+ # saying the toolchain cannot link -- which is a test bug wearing the
+ # costume of an environment one.
+ [ "${#orig[@]}" -gt 0 ] && [ "${#patched[@]}" -gt 0 ] ||
+ fail "make_vmlinux_pair needs objects either side of --"
+
+ partial_link "$workdir/vmlinux.o" "${orig[@]}" ||
+ probe_skip "partial link unavailable"
+ partial_link "$workdir/patched.o" "${patched[@]}" ||
+ probe_skip "partial link unavailable"
+
+ "$OBJTOOL" --klp-symids --link "$workdir/vmlinux.o" ||
+ fail "objtool --klp-symids failed"
+
+ link_vmlinux "$workdir/vmlinux" "$workdir/vmlinux.o" ||
+ probe_skip "cannot link a vmlinux here"
+
+ orig_obj=vmlinux.o
+}
+
+# build_module_pair <fixture.c> <module name> [cflags...]
+#
+# Build the pair as objects belonging to a module rather than to vmlinux. klp
+# diff reads the object's module name from .modinfo, and that decides which
+# object a relocation is attributed to and whether a reference counts as
+# cross-module, so a good deal of the code has a module path the vmlinux
+# fixtures never reach.
+#
+# The fixture defines its .modinfo name from MODNAME. Passing that through
+# -D needs two levels of quoting, which is easy to get wrong at the call site.
+build_module_pair()
+{
+ local fixture="$1" modname="$2"; shift 2
+
+ build_pair "$fixture" -DMODNAME="\"$modname\"" "$@"
+}
+
# find_thinlto_toolchain
#
# Set $THIN_LD to an lld from the same LLVM release as $CC (or THIN_CC). A