From 22727a9b65d2c6187721c739ff4f419e2bce616f Mon Sep 17 00:00:00 2001 From: Pete Swain Date: Wed, 12 Jul 2023 08:20:00 -0700 Subject: [PATCH] kpatch-build: support CONFIG_LTO_CLANG_THIN Support CONFIG_LTO_CLANG_THIN with ld.lld --lto-obj-path option. With CONFIG_LTO_CLANG_THIN, .o files are LLVM IR binary, so CDO doesn't work on .o file. To solve this issue, we CDO the thinlto files generated by the --lto-obj-path option. Clang LTO generates the thinlto files after cross file inline, so they are good candidates for CDO. See [1] for more discussions about this. To achieve this, we need: 1. kpatch-build to update kernel Makefile(s) so it generates thinlto files; 2. kpatch-build and kpatch-cc to save the thinlto file properly; 3. kpatch-build to feed these thinlto files to CDO; 4. The user need to supply vmlinux.o, from which we generate the symtab file. We need this because GLOBAL symbols may be marked as LOCAL in LTO vmlinux; 4. A small workaround in CDO that ignores changes in init.text for vmlinux.o.thinlto.* [1] https://github.com/dynup/kpatch/issues/1320 Signed-off-by: Song Liu --- kpatch-build/create-diff-object.c | 15 ++++++ kpatch-build/kpatch-build | 79 +++++++++++++++++++++++++++++-- kpatch-build/kpatch-cc | 18 +++++++ 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/kpatch-build/create-diff-object.c b/kpatch-build/create-diff-object.c index 3a0207fec..9ca6b640d 100644 --- a/kpatch-build/create-diff-object.c +++ b/kpatch-build/create-diff-object.c @@ -2942,6 +2942,21 @@ static void kpatch_mark_ignored_sections(struct kpatch_elf *kelf) !strcmp(sec->name, "__patchable_function_entries")) sec->ignore = 1; } + + /* + * With CONFIG_LTO_CLANG, we see some weird new functions, + * such as: + * __initstub__kmod_syscall__728_5326_bpf_syscall_sysctl_init7 + * while the original function is very similar, like: + * __initstub__kmod_syscall__728_5324_bpf_syscall_sysctl_init7 + * + * We don't have very good solution for it yet. To workaround + * the issue, it seems safe to ignore .init.text changes in + * vmlinux.o. This will skip such new functions. + */ + if (!strncmp(childobj, "vmlinux.o", 9) && + !strncmp(sec->name, ".init.text", 10)) + sec->ignore = 1; } sec = find_section_by_name(&kelf->sections, ".kpatch.ignore.sections"); diff --git a/kpatch-build/kpatch-build b/kpatch-build/kpatch-build index 8feee660f..b6282f7cd 100755 --- a/kpatch-build/kpatch-build +++ b/kpatch-build/kpatch-build @@ -176,6 +176,12 @@ cleanup() { [[ -e "$TEMPDIR/Makefile.modfinal" ]] && mv -f "$TEMPDIR/Makefile.modfinal" "$KERNEL_SRCDIR/scripts" [[ -e "$TEMPDIR/setlocalversion" ]] && mv -f "$TEMPDIR/setlocalversion" "$KERNEL_SRCDIR/scripts" + # restore original Makefile.build if we updated it for the build + [[ -e "$TEMPDIR/Makefile.build" ]] && mv -f "$TEMPDIR/Makefile.build" "$KERNEL_SRCDIR/scripts" + + # restore Makefile if we updated it for the build + [[ -e "$TEMPDIR/Makefile" ]] && mv -f "$TEMPDIR/Makefile" "$KERNEL_SRCDIR" + [[ "$DEBUG" -eq 0 ]] && rm -rf "$TEMPDIR" rm -rf "$RPMTOPDIR" unset KCFLAGS @@ -1175,6 +1181,35 @@ if [[ -n "$CONFIG_DEBUG_INFO_BTF" ]]; then fi fi +if [[ -n "$CONFIG_LTO_CLANG" ]]; then + [[ -n "$CONFIG_LTO_CLANG_THIN" ]] || die "Only thin lto is supported. Try enable CONFIG_LTO_CLANG_THIN." + + # With CONFIG_LTO_CLANG, vmlinux has LOCAL symbols for some GLOBAL + # functions. For example: + # + # readelf -s vmlinux | grep perf_event_bpf_event + # + # non-LTO: + # 124946: ffffffff81287070 858 FUNC GLOBAL DEFAULT 1 perf_event_bpf_event + # + # LTO: + # 122028: ffffffff81298cd0 861 FUNC LOCAL HIDDEN 1 perf_event_bpf_event + # + # To work around this, use vmlinux.o instead. + [[ -e "$VMLINUX" ]] || die "For kernel with CONFIG_LTO_CLANG, please supply vmlinux.o via -v|--vmlinux option." + + # This is a heuristic: use -x to check vmlinux vs. vmlinux.o + [[ -x "$VMLINUX" ]] && die "For kernel with CONFIG_LTO_CLANG, please supply vmlinux.o instead of vmlinux via -v|--vmlinux option." + + cp -f "$KERNEL_SRCDIR/Makefile" "$TEMPDIR/Makefile" || die + sed -i "s/--thinlto-cache-dir=\$(extmod_prefix).thinlto-cache/--lto-obj-path=vmlinux.o.thinlto.o/g" "$KERNEL_SRCDIR"/Makefile + + cp -f "$KERNEL_SRCDIR/scripts/Makefile.build" "$TEMPDIR/Makefile.build" || die + sed -i "s/\$(ld_flags)/\$(ld_flags) --lto-obj-path=\$@.thinlto.o/g" "$KERNEL_SRCDIR"/scripts/Makefile.build + + export KPATCH_CC_HANDLE_LTO=1 +fi + if [[ -n "$CONFIG_CC_IS_CLANG" ]]; then echo "WARNING: Clang support is experimental" fi @@ -1228,6 +1263,7 @@ unset KPATCH_GCC_TEMPDIR if [[ -n "$CONFIG_CC_IS_CLANG" ]]; then MAKEVARS+=("CC=${KPATCH_CC_PREFIX}${CLANG}") MAKEVARS+=("HOSTCC=${HOSTCC:-${CLANG}}") + MAKEVARS+=("LLVM=1") else MAKEVARS+=("CC=${KPATCH_CC_PREFIX}${GCC}") MAKEVARS+=("HOSTCC=${HOSTCC:-${GCC}}") @@ -1297,9 +1333,14 @@ if [[ -n "$CONFIG_MODVERSIONS" ]]; then trace_on fi +if [[ -n "$CONFIG_LTO_CLANG" ]]; then + DIFF_OBJS="$TEMPDIR/thinlto_objs" +else + DIFF_OBJS="$TEMPDIR/changed_objs" +fi # Read as words, no quotes. # shellcheck disable=SC2013 -for i in $(cat "$TEMPDIR/changed_objs") +for i in $(cat "$DIFF_OBJS") do mkdir -p "$TEMPDIR/patched/$(dirname "$i")" || die cp -f "$BUILDDIR/$i" "$TEMPDIR/patched/$i" || die @@ -1328,7 +1369,8 @@ if [[ -z "$MODNAME" ]] ; then MODNAME="$(module_name_string "$MODNAME")" fi -FILES="$(cat "$TEMPDIR/changed_objs")" +FILES="$(cat "$DIFF_OBJS")" + cd "$TEMPDIR" || die mkdir output declare -a objnames @@ -1352,7 +1394,11 @@ for i in $FILES; do mkdir -p "output/$(dirname "$i")" cd "$BUILDDIR" || die - find_kobj "$i" + if [[ -z "$CONFIG_LTO_CLANG" ]] ; then + find_kobj "$i" + else + KOBJFILE=${i/.o.thinlto.o*/} + fi cd "$TEMPDIR" || die if [[ -e "orig/$i" ]]; then if [[ -n $OOT_MODULE ]]; then @@ -1361,6 +1407,19 @@ for i in $FILES; do KOBJFILE_PATH="$OOT_MODULE" SYMTAB="${TEMPDIR}/module/${KOBJFILE_NAME}.symtab" SYMVERS_FILE="$TEMPDIR/Module.symvers" + elif [[ -n "$CONFIG_LTO_CLANG" ]] ; then + if [[ $KOBJFILE = vmlinux ]] ; then + KOBJFILE_NAME=vmlinux + KOBJFILE_PATH="$VMLINUX" + SYMTAB="${TEMPDIR}/${KOBJFILE_NAME}.symtab" + SYMVERS_FILE="$BUILDDIR/Module.symvers" + else + KOBJFILE_NAME=$(basename "${KOBJFILE%.ko}") + KOBJFILE_NAME="${KOBJFILE_NAME//-/_}" + KOBJFILE_PATH="${TEMPDIR}/module/$KOBJFILE.ko" + SYMTAB="${KOBJFILE_PATH}.symtab" + SYMVERS_FILE="$BUILDDIR/Module.symvers" + fi elif [[ "$(basename "$KOBJFILE")" = vmlinux ]]; then KOBJFILE_NAME=vmlinux KOBJFILE_PATH="$VMLINUX" @@ -1374,11 +1433,21 @@ for i in $FILES; do SYMVERS_FILE="$BUILDDIR/Module.symvers" fi - "$READELF" -s --wide "$KOBJFILE_PATH" > "$SYMTAB" + # With CONFIG_LTO_CLANG, multiple .thinlto files share a + # symtab file. Only generate the symtab file once. + [[ -e "$SYMTAB" ]] || "$READELF" -s --wide "$KOBJFILE_PATH" > "$SYMTAB" if [[ "$ARCH" = "ppc64le" ]]; then sed -ri 's/\s+\[: 8\]//' "$SYMTAB" fi + if [[ -n "$CONFIG_LTO_CLANG" ]] ; then + # skip .thinlto file that didn't change at all + diff "orig/$i" "patched/$i" 2> /dev/null && continue + # skip .thinlto file without any functions + num_func=$("$READELF" --symbols "orig/$i" | grep -c FUNC) + [[ $num_func -eq 0 ]] && continue + fi + # create-diff-object orig.o patched.o parent-name parent-symtab # Module.symvers patch-mod-name output.o "$TOOLSDIR"/create-diff-object $CDO_FLAGS "orig/$i" "patched/$i" "$KOBJFILE_NAME" \ @@ -1431,7 +1500,7 @@ fi cd "$TEMPDIR/output" || die # $KPATCH_LDFLAGS and result of find used as list, no quotes. # shellcheck disable=SC2086,SC2046 -"$LD" -r $KPATCH_LDFLAGS -o ../patch/tmp_output.o $(find . -name "*.o") 2>&1 | logger || die +"$LD" -r $KPATCH_LDFLAGS -o ../patch/tmp_output.o $(find . -name "*.o*") 2>&1 | logger || die if [[ "$USE_KLP" -eq 1 ]]; then cp -f "$TEMPDIR"/patch/tmp_output.o "$TEMPDIR"/patch/output.o || die diff --git a/kpatch-build/kpatch-cc b/kpatch-build/kpatch-cc index ee340fb59..ff74b2fc4 100755 --- a/kpatch-build/kpatch-cc +++ b/kpatch-build/kpatch-cc @@ -87,6 +87,24 @@ if [[ "$TOOLCHAINCMD" =~ ^(.*[-/])?(gcc|clang)$ ]] ; then args+=(--warn-unresolved-symbols) break ;; + */.tmp_*.o) + # .tmp_*.o is used for single file modules. + # See "cmd_ld_single_m" in scripts/Makefile.build. + if [[ $KPATCH_CC_HANDLE_LTO -ne 0 ]] ; then + mkdir -p "$KPATCH_GCC_TEMPDIR/orig/$(dirname "$relobj")" + cp "${obj/.tmp_/}".thinlto.o* "$KPATCH_GCC_TEMPDIR/orig/$(dirname "$relobj")" + echo "${obj/.tmp_/}".thinlto.o* >> "$KPATCH_GCC_TEMPDIR/thinlto_objs" + fi + break + ;; + *.o) + if [[ $KPATCH_CC_HANDLE_LTO -ne 0 ]] ; then + mkdir -p "$KPATCH_GCC_TEMPDIR/orig/$(dirname "$relobj")" + cp "$obj".thinlto* "$KPATCH_GCC_TEMPDIR/orig/$(dirname "$relobj")" + echo "$obj".thinlto.o* >> "$KPATCH_GCC_TEMPDIR/thinlto_objs" + fi + break + ;; *) break ;;