diff --git a/kernel-objects-build.sh b/kernel-objects-build.sh
new file mode 100755
index 000000000000..b92729994ded
--- /dev/null
+++ b/kernel-objects-build.sh
@@ -0,0 +1,132 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+set -u
+set -e
+
+exec 2>&1
+
+OBJECTS="${HOME}/.pahole_test_objects"
+KDIR=${HOME}/linux
+PAHOLE=$(which pahole)
+OUTPUT=
+
+usage()
+{
+	cat <<EOF
+Usage: $0 [-k KERNEL] [-O OUTPUT] [-o OBJECTS]
+
+The script prepares vmlinux and kernel modules for different archs/C:
+
+  - x86 gcc/clang
+  - arm64 gcc/clang
+  - powerpc gcc
+  - s390x gcc
+
+Options:
+  -k) Update kernel tree directory (default HOME/linux)
+  -O) Update temp output directory (default mktemp /tmp/pahole.test.XXX)
+  -o) Update final objects directory (default HOME/.pahole.test.XXX)
+
+Make images under '/tmp/build', and place it under 'objects':
+
+  $ $0 -o objects -O /tmp/build/
+
+EOF
+}
+
+build()
+{
+	local name=$1
+	local opts=$2
+
+	echo "build ${name} (${OUTPUT}/output)"
+
+	mkdir -p ${OBJECTS}/${name}
+	mkdir -p ${OUTPUT}
+
+	pushd ${KDIR}
+	make ${opts} -j"$(nproc)" O=${OUTPUT} olddefconfig > ${OUTPUT}/output 2>&1
+	scripts/config \
+		--file ${OUTPUT}/.config \
+		-e BPF_SYSCALL \
+		-e DEBUG_INFO \
+		-e DEBUG_INFO_BTF \
+		-e FTRACE \
+		-e FUNCTION_TRACER \
+		>> ${OUTPUT}/output 2>&1
+	make ${opts} -j"$(nproc)" O=${OUTPUT} PAHOLE=${PAHOLE} olddefconfig all >> ${OUTPUT}/output 2>&1
+
+	cp ${OUTPUT}/vmlinux ${OBJECTS}/${name}
+	find ${OUTPUT} -name '*.ko' | xargs cp -t ${OBJECTS}/${name}
+
+	rm -rf ${OUTPUT}
+	popd
+}
+
+main()
+{
+	while getopts 'k:o:O:' opt; do
+		case ${opt} in
+		k)
+			KDIR="$OPTARG"
+			;;
+		O)
+			OUTPUT="$OPTARG"
+			;;
+		o)
+			OBJECTS="$OPTARG"
+			;;
+		esac
+	done
+	shift $((OPTIND -1))
+
+	if [[ $# -ne 0 ]]; then
+		usage
+		exit 1
+	fi
+
+        if [[ "${OUTPUT}" == "" ]]; then
+                OUTPUT=$(mktemp -d /tmp/pahole.test.XXX)
+        fi
+
+	PAHOLE=$(realpath ${PAHOLE})
+	OBJECTS=$(realpath ${OBJECTS})
+
+	echo "output:  ${OUTPUT}"
+	echo "kdir:    ${KDIR}"
+	echo "pahole:  ${PAHOLE}"
+	echo "objects: ${OBJECTS}"
+	echo
+
+	mkdir -p ${OBJECTS}
+
+	echo "cleanup ${KDIR}"
+	make -C ${KDIR} mrproper
+
+
+	build x86-clang     "LLVM=1"
+	build x86-gcc       ""
+
+	build aarch64-clang "ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- LLVM=1"
+	build aarch64-gcc   "ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-"
+
+#	build powerpc-clang "ARCH=powerpc CROSS_COMPILE=powerpc64-linux-gnu- LLVM=1"
+	build powerpc-gcc   "ARCH=powerpc CROSS_COMPILE=powerpc64-linux-gnu-"
+
+#	build powerpcle-clang "ARCH=powerpc CROSS_COMPILE=powerpc64le-linux-gnu- LLVM=1"
+	build powerpcle-gcc   "ARCH=powerpc CROSS_COMPILE=powerpc64le-linux-gnu-"
+
+#	build s390x-clang   "ARCH=s390 CROSS_COMPILE=s390x-linux-gnu- LLVM=1"
+	build s390x-gcc     "ARCH=s390 CROSS_COMPILE=s390x-linux-gnu-"
+}
+
+catch()
+{
+	local exit_code=$1
+	exit ${exit_code}
+}
+
+trap 'catch "$?"' EXIT
+
+main "$@"
diff --git a/kernel-objects-test.sh b/kernel-objects-test.sh
new file mode 100755
index 000000000000..a34c22c2eb09
--- /dev/null
+++ b/kernel-objects-test.sh
@@ -0,0 +1,282 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+set -u
+
+exec 2>&1
+
+PAHOLE=$(which pahole)
+BPFTOOL=$(which bpftool)
+BTFDIFF=$(which btfdiff)
+
+OBJECTS="$HOME/.pahole_test_objects"
+CLEANUP="yes"
+BASE=
+FAIL="no"
+
+function test_funcs()
+{
+	local vmlinux=$1
+	local obj=$2
+	local err=0
+
+	cp ${obj} ${BASE}/object
+
+	if [[ ${obj} == *.ko ]]; then
+		${BPFTOOL} --base ${vmlinux} btf dump file ${BASE}/object > ${BASE}/btf.old
+		${PAHOLE} -V -J --btf_base ${vmlinux} ${BASE}/object > ${BASE}/output
+		${BPFTOOL} --base ${vmlinux} btf dump file ${BASE}/object > ${BASE}/btf.new
+	else
+		${BPFTOOL} btf dump file ${BASE}/object > ${BASE}/btf.old
+		${PAHOLE} -V -J ${BASE}/object > ${BASE}/output
+		${BPFTOOL} btf dump file ${BASE}/object > ${BASE}/btf.new
+	fi
+
+	diff -puw ${BASE}/btf.old ${BASE}/btf.new > ${BASE}/diff.all
+	if [ $? -ne 0 ]; then
+		funcs_old=${BASE}/funcs.old
+		funcs_new=${BASE}/funcs.new
+
+		cat ${BASE}/btf.old | grep 'FUNC ' | awk '{ print $3 }' | sort | uniq > ${funcs_old}
+		cat ${BASE}/btf.new | grep 'FUNC ' | awk '{ print $3 }' | sort | uniq > ${funcs_new}
+
+		diff -puw ${funcs_old} ${funcs_new} > ${BASE}/diff.funcs
+	fi
+
+	if [[ $? -ne 0 ]]; then
+		err=1
+	fi
+
+	return ${err};
+}
+
+function test_format_c()
+{
+	local vmlinux=$1
+	local obj=$2
+	local err=0
+
+	cp ${obj} ${BASE}/object
+
+	if [[ ${obj} == *.ko ]]; then
+		${BPFTOOL} --base ${vmlinux} btf dump file ${BASE}/object format c > ${BASE}/c.old
+		${PAHOLE} -V -J --btf_base ${vmlinux} ${BASE}/object > ${BASE}/output
+		${BPFTOOL} --base ${vmlinux} btf dump file ${BASE}/object format c > ${BASE}/c.new
+	else
+		${BPFTOOL} btf dump file ${BASE}/object format c > ${BASE}/c.old
+		${PAHOLE} -V -J ${BASE}/object > ${BASE}/output
+		${BPFTOOL} btf dump file ${BASE}/object format c > ${BASE}/c.new
+	fi
+
+	diff -puw ${BASE}/c.old ${BASE}/c.new > ${BASE}/diff.all
+	if [[ $? -ne 0 ]]; then
+		err=1
+	fi
+
+	return ${err};
+}
+
+function test_btfdiff()
+{
+	local vmlinux=$1
+	local obj=$2
+	local err=0
+
+	if [[ -x ${BTFDIFF} ]]; then
+		${BTFDIFF} ${obj} > ${BASE}/output
+		if [[ -s "${BASE}/output" ]]; then
+			err=1
+		fi
+	else
+		err=2
+	fi
+
+	return ${err}
+}
+
+usage()
+{
+	cat <<EOF
+Usage: $0 [-f] [-o object] [-O objects] [-b BASE] [-P PAHOLE] [-B BPFTOOL] -- [test]
+
+The script runs tests on objects with BTF data.
+
+Options:
+  -f) Stop on failure
+  -o) Run tests on specific objects
+  -O) Update the root objects directory (default HOME/.pahole_test_objects)
+  -b) Update work base/temporary directory (default mktemp -d /tmp/pahole.test.XXX)
+  -P) Update pahole path (default which pahole)
+  -B) Update bpftool path (default which bpftool)
+
+Test image under 'objects':
+
+  $ $0 -O objects/
+
+Test specific image (objects/aarch64-clang) and stop on failure:
+
+  $ $0 -o o objects/aarch64-clang -f
+
+Run specific test (test_format_c):
+
+  $ $0 -o o objects/aarch64-clang -f test_format_c
+EOF
+}
+
+do_test()
+{
+	local test_name=$1
+	local vmlinux=$2
+	local obj=$3
+
+	printf "%-15s on %s ... " "${test_name}"  "${obj}"
+
+	eval ${test_name} ${vmlinux} ${obj}
+	local err=$?
+
+	case ${err} in
+	0)
+		echo "OK"
+		;;
+	1)
+		echo "FAIL"
+		;;
+	2)
+		echo "SKIP"
+		;;
+	esac
+
+	if [[ ${err} -eq 1 && "${FAIL}" == "yes" ]]; then
+		exit 1
+	fi
+
+	return ${err}
+}
+
+run_tests()
+{
+	local vmlinux=$1
+	local obj=$2
+	local test_name=$3
+
+	if [[ "${test_name}" != "all" ]]; then
+		do_test ${test_name} ${vmlinux} ${obj}
+	else
+		do_test test_funcs ${vmlinux} ${obj}
+		do_test test_format_c ${vmlinux} ${obj}
+
+		# btfdiff is only for vmlinux
+		if [[ ${obj} != *.ko ]]; then
+			do_test test_btfdiff ${vmlinux} ${obj}
+		fi
+	fi
+}
+
+do_obj()
+{
+	local obj=$1
+	local test_name=$2
+	local vmlinux=${obj}/vmlinux
+
+	run_tests ${vmlinux} ${vmlinux} ${test_name}
+
+	for kmod in $(ls ${obj}/*.ko); do
+		run_tests ${vmlinux} ${kmod} ${test_name}
+	done
+}
+
+main()
+{
+	local test_name="all"
+
+	while getopts 'b:o:dhP:B:fO:' opt; do
+		case ${opt} in
+		f)
+			FAIL="yes"
+			CLEANUP="no"
+			;;
+		o)
+			obj="$OPTARG"
+			;;
+		O)
+			OBJECTS="$OPTARG"
+			;;
+		b)
+			BASE="$OPTARG"
+			;;
+		P)
+			PAHOLE="$OPTARG"
+			;;
+		B)
+			BPFTOOL="$OPTARG"
+			;;
+		h)
+			usage
+			exit 0
+			;;
+		\? )
+			echo "Invalid Option: -$OPTARG"
+			usage
+			exit 1
+			;;
+		: )
+			echo "Invalid Option: -$OPTARG requires an argument"
+			usage
+			exit 1
+			;;
+		esac
+	done
+	shift $((OPTIND -1))
+
+	if [[ $# -gt 1 ]]; then
+		echo "Invalid test: $@"
+		usage
+		exit 1
+	fi
+
+	if [[ $# -eq 1 ]]; then
+		test_name="$@"
+	fi
+
+	if [[ "${BASE}" == "" ]]; then
+		BASE=$(mktemp -d /tmp/pahole.test.XXX)
+	else
+		mkdir -p ${BASE}
+	fi
+
+	PAHOLE=$(realpath ${PAHOLE})
+	BPFTOOL=$(realpath ${BPFTOOL})
+	OBJECTS=$(realpath ${OBJECTS})
+
+	echo "pahole:  ${PAHOLE}"
+	echo "bpftool: ${BPFTOOL}"
+	echo "base:    ${BASE}"
+	echo "objects: ${obj:-${OBJECTS}}"
+	echo "fail:    ${FAIL}"
+	echo "cleanup: ${CLEANUP}"
+	echo
+
+	if [[ "${obj:=""}" != "" ]]; then
+		do_obj ${obj} ${test_name}
+	else
+		for obj in $(ls ${OBJECTS}); do
+			do_obj ${OBJECTS}/${obj} ${test_name}
+		done
+	fi
+}
+
+catch()
+{
+	local exit_code=$1
+	if [[ "${BASE:=""}" != "" && "${CLEANUP}" == "yes" ]]; then
+		rm -rf ${BASE}
+	else
+		echo
+		echo "Keeping test data in: ${BASE}"
+	fi
+	exit ${exit_code}
+}
+
+trap 'catch "$?"' EXIT
+
+main "$@"
