[PATCH RFC v3 8/8] selftests/kcov_dataflow: add trigger-view.py

0 views
Skip to first unread message

Yunseong Kim

unread,
Sep 2, 2026, 12:30:24 PM (8 days ago) Sep 2
to linux-...@vger.kernel.org, kasa...@googlegroups.com, linu...@kvack.org, linux-...@vger.kernel.org, rust-fo...@vger.kernel.org, ll...@lists.linux.dev, work...@vger.kernel.org, linu...@vger.kernel.org, linux-k...@vger.kernel.org, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Andrew Morton, David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett, Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko, Kees Cook, Nathan Chancellor, Nicolas Schier, Josh Poimboeuf, Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich, Daniel Almeida, Tamir Duberstein, Alexandre Courbot, Onur Özkan, Nick Desaulniers, Bill Wendling, Marco Elver, Nikita Popov, Matt Arsenault, Justin Stitt, Jonathan Corbet, Shuah Khan, Randy Dunlap, Shuah Khan, Yeoreum Yun, Yunseong Kim, Yunseong Kim
Wire the kcov_dataflow test modules into a standard kselftest target and
add the viewer they are built on.

trigger-view.py loads a module with finit_module(), enables recording
(KCOV_DF_ENABLE, or KCOV_DF_REMOTE_ENABLE with "--remote" argument),
writes the module's debugfs trigger file, unloads it, and decodes
the buffer into a call tree or, with "--raw", a flat record list.
It symbolizes PCs against a vmlinux with addr2line or against
/proc/kallsyms, adding the running kernel's KASLR offset back to
the recorded PCs (runtime _text minus the link-time _text from
System.map / nm / a per-architecture default), and demangles
Rust v0 symbols.

test_modules.py is the KTAP runner: it drives run_capture() from
trigger-view.py for each module and compares the captured arguments,
struct fields and return values against the values the module uses
emitting the module's call tree as diagnostics. A test passes only
when the data came back intact, not merely when records appeared.
Modules that were not built are reported as SKIP.

The Makefile builds user_ioctl and binderfs as ordinary kselftest
programs and the .ko modules via Kbuild against KDIR (the Rust ones only
when the configured kernel has CONFIG_RUST=y); config lists the kernel
options the tests need, and settings raises the per-test timeout for the
in-VM runs. The whole target builds with

make -C tools/testing/selftests TARGETS=kcov_dataflow \
LLVM=1 CC=clang [RUSTC=... RUST_LIB_SRC=...]

and runs under run_kselftest.sh.

Assisted-by: Claude:claude-opus-4-6 [kiro-chat]
Signed-off-by: Yunseong Kim <yunseo...@est.tech>
---
tools/testing/selftests/kcov_dataflow/.gitignore | 4 +
tools/testing/selftests/kcov_dataflow/Kbuild | 10 +
tools/testing/selftests/kcov_dataflow/Makefile | 46 ++
tools/testing/selftests/kcov_dataflow/README.rst | 69 ++
tools/testing/selftests/kcov_dataflow/config | 11 +
tools/testing/selftests/kcov_dataflow/settings | 1 +
.../selftests/kcov_dataflow/test_modules.py | 249 +++++++
.../selftests/kcov_dataflow/trigger-view.py | 755 +++++++++++++++++++++
8 files changed, 1145 insertions(+)

diff --git a/tools/testing/selftests/kcov_dataflow/.gitignore b/tools/testing/selftests/kcov_dataflow/.gitignore
new file mode 100644
index 0000000000000..4f2957a017957
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/.gitignore
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0
+user_ioctl/user_ioctl
+binderfs/binderfs_test
+__pycache__/
diff --git a/tools/testing/selftests/kcov_dataflow/Kbuild b/tools/testing/selftests/kcov_dataflow/Kbuild
new file mode 100644
index 0000000000000..2e19e9008fdca
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/Kbuild
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Test modules, built as external modules against the configured kernel tree
+# by the selftest Makefile ("make -C $(KDIR) M=$(CURDIR) modules"). Every
+# directory opts its object into dataflow instrumentation with
+# KCOV_DATAFLOW_<object>.o := y, the same per-file switch in-tree code uses.
+obj-m += rust_ffi_contract/
+obj-m += eight_struct_args_c/
+obj-$(CONFIG_RUST) += eight_struct_args_rust/
+obj-$(CONFIG_RUST) += rust_kworker_remote/
diff --git a/tools/testing/selftests/kcov_dataflow/Makefile b/tools/testing/selftests/kcov_dataflow/Makefile
new file mode 100644
index 0000000000000..fc979e2d4ecc3
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/Makefile
@@ -0,0 +1,46 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# kcov_dataflow selftests
+#
+# user_ioctl and binderfs are ordinary kselftest programs. The test modules
+# (one per directory, listed in Kbuild) are built by kbuild against KDIR and
+# are loaded, triggered and checked by test_modules.py; trigger-view.py is
+# the interactive viewer the runner is built on.
+#
+# KDIR is the configured kernel build tree. It defaults to the source tree
+# this directory lives in; point it at the O= directory for out-of-tree
+# builds. Pass the same LLVM=1 CC=clang [RUSTC= RUST_LIB_SRC=] the kernel was
+# built with so that kbuild picks the toolchain that has the trace-args and
+# trace-ret passes.
+KDIR ?= $(abspath ../../../..)
+
+TEST_GEN_PROGS := user_ioctl/user_ioctl binderfs/binderfs_test
+TEST_PROGS := test_modules.py
+TEST_FILES := trigger-view.py
+
+CFLAGS += -Wall -O2 $(KHDR_INCLUDES)
+
+# The .ko files kbuild produces for KDIR's configuration, so that they are
+# built by "all" and copied by "install"; the Rust modules need CONFIG_RUST.
+KMODS := rust_ffi_contract eight_struct_args_c
+ifneq ($(shell grep -s ^CONFIG_RUST=y $(KDIR)/.config),)
+KMODS += eight_struct_args_rust rust_kworker_remote
+endif
+TEST_GEN_FILES := $(foreach m,$(KMODS),$(m)/$(m).ko)
+
+include ../lib.mk
+
+ifneq ($(wildcard $(KDIR)/.config),)
+$(TEST_GEN_FILES): modules
+modules:
+ $(Q)$(MAKE) -C $(KDIR) M=$(CURDIR) modules
+clean_modules:
+ $(Q)$(MAKE) -C $(KDIR) M=$(CURDIR) clean
+else
+$(TEST_GEN_FILES):
+ @echo "SKIP $(notdir $@): no configured kernel tree at $(KDIR), set KDIR="
+clean_modules:
+endif
+
+clean: clean_modules
+.PHONY: modules clean_modules
diff --git a/tools/testing/selftests/kcov_dataflow/README.rst b/tools/testing/selftests/kcov_dataflow/README.rst
new file mode 100644
index 0000000000000..1929a357aca47
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/README.rst
@@ -0,0 +1,69 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+KCOV-Dataflow Selftests
+=======================
+
+Selftests for ``/sys/kernel/debug/kcov_dataflow`` (see
+Documentation/dev-tools/kcov-dataflow.rst).
+
+Layout
+------
+
+Makefile, Kbuild
+ kselftest build: the C programs are built by lib.mk, the test modules
+ (one directory each, listed in Kbuild) by kbuild against ``KDIR``.
+user_ioctl/
+ ioctl interface test (kselftest harness, TAP).
+binderfs/
+ binder ioctls under recording (TAP).
+rust_ffi_contract/, eight_struct_args_c/, eight_struct_args_rust/,
+rust_kworker_remote/
+ test modules; each README.rst says what the module exercises.
+test_modules.py
+ KTAP runner: loads every module, triggers it with recording active and
+ checks the captured arguments, struct fields and return values against
+ the values the module uses. Modules that are not built are SKIPped.
+trigger-view.py
+ Interactive viewer the runner is built on (call tree or ``--raw``
+ records, kallsyms/addr2line symbolization, ``--remote`` capture).
+
+Kernel
+------
+
+The kernel and the modules must be built with a clang that has the
+trace-args/trace-ret passes (and, for the Rust modules, a rustc built
+against that LLVM). The config fragment ``config`` lists what the tests
+need; with virtme-ng::
+
+ vng --build --config tools/testing/selftests/kcov_dataflow/config \
+ LLVM=1 CC=clang RUSTC=$RUSTC RUST_LIB_SRC=$RUST_LIB_SRC
+
+Build
+-----
+
+From the kernel tree, with the same toolchain variables::
+
+ make LLVM=1 headers
+ make -C tools/testing/selftests TARGETS=kcov_dataflow \
+ LLVM=1 CC=clang RUSTC=$RUSTC RUST_LIB_SRC=$RUST_LIB_SRC
+
+``KDIR`` defaults to the source tree; pass ``KDIR=<O dir>`` for out-of-tree
+builds. The Rust modules are built only when ``KDIR/.config`` has
+``CONFIG_RUST=y``. ``make ... install INSTALL_PATH=<dir>`` produces a
+self-contained tree with ``run_kselftest.sh``.
+
+Run
+---
+
+On the target (root, debugfs mounted)::
+
+ vng --user root --exec \
+ "tools/testing/selftests/kcov_dataflow/test_modules.py"
+ tools/testing/selftests/kcov_dataflow/user_ioctl/user_ioctl
+ tools/testing/selftests/kcov_dataflow/binderfs/binderfs_test
+
+or, from an installed tree, ``run_kselftest.sh -c kcov_dataflow``.
+``test_modules.py -t <module> -C 8`` runs one module and echoes eight
+records of context around each module record; ``trigger-view.py <module>
+[--raw] [-C N] [--remote] [--vmlinux vmlinux]`` shows the capture
+without checking it.
diff --git a/tools/testing/selftests/kcov_dataflow/config b/tools/testing/selftests/kcov_dataflow/config
new file mode 100644
index 0000000000000..7f3a2fda0641d
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/config
@@ -0,0 +1,11 @@
+CONFIG_KCOV=y
+CONFIG_KCOV_DATAFLOW_ARGS=y
+CONFIG_KCOV_DATAFLOW_RET=y
+CONFIG_KCOV_DATAFLOW_INSTRUMENT_ALL=y
+CONFIG_KCOV_DATAFLOW_NO_INLINE=y
+CONFIG_DEBUG_INFO_DWARF5=y
+CONFIG_DEBUG_FS=y
+CONFIG_MODULES=y
+CONFIG_ANDROID_BINDER_IPC=y
+CONFIG_ANDROID_BINDERFS=y
+CONFIG_RUST=y
diff --git a/tools/testing/selftests/kcov_dataflow/settings b/tools/testing/selftests/kcov_dataflow/settings
new file mode 100644
index 0000000000000..694d70710ff08
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/settings
@@ -0,0 +1 @@
+timeout=300
diff --git a/tools/testing/selftests/kcov_dataflow/test_modules.py b/tools/testing/selftests/kcov_dataflow/test_modules.py
new file mode 100755
index 0000000000000..13cb706a06ff6
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/test_modules.py
@@ -0,0 +1,249 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+test_modules.py - run the kcov_dataflow test modules, one KTAP test each.
+
+Every module is loaded, triggered with recording active and unloaded by
+trigger-view.py's run_capture(). The records that belong to the module are
+then compared with the values its trigger function passes and returns, so a
+test passes only when the instrumented arguments, struct field expansions
+and return values came back intact through the kcov_dataflow buffer. The
+module's call tree is echoed as KTAP diagnostics.
+
+ ./test_modules.py # all modules
+ ./test_modules.py -t rust_ffi_contract -C 8 --vmlinux vmlinux
+
+Modules that were not built (no CONFIG_RUST, no toolchain) are reported as
+SKIP; a kernel without /sys/kernel/debug/kcov_dataflow skips everything.
+"""
+import argparse
+import contextlib
+import importlib.util
+import io
+import os
+import sys
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, os.path.join(HERE, "..", "kselftest"))
+import ksft # noqa: E402
+
+
+def _load_trigger_view():
+ spec = importlib.util.spec_from_file_location(
+ "trigger_view", os.path.join(HERE, "trigger-view.py"))
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ return mod
+
+
+tv = _load_trigger_view()
+
+
+class Check:
+ """Collects expectation failures for one module."""
+
+ def __init__(self):
+ self.failures = []
+
+ def eq(self, what, got, want):
+ if got != want:
+ self.failures.append(f"{what}: got {fmt(got)}, want {fmt(want)}")
+
+ def true(self, what, cond):
+ if not cond:
+ self.failures.append(what)
+
+
+def fmt(v):
+ if isinstance(v, list):
+ return "[" + ", ".join(fmt(x) for x in v) + "]"
+ if isinstance(v, int):
+ return f"0x{v:x}"
+ return str(v)
+
+
+def entries(cap, recs, func):
+ return [r for r in recs if r["type"] == tv.DF_TYPE_ENTRY and func in cap.funcs(r)]
+
+
+def rets(cap, recs, func):
+ return [r["val"] for r in recs if r["type"] == tv.DF_TYPE_RET and func in cap.funcs(r)]
+
+
+def flat_sum(n):
+ """sf_n() returns a->a + b->b + ... over s1..sn: 0x11 + 0x22 + ..."""
+ return sum(0x11 * k for k in range(1, n + 1))
+
+
+def nested_sum(n, _memo={}):
+ """
+ stf_n()/stpf_n() return field0 (0x11 * n) plus the recursive sums of the
+ embedded st1..st(n-1); the same values are used for the value-nested and
+ the pointer-linked towers.
+ """
+ if n not in _memo:
+ _memo[n] = 0x11 * n + sum(nested_sum(k) for k in range(1, n))
+ return _memo[n]
+
+
+def check_struct_family(cap, recs, c, p, flat_ns, stpf8_runs):
+ """
+ Shared expectations for eight_struct_args_c (p="") and
+ eight_struct_args_rust (p="r"): @flat_ns are the sf_N called by the
+ trigger, @stpf8_runs how often the pointer-linked tower is walked.
+ """
+ for n in flat_ns:
+ ents = entries(cap, recs, f"{p}sf_{n}")
+ c.true(f"{p}sf_{n}: ENTRY records", bool(ents))
+ for k in range(n):
+ # arg k is a struct s(k+1) * whose fields are 0x11, 0x22, ...
+ got = [r["vals"] for r in ents if r["arg_idx"] == k]
+ c.true(f"{p}sf_{n} arg[{k}]: ENTRY record", bool(got))
+ for vals in got:
+ c.eq(f"{p}sf_{n} arg[{k}] expanded fields", vals,
+ [0x11 * (j + 1) for j in range(k + 1)])
+ # rustc may alias identical bodies (rsf_1 == rstf_1), so the RET
+ # list can carry the alias's calls too: check every value.
+ got = rets(cap, recs, f"{p}sf_{n}")
+ c.true(f"{p}sf_{n} RET values all {fmt(flat_sum(n))}: {fmt(got)}",
+ bool(got) and all(v == flat_sum(n) for v in got))
+
+ for fam, calls in ((f"{p}stf", 1), (f"{p}stpf", stpf8_runs)):
+ for n in range(1, 9):
+ got = rets(cap, recs, f"{fam}_{n}")
+ c.true(f"{fam}_{n}: RET records", bool(got))
+ c.true(f"{fam}_{n} RET values all {fmt(nested_sum(n))}: {fmt(got)}",
+ all(v == nested_sum(n) for v in got))
+ c.eq(f"{fam}_8 RET count", len(rets(cap, recs, f"{fam}_8")), calls)
+
+ for f in (f"{p}sf_fwd", f"{p}sf_fwd_inner"):
+ c.eq(f"{f} RET", rets(cap, recs, f), [flat_sum(4)])
+
+ c.true(f"{p}sf_ret_struct: ENTRY records",
+ bool(entries(cap, recs, f"{p}sf_ret_struct")))
+ c.true(f"{p}sf_ret_struct: RET record",
+ bool(rets(cap, recs, f"{p}sf_ret_struct")))
+
+
+def check_eight_struct_args_c(cap, recs, c):
+ check_struct_family(cap, recs, c, "", range(1, 9), stpf8_runs=2)
+
+
+def check_eight_struct_args_rust(cap, recs, c):
+ check_struct_family(cap, recs, c, "r", (1, 2, 4, 8), stpf8_runs=1)
+
+
+def check_rust_ffi_contract(cap, recs, c):
+ """
+ ffi_alloc_buf(&alloc = {NULL, 0, 0, 0}, 256, 16, is_async=1) records
+ data_size + offsets_size and returns 0 without filling alloc->buffer;
+ ffi_check_result() then sees {NULL, 0x110, 0, 0}. The records must show
+ the violated contract at both boundaries.
+ """
+ ents = entries(cap, recs, "ffi_alloc_buf")
+ by_arg = {r["arg_idx"]: r for r in ents}
+ c.eq("ffi_alloc_buf ENTRY arg indexes", sorted(by_arg), [0, 1, 2, 3])
+ if 0 in by_arg:
+ c.eq("ffi_alloc_buf arg[0] struct ffi_alloc fields",
+ by_arg[0]["vals"], [0, 0, 0, 0])
+ if 1 in by_arg:
+ c.eq("ffi_alloc_buf arg[1] data_size", by_arg[1]["val"], 256)
+ if 2 in by_arg:
+ c.eq("ffi_alloc_buf arg[2] offsets_size", by_arg[2]["val"], 16)
+ if 3 in by_arg:
+ c.eq("ffi_alloc_buf arg[3] is_async", by_arg[3]["val"], 1)
+ c.eq("ffi_alloc_buf RET (claims success)", rets(cap, recs, "ffi_alloc_buf"), [0])
+
+ ents = entries(cap, recs, "ffi_check_result")
+ c.true("ffi_check_result: ENTRY record", bool(ents))
+ for r in ents:
+ c.eq("ffi_check_result arg[0] {buffer NULL: contract violated, "
+ "data_size, free_async, flags}", r["vals"], [0, 0x110, 0, 0])
+ got = rets(cap, recs, "ffi_check_result")
+ c.true(f"ffi_check_result RET -EFAULT: {fmt(got)}",
+ len(got) == 1 and got[0] & 0xffffffff == 0xfffffff2)
+
+
+def check_rust_kworker_remote(cap, recs, c):
+ """
+ The trigger only queues a work item and waits; the records come from the
+ kworker that called kcov_df_remote_start(REMOTE_HANDLE). All three phases
+ of CompositeStore must show up (v0-mangled names keep the method names).
+ """
+ c.true("records captured from the kworker", bool(recs))
+ names = set().union(*(cap.funcs(r) for r in recs)) if recs else set()
+ for phase in ("populate", "update", "drain"):
+ c.true(f"CompositeStore::{phase} recorded",
+ any("CompositeStore" in n and phase in n for n in names))
+
+
+TESTS = (
+ ("rust_ffi_contract", False, check_rust_ffi_contract),
+ ("eight_struct_args_c", False, check_eight_struct_args_c),
+ ("eight_struct_args_rust", False, check_eight_struct_args_rust),
+ ("rust_kworker_remote", True, check_rust_kworker_remote),
+)
+
+
+def diag_tree(cap, recs, vmlinux):
+ out = io.StringIO()
+ with contextlib.redirect_stdout(out):
+ tv.print_tree(recs, cap.syms, vmlinux, {}, cap.ko_path,
+ cap.mod_text_start)
+ for line in out.getvalue().splitlines():
+ ksft.print_msg(line)
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
+ parser.add_argument("-t", "--test", action="append",
+ help="run only this module (repeatable)")
+ parser.add_argument("-C", "--context", type=int, default=0,
+ help="echo N records before/after each module record")
+ parser.add_argument("--vmlinux", help="vmlinux for addr2line and KASLR")
+ args = parser.parse_args()
+
+ tests = [t for t in TESTS if not args.test or t[0] in args.test]
+ ksft.print_header()
+ ksft.set_plan(len(tests))
+
+ skip_all = None
+ if not os.path.exists(tv.KCOV_DF_PATH):
+ skip_all = f"{tv.KCOV_DF_PATH} not available (CONFIG_KCOV_DATAFLOW_ARGS/RET)"
+ elif os.geteuid() != 0:
+ skip_all = "must run as root"
+
+ vmlinux = tv.find_vmlinux(args.vmlinux)
+ for name, remote, check in tests:
+ if skip_all:
+ ksft.test_result_skip(f"{name}: {skip_all}")
+ continue
+ ko = tv.find_module(name)
+ if not ko:
+ ksft.test_result_skip(f"{name}: {name}.ko not built")
+ continue
+ try:
+ cap = tv.run_capture(ko, remote=remote, vmlinux=vmlinux,
+ log=ksft.print_msg)
+ except OSError as e:
+ ksft.test_result_fail(f"{name}: {e}")
+ continue
+
+ recs = cap.module_records()
+ ksft.print_msg(f"{name}: {cap.total_words} words, {len(cap.records)} "
+ f"records, {len(recs)} from {name} "
+ f"(kaslr_offset=0x{cap.kaslr_offset:x})")
+ diag_tree(cap, cap.context_records(args.context) if args.context
+ else recs, vmlinux)
+
+ c = Check()
+ check(cap, recs, c)
+ for f in c.failures:
+ ksft.print_msg(f"FAIL {name}: {f}")
+ ksft.test_result(not c.failures, name)
+
+ ksft.finished()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/testing/selftests/kcov_dataflow/trigger-view.py b/tools/testing/selftests/kcov_dataflow/trigger-view.py
new file mode 100755
index 0000000000000..b17e49da402d7
--- /dev/null
+++ b/tools/testing/selftests/kcov_dataflow/trigger-view.py
@@ -0,0 +1,755 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+trigger-view.py - Load a test module, trigger it with kcov_dataflow
+recording active, then pretty-print the captured records.
+
+Usage:
+ python3 trigger-view.py eight_struct_args_c
+ python3 trigger-view.py rust_ffi_contract --raw -C 8
+ python3 trigger-view.py rust_kworker_remote --remote
+ python3 trigger-view.py <module> --vmlinux vmlinux --kaslr-offset 0x...
+
+run_capture() does the work and is also what test_modules.py drives:
+ 1. Opens /sys/kernel/debug/kcov_dataflow, inits and mmaps the buffer
+ 2. Loads the module via finit_module() (its init noise is not recorded)
+ 3. Enables recording: KCOV_DF_ENABLE for this task, or with --remote
+ KCOV_DF_REMOTE_ENABLE with handle REMOTE_HANDLE, which the module's
+ kworker opens with kcov_df_remote_start(REMOTE_HANDLE)
+ 4. Writes the trigger file(s) the module created under TRIGGER_DIR
+ 5. Disables recording and unloads the module
+ 6. Parses the records (layout: include/uapi/linux/kcov_dataflow.h)
+
+The CLI then prints them as a call tree, or flat with --raw, with kallsyms
+symbol resolution and addr2line source lines (vmlinux / module .ko).
+
+Recorded PCs have the KASLR offset removed (same as mainline kcov), so
+the runtime offset is derived from /proc/kallsyms and System.map / vmlinux
+(or a per-architecture default) and added back for symbolization; use
+--kaslr-offset to override. Records must contain at least one value word
+and one of the three record types, otherwise the parser resyncs word by
+word (e.g. after a userspace reset of area[0] mid-run).
+"""
+import os
+import sys
+import struct
+import ctypes
+import ctypes.util
+import argparse
+import fcntl
+import platform
+import subprocess
+import shutil
+
+# Constants -- must match include/uapi/linux/kcov_dataflow.h
+DF_TYPE_CMP = 0xC
+DF_TYPE_ENTRY = 0xE
+DF_TYPE_RET = 0xF
+MAGIC_BAD = 0xBADADD85
+BUF_SIZE = 1048576 # 1M words = 8MB
+
+# Record header word: bits 0-23 seq | 28-31 type | 32-47 nvals |
+# 48-55 arg/ret size | 56-63 arg index. Word 1 is the pc (KASLR offset
+# removed, like mainline kcov), word 2 the traced pointer (ENTRY/RET) or the
+# comparison type (CMP), then nvals value words.
+def hdr_seq(h):
+ return h & 0x00FFFFFF
+
+def hdr_type(h):
+ return (h >> 28) & 0xF
+
+def hdr_nvals(h):
+ return (h >> 32) & 0xFFFF
+
+def hdr_size(h):
+ return (h >> 48) & 0xFF
+
+def hdr_arg_idx(h):
+ return (h >> 56) & 0xFF
+
+RECORD_HDR_WORDS = 3
+
+# Runtime KASLR offset (see kaslr_offset()); added back to every recorded pc
+# so /proc/kallsyms lookups work, subtracted again for addr2line on vmlinux.
+KASLR_OFFSET = 0
+
+# Ioctl numbers
+def _IOR(t, nr, size):
+ return (2 << 30) | (ord(t) << 8) | nr | (size << 16)
+
+def _IOW(t, nr, size):
+ return (1 << 30) | (ord(t) << 8) | nr | (size << 16)
+
+def _IO(t, nr):
+ return (ord(t) << 8) | nr
+
+KCOV_DF_INIT_TRACK = _IOR('d', 1, 8)
+KCOV_DF_ENABLE = _IO('d', 100)
+KCOV_DF_DISABLE = _IO('d', 101)
+KCOV_DF_REMOTE_ENABLE = _IOW('d', 102, 8) # arg: pointer to a __u64 handle
+KCOV_DF_REMOTE_DISABLE = _IO('d', 103)
+
+KCOV_DF_PATH = "/sys/kernel/debug/kcov_dataflow"
+
+# Every test module creates its trigger file(s) in this debugfs directory;
+# writing to them runs the instrumented test functions.
+TRIGGER_DIR = "/sys/kernel/debug/kcov_dataflow_test"
+
+# Remote handle registered with KCOV_DF_REMOTE_ENABLE; must match the
+# kcov_df_remote_start(1) call in the rust_kworker_remote test module
+# (KCOV_SUBSYSTEM_COMMON, instance 1).
+REMOTE_HANDLE = 1
+
+# syscall numbers
+_machine = platform.machine()
+if _machine == "aarch64":
+ SYS_FINIT_MODULE = 273
+ SYS_DELETE_MODULE = 106
+else: # x86_64
+ SYS_FINIT_MODULE = 313
+ SYS_DELETE_MODULE = 176
+
+SELFTEST_DIR = os.path.dirname(os.path.abspath(__file__))
+
+
+def load_kallsyms():
+ """Load kernel symbols for PC resolution."""
+ syms = []
+ try:
+ with open("/proc/kallsyms") as f:
+ for line in f:
+ parts = line.split()
+ if len(parts) >= 3:
+ addr = int(parts[0], 16)
+ name = parts[2]
+ mod = parts[3].strip("[]") if len(parts) > 3 else ""
+ syms.append((addr, name, mod))
+ except (PermissionError, FileNotFoundError):
+ pass
+ syms.sort()
+ return syms
+
+
+def runtime_text(syms):
+ """Runtime address of _text from kallsyms, 0 if hidden."""
+ return next((a for a, n, m in syms if n == "_text" and not m), 0)
+
+
+# Link-time address of _text per architecture, used only when neither
+# System.map nor vmlinux is available: x86_64 __START_KERNEL
+# (__START_KERNEL_map + CONFIG_PHYSICAL_START), arm64 KIMAGE_VADDR.
+LINKTIME_TEXT_DEFAULT = {
+ "x86_64": 0xffffffff81000000,
+ "aarch64": 0xffff800080000000,
+}
+
+
+def linktime_text(vmlinux=None):
+ """Return (link-time address of _text, source description) or (0, "")."""
+ rel = os.uname().release
+ candidates = []
+ if vmlinux:
+ candidates.append(os.path.join(os.path.dirname(vmlinux) or ".", "System.map"))
+ candidates += ["System.map", f"/boot/System.map-{rel}",
+ f"/usr/lib/debug/boot/System.map-{rel}"]
+ for sm in candidates:
+ try:
+ with open(sm) as f:
+ for line in f:
+ parts = line.split()
+ if len(parts) == 3 and parts[2] == "_text":
+ return int(parts[0], 16), sm
+ except (OSError, ValueError):
+ continue
+ if vmlinux and shutil.which("nm"):
+ try:
+ r = subprocess.run(["nm", "--defined-only", vmlinux],
+ capture_output=True, text=True, timeout=300)
+ for line in r.stdout.splitlines():
+ parts = line.split()
+ if len(parts) == 3 and parts[2] == "_text":
+ return int(parts[0], 16), f"nm {vmlinux}"
+ except (OSError, subprocess.TimeoutExpired):
+ pass
+ link = LINKTIME_TEXT_DEFAULT.get(platform.machine(), 0)
+ return link, f"{platform.machine()} default" if link else ""
+
+
+def kaslr_offset(syms, vmlinux=None):
+ """
+ Runtime KASLR offset: recorded PCs have it removed (kcov's
+ canonicalize_ip()), /proc/kallsyms has it applied. Computed as the
+ runtime _text (kallsyms) minus the link-time _text (System.map, nm
+ vmlinux, or the architecture default). KASLR offsets are 2 MiB aligned
+ on x86_64 and arm64, which is used as a sanity check on the result.
+ """
+ runtime = runtime_text(syms)
+ if not runtime:
+ print("# warning: _text not in /proc/kallsyms (kptr_restrict?); "
+ "PCs will not symbolize", file=sys.stderr)
+ return 0
+ link, source = linktime_text(vmlinux)
+ if not link:
+ print(f"# warning: no System.map/vmlinux and no default _text for "
+ f"{platform.machine()}; pass --kaslr-offset", file=sys.stderr)
+ return 0
+ off = runtime - link
+ if off % (2 << 20):
+ print(f"# warning: kaslr offset 0x{off:x} from {source} is not 2 MiB "
+ f"aligned; check CONFIG_PHYSICAL_START/KIMAGE_VADDR or pass "
+ f"--kaslr-offset", file=sys.stderr)
+ return off
+
+
+# Rust symbol demangling via llvm-cxxfilt or rustfilt
+_demangler = None
+
+def _init_demangler():
+ global _demangler
+ for tool in ["llvm-cxxfilt", "rustfilt", "c++filt"]:
+ path = shutil.which(tool)
+ if path:
+ _demangler = path
+ return
+ _demangler = ""
+
+_demangled = {}
+
+def demangle(name):
+ """Demangle a Rust/C++ symbol name (memoized: one process per name)."""
+ global _demangler
+ if _demangler is None:
+ _init_demangler()
+ if not _demangler or not name.startswith("_R"):
+ return name
+ if name not in _demangled:
+ try:
+ r = subprocess.run([_demangler, name], capture_output=True,
+ text=True, timeout=2)
+ _demangled[name] = r.stdout.strip() if r.returncode == 0 else name
+ except (OSError, subprocess.TimeoutExpired):
+ _demangled[name] = name
+ return _demangled[name]
+
+
+def find_vmlinux(vmlinux=None):
+ """Locate vmlinux for addr2line: explicit path, else the usual places."""
+ if vmlinux:
+ return vmlinux
+ for p in ["vmlinux", "/boot/vmlinux", "/usr/lib/debug/boot/vmlinux"]:
+ if os.path.exists(p):
+ return p
+ return None
+
+
+def _a2l_target(pc, vmlinux, ko_path, mod_text_base):
+ """(binary, address in it) to symbolize pc with, or None."""
+ if ko_path and mod_text_base and pc >= mod_text_base:
+ return ko_path, pc - mod_text_base
+ if vmlinux:
+ return vmlinux, pc - KASLR_OFFSET # vmlinux holds link-time addresses
+ return None
+
+
+def resolve_lines(pcs, vmlinux, cache, ko_path=None, mod_text_base=0):
+ """
+ Resolve every pc in @pcs to file:line into @cache, one addr2line run
+ per binary: a DWARF5 vmlinux takes hundreds of ms to open, so one
+ process per record does not scale to thousands of records.
+ """
+ todo = {}
+ for pc in pcs:
+ if pc in cache:
+ continue
+ cache[pc] = ""
+ tgt = _a2l_target(pc, vmlinux, ko_path, mod_text_base)
+ if tgt:
+ todo.setdefault(tgt[0], []).append((pc, tgt[1]))
+ for binary, pairs in todo.items():
+ try:
+ r = subprocess.run(
+ ["addr2line", "-e", binary] + [f"0x{a:x}" for _, a in pairs],
+ capture_output=True, text=True, timeout=300)
+ except (subprocess.TimeoutExpired, FileNotFoundError):
+ continue
+ for (pc, _), loc in zip(pairs, r.stdout.splitlines()):
+ loc = loc.strip()
+ if loc and loc != "??:0" and loc != "??:?":
+ # Shorten path: keep only filename:line
+ cache[pc] = loc.rsplit("/", 1)[-1]
+
+
+def resolve_line(pc, vmlinux, cache, ko_path=None, mod_text_base=0):
+ """Resolve one PC to source file:line using addr2line (cached)."""
+ if pc not in cache:
+ resolve_lines([pc], vmlinux, cache, ko_path, mod_text_base)
+ return cache[pc]
+
+
+def get_kernel_meta():
+ """Collect kernel build metadata."""
+ meta = {"release": os.uname().release}
+ try:
+ with open("/proc/version") as f:
+ v = f.read().strip()
+ meta["version"] = v
+ # Extract compiler version
+ if "gcc" in v.lower():
+ meta["compiler"] = v.split("(")[1].split(")")[0] if "(" in v else ""
+ elif "clang" in v.lower():
+ idx = v.lower().find("clang")
+ meta["compiler"] = v[idx:idx+30].split(")")[0]
+ except OSError:
+ pass
+ return meta
+
+
+def print_kernel_meta(meta, ko_path=None):
+ """Print kernel metadata header/footer."""
+ print(f"# {'=' * 60}")
+ print(f"# Kernel: {meta.get('release', 'unknown')}")
+ print(f"# Build: {meta.get('version', 'unknown')[:80]}")
+ if meta.get('compiler'):
+ print(f"# Compiler: {meta['compiler']}")
+ # Read rustc version from .ko .comment section
+ if ko_path:
+ try:
+ r = subprocess.run(
+ ["readelf", "-p", ".comment", ko_path],
+ capture_output=True, text=True, timeout=5)
+ for line in r.stdout.splitlines():
+ if "rustc" in line:
+ ver = line.split("]", 1)[-1].strip()
+ print(f"# Rustc: {ver}")
+ break
+ except (OSError, subprocess.TimeoutExpired):
+ pass
+ print(f"# {'=' * 60}")
+
+
+def lookup(pc, syms):
+ """Nearest kallsyms entry <= pc as (name, offset, module) or None."""
+ if not syms:
+ return None
+ lo, hi = 0, len(syms) - 1
+ while lo < hi:
+ mid = (lo + hi + 1) // 2
+ if syms[mid][0] <= pc:
+ lo = mid
+ else:
+ hi = mid - 1
+ addr, name, mod = syms[lo]
+ if addr > pc:
+ return None
+ return name, pc - addr, mod
+
+
+def symbolize(pc, syms):
+ """Find nearest symbol <= pc. Returns (display_name, module_tag)."""
+ hit = lookup(pc, syms)
+ if not hit:
+ return f"0x{pc:x}", ""
+ name, offset, mod = hit
+ dname = demangle(name)
+ display = f"{dname}+0x{offset:x}" if offset else dname
+ return display, f" [{mod}]" if mod else ""
+
+
+def format_val(v):
+ """Format a captured value."""
+ if v == MAGIC_BAD:
+ return "FAULT"
+ if v == 0:
+ return "0x0"
+ return f"0x{v:x}"
+
+
+def find_module(name):
+ """
+ Find the .ko for test @name: <name>/<name>.ko in the source tree, or
+ <name>.ko next to this script in an installed (make install) tree.
+ """
+ for ko_path in (os.path.join(SELFTEST_DIR, name, f"{name}.ko"),
+ os.path.join(SELFTEST_DIR, f"{name}.ko")):
+ if os.path.exists(ko_path):
+ return ko_path
+ return None
+
+
+def finit_module(ko_path):
+ """Load a kernel module via finit_module syscall."""
+ libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
+ fd = os.open(ko_path, os.O_RDONLY)
+ ret = libc.syscall(SYS_FINIT_MODULE, fd, b"", 0)
+ os.close(fd)
+ if ret != 0:
+ errno = ctypes.get_errno()
+ raise OSError(errno, f"finit_module({ko_path}): {os.strerror(errno)}")
+
+
+def delete_module(name):
+ """Unload a kernel module."""
+ libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
+ ret = libc.syscall(SYS_DELETE_MODULE, name.encode(), 0)
+ if ret != 0:
+ errno = ctypes.get_errno()
+ raise OSError(errno, f"delete_module({name}): {os.strerror(errno)}")
+
+
+def trigger_module():
+ """
+ Write to every trigger file the loaded module created under TRIGGER_DIR.
+ Opened without O_CREAT: debugfs directories have no ->create, so a
+ "w"-mode open of a missing name fails with EOPNOTSUPP, not ENOENT.
+ """
+ try:
+ names = sorted(os.listdir(TRIGGER_DIR))
+ except OSError:
+ names = []
+ hits = []
+ for n in names:
+ path = os.path.join(TRIGGER_DIR, n)
+ try:
+ fd = os.open(path, os.O_WRONLY)
+ except OSError:
+ continue
+ try:
+ os.write(fd, b"1")
+ finally:
+ os.close(fd)
+ hits.append(path)
+ if not hits:
+ raise FileNotFoundError(f"no trigger file under {TRIGGER_DIR}")
+ return hits
+
+
+def parse_records(buf, total_words):
+ """Parse the ring buffer into a list of records."""
+ records = []
+ pos = 1
+ end = min(1 + total_words, BUF_SIZE)
+ while pos + RECORD_HDR_WORDS <= end:
+ hdr = buf[pos]
+ rtype = hdr_type(hdr)
+ num_vals = hdr_nvals(hdr)
+
+ # Every record the kernel writes has nvals >= 1 and a known type;
+ # anything else is garbage (e.g. a userspace reset mid-run): resync.
+ if rtype not in (DF_TYPE_ENTRY, DF_TYPE_RET, DF_TYPE_CMP) \
+ or num_vals == 0 or pos + RECORD_HDR_WORDS + num_vals > end:
+ pos += 1
+ continue
+
+ pc = int(buf[pos + 1]) + KASLR_OFFSET
+ ptr = int(buf[pos + 2]) # ENTRY/RET: traced pointer; CMP: cmp type
+ if rtype == DF_TYPE_CMP:
+ pos += RECORD_HDR_WORDS + num_vals
+ continue
+
+ # Valid records always have a non-zero PC (kernel text address)
+ if pc == 0:
+ pos += 1
+ continue
+
+ vals = [int(buf[pos + RECORD_HDR_WORDS + vi]) for vi in range(num_vals)]
+ records.append({
+ "type": rtype,
+ "seq": hdr_seq(hdr),
+ "pc": pc,
+ "ptr": ptr,
+ "arg_idx": hdr_arg_idx(hdr),
+ "size": hdr_size(hdr),
+ "val": vals[0],
+ "vals": vals,
+ })
+ pos += RECORD_HDR_WORDS + num_vals
+ return records
+
+
+class Capture:
+ """Everything run_capture() collected for one module run."""
+
+ def __init__(self, ko_path, mod_name, records, syms, total_words,
+ mod_text_start, kaslr_off):
+ self.ko_path = ko_path
+ self.mod_name = mod_name
+ self.records = records
+ self.syms = syms
+ self.total_words = total_words
+ self.mod_text_start = mod_text_start
+ self.kaslr_offset = kaslr_off
+ self.runtime_text = runtime_text(syms)
+ self._mod_syms = any(m == mod_name for _, _, m in syms)
+ # Aliases: rustc's merge-functions makes identical bodies (e.g. the
+ # one-field rsf_1 and rstf_1) share one address, so a PC can carry
+ # several names.
+ self._names = {}
+ for addr, name, mod in syms:
+ self._names.setdefault((addr, mod), set()).add(name)
+
+ def is_module_pc(self, pc):
+ """True if pc lies in the test module (kallsyms, else .text start)."""
+ if self._mod_syms:
+ hit = lookup(pc, self.syms)
+ return bool(hit) and hit[2] == self.mod_name
+ # Fallback: if no module symbols (kptr_restrict), use .text start
+ return bool(self.mod_text_start) and pc >= self.mod_text_start
+
+ def funcs(self, rec):
+ """All raw kallsyms names of the function a record belongs to."""
+ hit = lookup(rec["pc"], self.syms)
+ if not hit:
+ return set()
+ name, offset, mod = hit
+ return self._names.get((rec["pc"] - offset, mod), {name})
+
+ def module_records(self):
+ return [r for r in self.records if self.is_module_pc(r["pc"])]
+
+ def context_records(self, n):
+ """Module records plus n records before/after each of them."""
+ keep = set()
+ for i, r in enumerate(self.records):
+ if self.is_module_pc(r["pc"]):
+ keep.update(range(max(0, i - n),
+ min(len(self.records), i + n + 1)))
+ return [self.records[i] for i in sorted(keep)]
+
+
+def run_capture(ko_path, remote=False, vmlinux=None, kaslr_override=None,
+ log=None):
+ """
+ Load @ko_path, record while its trigger file(s) are written, unload it
+ and return a Capture. @remote publishes the buffer for REMOTE_HANDLE
+ instead of enabling recording for this task. Raises OSError.
+ """
+ global KASLR_OFFSET
+ log = log or (lambda msg: print(f"# {msg}"))
+
+ # Ensure kallsyms shows real addresses
+ try:
+ with open("/proc/sys/kernel/kptr_restrict", "w") as f:
+ f.write("0")
+ except OSError:
+ pass
+
+ df_fd = os.open(KCOV_DF_PATH, os.O_RDWR)
+ try:
+ # Init + mmap
+ fcntl.ioctl(df_fd, KCOV_DF_INIT_TRACK, BUF_SIZE)
+ libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
+ libc.mmap.restype = ctypes.c_void_p
+ libc.mmap.argtypes = [
+ ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int,
+ ctypes.c_int, ctypes.c_int, ctypes.c_long
+ ]
+ buf_ptr = libc.mmap(None, BUF_SIZE * 8, 0x3, 0x01, df_fd, 0)
+ if buf_ptr == ctypes.c_void_p(-1).value:
+ errno = ctypes.get_errno()
+ raise OSError(errno, f"mmap: {os.strerror(errno)}")
+ buf = (ctypes.c_uint64 * BUF_SIZE).from_address(buf_ptr)
+
+ # Load module first (its init generates noise with INSTRUMENT_ALL)
+ mod_name = os.path.basename(ko_path).replace(".ko", "")
+ finit_module(ko_path)
+ log(f"Loaded {mod_name}")
+ try:
+ # Module .text address, the PC filter fallback without kallsyms
+ mod_text_start = 0
+ try:
+ with open(f"/sys/module/{mod_name}/sections/.text") as f:
+ mod_text_start = int(f.read().strip(), 16)
+ except (OSError, ValueError):
+ pass
+
+ # Enable recording AFTER load, BEFORE trigger (no loader noise).
+ # Remote: the handle is passed by pointer (a __u64 in a buffer),
+ # so the full 64-bit value survives 32-bit/compat callers.
+ if remote:
+ fcntl.ioctl(df_fd, KCOV_DF_REMOTE_ENABLE,
+ struct.pack("Q", REMOTE_HANDLE))
+ else:
+ fcntl.ioctl(df_fd, KCOV_DF_ENABLE, 0)
+ buf[0] = 0
+ try:
+ for path in trigger_module():
+ log(f"Triggered {path}")
+ finally:
+ fcntl.ioctl(df_fd, KCOV_DF_REMOTE_DISABLE if remote
+ else KCOV_DF_DISABLE, 0)
+
+ # Read kallsyms while the module is still loaded
+ syms = load_kallsyms()
+ finally:
+ try:
+ delete_module(mod_name)
+ except OSError as e:
+ log(f"warning: {e}")
+
+ if kaslr_override is not None:
+ KASLR_OFFSET = kaslr_override
+ else:
+ KASLR_OFFSET = kaslr_offset(syms, find_vmlinux(vmlinux))
+
+ total = int(buf[0])
+ records = parse_records(buf, total)
+ return Capture(ko_path, mod_name, records, syms, total,
+ mod_text_start, KASLR_OFFSET)
+ finally:
+ os.close(df_fd)
+
+
+def print_raw(records, syms, vmlinux=None, cache=None, ko_path=None, mod_text_base=0):
+ """Print records in raw format with source line on left."""
+ if cache is None:
+ cache = {}
+ # Pre-resolve all locations (one addr2line run) to find max width
+ resolve_lines([r["pc"] for r in records], vmlinux, cache, ko_path,
+ mod_text_base)
+ locs = [cache[r["pc"]] for r in records]
+ max_w = max((len(l) for l in locs if l), default=0)
+ max_w = max(max_w, 10) # minimum width
+
+ for i, r in enumerate(records):
+ name, mod = symbolize(r["pc"], syms)
+ sym = f"{name}{mod}"
+ t = "ENTRY" if r["type"] == DF_TYPE_ENTRY else "RET "
+ arg_idx = r["arg_idx"]
+ size = r["size"]
+ left = f"{locs[i]:>{max_w}s}" if locs[i] else f"{'':>{max_w}s}"
+ vals = format_val(r["val"]) if len(r["vals"]) == 1 else \
+ "{" + ", ".join(format_val(v) for v in r["vals"]) + "}"
+ print(f"{left} [{t}] seq={r['seq']:3d} {sym} "
+ f"arg[{arg_idx}]({size}) @0x{r['ptr']:x} = {vals}")
+
+
+def print_tree(records, syms, vmlinux=None, cache=None, ko_path=None, mod_text_base=0):
+ """Print records as indented call tree with source line on left."""
+ if cache is None:
+ cache = {}
+ # Pre-resolve all PCs (one addr2line run) for alignment
+ resolve_lines([r["pc"] for r in records], vmlinux, cache, ko_path,
+ mod_text_base)
+ max_w = max((len(v) for v in cache.values() if v), default=10)
+ max_w = max(max_w, 10)
+
+ depth = 0
+ call_stack = [] # Stack of (name, mod, args_str, pc) for matching returns
+ i = 0
+ while i < len(records):
+ r = records[i]
+ name, mod = symbolize(r["pc"], syms)
+
+ if r["type"] == DF_TYPE_ENTRY:
+ # Collect all args for this call (same PC, consecutive entries);
+ # order by index, as the pass emits dead-arg traces last.
+ args = []
+ pc = r["pc"]
+ while i < len(records) and records[i]["type"] == DF_TYPE_ENTRY \
+ and records[i]["pc"] == pc:
+ vals = records[i]["vals"]
+ if len(vals) > 1:
+ fields = ", ".join(format_val(v) for v in vals)
+ args.append((records[i]["arg_idx"], "{" + fields + "}"))
+ else:
+ args.append((records[i]["arg_idx"],
+ format_val(records[i]["val"])))
+ i += 1
+ args_str = ", ".join(a for _, a in sorted(args, key=lambda x: x[0]))
+ call_stack.append((name, mod, args_str, pc))
+ depth += 1
+ else:
+ # Pop void calls (no return record) until we find matching PC
+ while call_stack and call_stack[-1][3] != r["pc"]:
+ depth = max(0, depth - 1)
+ indent = " " * depth
+ vname, vmod, vargs, vpc = call_stack.pop()
+ loc = resolve_line(vpc, vmlinux, cache, ko_path, mod_text_base)
+ left = f"{loc:>{max_w}s}" if loc else f"{'':>{max_w}s}"
+ print(f"{left} {indent}{vname}({vargs}){vmod}")
+ depth = max(0, depth - 1)
+ indent = " " * depth
+ ret_size = r["size"]
+ loc = resolve_line(r["pc"], vmlinux, cache, ko_path, mod_text_base)
+ left = f"{loc:>{max_w}s}" if loc else f"{'':>{max_w}s}"
+ if call_stack:
+ cname, cmod, cargs, _ = call_stack.pop()
+ if ret_size == 0:
+ print(f"{left} {indent}{cname}({cargs}){cmod}")
+ else:
+ print(f"{left} {indent}{format_val(r['val'])} = {cname}({cargs}){cmod}")
+ else:
+ if ret_size == 0:
+ print(f"{left} {indent}{name}(){mod}")
+ else:
+ print(f"{left} {indent}{format_val(r['val'])} = {name}(){mod}")
+ i += 1
+
+ # Flush remaining void calls on the stack
+ while call_stack:
+ depth = max(0, depth - 1)
+ indent = " " * depth
+ vname, vmod, vargs, vpc = call_stack.pop()
+ loc = resolve_line(vpc, vmlinux, cache, ko_path, mod_text_base)
+ left = f"{loc:>{max_w}s}" if loc else f"{'':>{max_w}s}"
+ print(f"{left} {indent}{vname}({vargs}){vmod}")
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Load a test module with kcov_dataflow and view records")
+ parser.add_argument("module", help="Test module name (e.g. eight_struct_args_c)")
+ parser.add_argument("--raw", action="store_true",
+ help="Print raw records instead of tree")
+ parser.add_argument("--ko", help="Explicit path to .ko file")
+ parser.add_argument("--context", "-C", type=int, default=0,
+ help="Show N records before/after each module record")
+ parser.add_argument("--vmlinux", help="Path to vmlinux for addr2line")
+ parser.add_argument("--remote", action="store_true",
+ help="Use KCOV_DF_REMOTE_ENABLE for kworker capture")
+ parser.add_argument("--kaslr-offset", type=lambda x: int(x, 0),
+ help="Override the runtime KASLR offset added to PCs")
+ args = parser.parse_args()
+
+ ko_path = args.ko or find_module(args.module)
+ if not ko_path or not os.path.exists(ko_path):
+ print(f"Cannot find module for '{args.module}'", file=sys.stderr)
+ print("Build it first: make -C tools/testing/selftests "
+ "TARGETS=kcov_dataflow LLVM=1 CC=clang", file=sys.stderr)
+ sys.exit(1)
+
+ try:
+ cap = run_capture(ko_path, remote=args.remote, vmlinux=args.vmlinux,
+ kaslr_override=args.kaslr_offset)
+ except OSError as e:
+ print(f"{args.module}: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ print(f"# Captured {cap.total_words} words (kaslr_offset=0x{cap.kaslr_offset:x}, "
+ f"_text=0x{cap.runtime_text:x})")
+ print(f"# {len(cap.records)} records")
+
+ if cap.syms or cap.mod_text_start:
+ if args.context > 0:
+ records = cap.context_records(args.context)
+ print(f"# showing {len(records)} records with context={args.context} "
+ f"around {cap.mod_name}\n")
+ else:
+ records = cap.module_records()
+ print(f"# {len(records)} from {cap.mod_name}\n")
+ else:
+ records = cap.records
+ print("")
+
+ meta = get_kernel_meta()
+ print_kernel_meta(meta, ko_path=ko_path)
+
+ vmlinux = find_vmlinux(args.vmlinux)
+ show = print_raw if args.raw else print_tree
+ show(records, cap.syms, vmlinux, {}, ko_path, cap.mod_text_start)
+
+ print_kernel_meta(meta, ko_path=ko_path)
+
+
+if __name__ == "__main__":
+ main()

--
2.47.3

Reply all
Reply to author
Forward
0 new messages