[PATCH v2 61/62] livepatch/klp-build: Add --show-first-changed option to show function divergence

From: Josh Poimboeuf
Date: Fri May 09 2025 - 16:30:55 EST


Add a --show-first-changed option to identify where changed functions
begin to diverge:

- Parse 'objtool klp diff' output to find changed functions.

- Run objtool again on each object with --debug-checksum=<funcs>.

- Diff the per-instruction checksum debug output to locate the first
differing instruction.

This can be useful for quickly determining where and why a function
changed.

Signed-off-by: Josh Poimboeuf <jpoimboe@xxxxxxxxxx>
---
scripts/livepatch/klp-build | 82 +++++++++++++++++++++++++++++++++++--
1 file changed, 78 insertions(+), 4 deletions(-)

diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build
index 08ef903d4090..f7d88726ed4f 100755
--- a/scripts/livepatch/klp-build
+++ b/scripts/livepatch/klp-build
@@ -21,7 +21,7 @@ set -o nounset
# This helps keep execution in pipes so pipefail+errexit can catch errors.
shopt -s lastpipe

-unset DEBUG_CLONE SKIP_CLEANUP XTRACE
+unset DEBUG_CLONE DIFF_CHECKSUM SKIP_CLEANUP XTRACE
REPLACE=1
SHORT_CIRCUIT=0
shopt -o xtrace | grep -q 'on' && XTRACE=1
@@ -115,6 +115,7 @@ Usage: $SCRIPT [OPTIONS] PATCH_FILE(s)
Generate a livepatch module.

Options:
+ -f, --show-first-changed Show address of first changed instruction
-o, --output <file.ko> Output file [default: livepatch-<patch-name>.ko]
--no-replace Disable livepatch atomic replace
-v, --verbose Pass V=1 to kernel/module builds
@@ -141,8 +142,8 @@ process_args() {
local long
local args

- short="ho:vDS:T"
- long="help,output:,no-replace,verbose,debug,short-circuit:,keep-tmp"
+ short="hfo:vDS:T"
+ long="help,show-first-changed,output:,no-replace,verbose,debug,short-circuit:,keep-tmp"

args=$(getopt --options "$short" --longoptions "$long" -- "$@") || {
echo; usage; exit
@@ -155,6 +156,10 @@ process_args() {
usage
exit 0
;;
+ -f | --show-first-changed)
+ DIFF_CHECKSUM=1
+ shift
+ ;;
-o | --output)
[[ "$2" != *.ko ]] && die "output filename should end with .ko"
OUTFILE="$2"
@@ -581,6 +586,7 @@ diff_objects() {
local orig_file="$rel_file"
local patched_file="$PATCHED_DIR/$rel_file"
local out_file="$DIFF_DIR/$rel_file"
+ local filter=()
local cmd=()

mkdir -p "$(dirname "$out_file")"
@@ -593,16 +599,80 @@ diff_objects() {
cmd+=("$patched_file")
cmd+=("$out_file")

+ if [[ -v DIFF_CHECKSUM ]]; then
+ filter=("grep0")
+ filter+=("-Ev")
+ filter+=("DEBUG: .*checksum: ")
+ else
+ filter=("cat")
+ fi
+
(
cd "$ORIG_DIR"
"${cmd[@]}" \
> >(tee -a "$log") \
- 2> >(tee -a "$log" >&2) \
+ 2> >(tee -a "$log" | "${filter[@]}" >&2) \
|| die "objtool klp diff failed"
)
done
}

+# For each changed object, run objtool with --debug-checksum to get the
+# per-instruction checksums, and then diff those to find the first changed
+# instruction for each function.
+diff_checksums() {
+ local orig_log="$ORIG_DIR/checksum.log"
+ local patched_log="$PATCHED_DIR/checksum.log"
+ local -A funcs
+ local cmd=()
+ local line
+ local file
+ local func
+
+ gawk '/\.o: changed function: / {
+ sub(/:$/, "", $1)
+ print $1, $NF
+ }' "$KLP_DIFF_LOG" | mapfile -t lines
+
+ for line in "${lines[@]}"; do
+ read -r file func <<< "$line"
+ if [[ ! -v funcs["$file"] ]]; then
+ funcs["$file"]="$func"
+ else
+ funcs["$file"]+=" $func"
+ fi
+ done
+
+ cmd=("$SRC/tools/objtool/objtool")
+ cmd+=("--checksum")
+ cmd+=("--link")
+ cmd+=("--dry-run")
+
+ for file in "${!funcs[@]}"; do
+ local opt="--debug-checksum=${funcs[$file]// /,}"
+
+ (
+ cd "$ORIG_DIR"
+ "${cmd[@]}" "$opt" "$file" &> "$orig_log" \
+ || ( cat "$orig_log" >&2; die "objtool --debug-checksum failed" )
+
+ cd "$PATCHED_DIR"
+ "${cmd[@]}" "$opt" "$file" &> "$patched_log" \
+ || ( cat "$patched_log" >&2; die "objtool --debug-checksum failed" )
+ )
+
+ for func in ${funcs[$file]}; do
+ diff <( grep0 -E "^DEBUG: .*checksum: $func " "$orig_log" | sed "s|$ORIG_DIR/||") \
+ <( grep0 -E "^DEBUG: .*checksum: $func " "$patched_log" | sed "s|$PATCHED_DIR/||") \
+ | gawk '/^< DEBUG: / {
+ gsub(/:/, "")
+ printf "%s: %s: %s\n", $3, $5, $6
+ exit
+ }' || true
+ done
+ done
+}
+
# Build and post-process livepatch module in $KMOD_DIR
build_patch_module() {
local makefile="$KMOD_DIR/Kbuild"
@@ -697,6 +767,10 @@ fi
if (( SHORT_CIRCUIT <= 3 )); then
status "Diffing objects"
diff_objects
+ if [[ -v DIFF_CHECKSUM ]]; then
+ status "Finding first changed instructions"
+ diff_checksums
+ fi
fi

if (( SHORT_CIRCUIT <= 4 )); then
--
2.49.0