Embed DWARF-derived function parameter name and type information in the
kernel image so that oops and WARN dumps display the crashing function's
register-passed arguments with their names, types, and values.
A new build-time tool (scripts/gen_paraminfo.c) parses DW_TAG_subprogram
and DW_TAG_formal_parameter entries from DWARF .debug_info, extracting
parameter names and human-readable type strings. The resulting tables are
stored in .rodata using the same two-phase link approach as lineinfo.
At runtime, kallsyms_show_paraminfo() performs a binary search on the
paraminfo tables, maps parameters to x86-64 calling convention registers
(RDI, RSI, RDX, RCX, R8, R9), and prints each parameter's name, type,
and value from pt_regs. If a parameter value matches the page fault
address (CR2), it is highlighted with "<-- fault address".
Integration at show_regs() means this works for both oops and WARN()
automatically, since both paths provide full pt_regs at the exception
point.
Example output:
Function parameters (ext4_readdir):
file (struct file *) = 0xffff888123456000
ctx (struct dir_context *) = 0x0000000000001234 <-- fault address
Gated behind CONFIG_KALLSYMS_PARAMINFO (depends on CONFIG_KALLSYMS_LINEINFO).
Adds approximately 1-2 MB to the kernel image for ~58K functions.
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <[email protected]>
---
.../admin-guide/kallsyms-lineinfo.rst | 31 +
arch/x86/kernel/dumpstack.c | 6 +-
include/linux/kallsyms.h | 9 +
init/Kconfig | 21 +
kernel/kallsyms.c | 168 ++++++
kernel/kallsyms_internal.h | 6 +
lib/Kconfig.debug | 11 +
lib/tests/Makefile | 3 +
lib/tests/paraminfo_kunit.c | 256 ++++++++
scripts/Makefile | 3 +
scripts/empty_paraminfo.S | 18 +
scripts/gen_paraminfo.c | 549 ++++++++++++++++++
scripts/link-vmlinux.sh | 44 +-
13 files changed, 1119 insertions(+), 6 deletions(-)
create mode 100644 lib/tests/paraminfo_kunit.c
create mode 100644 scripts/empty_paraminfo.S
create mode 100644 scripts/gen_paraminfo.c
diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst
b/Documentation/admin-guide/kallsyms-lineinfo.rst
index dd264830c8d5b..26921bb3f7f81 100644
--- a/Documentation/admin-guide/kallsyms-lineinfo.rst
+++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
@@ -83,6 +83,37 @@ compression).
Per-module lineinfo adds approximately 2-3 bytes per DWARF line entry to each
``.ko`` file.
+Function Parameter Info
+======================
+
+``CONFIG_KALLSYMS_PARAMINFO`` extends the debugging information by embedding
+function parameter names and types in the kernel image. When an oops or WARN
+occurs, the faulting function's register-passed arguments are displayed with
+their names, types, and values from the saved registers.
+
+Enable in addition to the lineinfo options::
+
+ CONFIG_KALLSYMS_PARAMINFO=y
+
+Example oops output with paraminfo::
+
+ RIP: 0010:ext4_readdir+0x1a3/0x5b0 (fs/ext4/dir.c:421)
+ ...
+ Function parameters (ext4_readdir):
+ file (struct file *) = 0xffff888123456000
+ ctx (struct dir_context *) = 0x0000000000001234 <-- fault address
+
+The ``<-- fault address`` annotation appears when a parameter value matches
+the page fault address (CR2 on x86), helping quickly identify which argument
+caused the crash.
+
+This feature works for both oops and WARN() on x86-64. Only register-passed
+parameters (up to 6 on x86-64: RDI, RSI, RDX, RCX, R8, R9) are displayed.
+The parameter info is only shown for the faulting/warning frame where full
+register state is available.
+
+The paraminfo tables add approximately 1-2 MiB to the kernel image.
+
Known Limitations
=================
diff --git a/arch/x86/kernel/dumpstack.c b/arch/x86/kernel/dumpstack.c
index b10684dedc589..4e9b5fd58fd1b 100644
--- a/arch/x86/kernel/dumpstack.c
+++ b/arch/x86/kernel/dumpstack.c
@@ -483,8 +483,10 @@ void show_regs(struct pt_regs *regs)
__show_regs(regs, print_kernel_regs, KERN_DEFAULT);
/*
- * When in-kernel, we also print out the stack at the time of the
fault..
+ * When in-kernel, show function parameter info and stack trace.
*/
- if (!user_mode(regs))
+ if (!user_mode(regs)) {
+ kallsyms_show_paraminfo(regs);
show_trace_log_lvl(current, regs, NULL, KERN_DEFAULT);
+ }
}
diff --git a/include/linux/kallsyms.h b/include/linux/kallsyms.h
index 7d4c9dca06c87..17c9df520b2b0 100644
--- a/include/linux/kallsyms.h
+++ b/include/linux/kallsyms.h
@@ -104,6 +104,13 @@ int lookup_symbol_name(unsigned long addr, char *symname);
bool kallsyms_lookup_lineinfo(unsigned long addr,
const char **file, unsigned int *line);
+#ifdef CONFIG_KALLSYMS_PARAMINFO
+struct pt_regs;
+void kallsyms_show_paraminfo(struct pt_regs *regs);
+#else
+static inline void kallsyms_show_paraminfo(struct pt_regs *regs) {}
+#endif
+
#else /* !CONFIG_KALLSYMS */
static inline unsigned long kallsyms_lookup_name(const char *name)
@@ -179,6 +186,8 @@ static inline bool kallsyms_lookup_lineinfo(unsigned long
addr,
{
return false;
}
+
+static inline void kallsyms_show_paraminfo(struct pt_regs *regs) {}
#endif /*CONFIG_KALLSYMS*/
static inline void print_ip_sym(const char *loglvl, unsigned long ip)
diff --git a/init/Kconfig b/init/Kconfig
index 6e3795b3dbd62..76d0c2da7d612 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2085,6 +2085,27 @@ config KALLSYMS_LINEINFO_MODULES
If unsure, say N.
+config KALLSYMS_PARAMINFO
+ bool "Show function parameter info in oops/WARN dumps"
+ depends on KALLSYMS_LINEINFO
+ help
+ Embeds function parameter name and type information in the kernel
+ image, extracted from DWARF debug info at build time. When an
+ oops or WARN occurs, the crashing/warning function's register-
+ passed arguments are displayed with their names, types, and
+ values from pt_regs.
+
+ When enabled, oops/WARN dumps include lines like:
+
+ Function parameters (ext4_readdir):
+ file (struct file *) = 0xffff888123456000
+ ctx (struct dir_context *) = 0x0000000000001234
+
+ Requires elfutils (libdw-dev/elfutils-devel) on the build host.
+ Adds approximately 1-2 MB to the kernel image.
+
+ If unsure, say N.
+
# end of the "standard kernel features (expert users)" menu
config ARCH_HAS_MEMBARRIER_CALLBACKS
diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
index e6f796d43dd70..af8de3d8e3ba3 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -501,6 +501,174 @@ bool kallsyms_lookup_lineinfo(unsigned long addr,
return lineinfo_search(&tbl, (unsigned int)raw_offset, file, line);
}
+#ifdef CONFIG_KALLSYMS_PARAMINFO
+
+#include <linux/ptrace.h>
+#ifdef CONFIG_X86
+#include <asm/special_insns.h>
+#endif
+
+#define MAX_PARAMINFO_PARAMS 6
+
+/*
+ * x86-64 calling convention: arguments are passed in registers
+ * RDI, RSI, RDX, RCX, R8, R9 (in that order).
+ */
+#ifdef CONFIG_X86_64
+
+static const char * const paraminfo_reg_names[] = {
+ "RDI", "RSI", "RDX", "RCX", "R8", "R9"
+};
+
+static unsigned long paraminfo_get_reg(const struct pt_regs *regs,
+ unsigned int idx)
+{
+ switch (idx) {
+ case 0: return regs->di;
+ case 1: return regs->si;
+ case 2: return regs->dx;
+ case 3: return regs->cx;
+ case 4: return regs->r8;
+ case 5: return regs->r9;
+ default: return 0;
+ }
+}
+#else
+/* Stub for non-x86-64 architectures */
+static const char * const paraminfo_reg_names[] = {};
+
+static unsigned long paraminfo_get_reg(const struct pt_regs *regs,
+ unsigned int idx)
+{
+ return 0;
+}
+#endif /* CONFIG_X86_64 */
+
+/*
+ * Binary search for the function containing the given offset in
+ * paraminfo_func_addrs[]. Returns the index of the function whose
+ * start address is <= offset, or -1 if not found.
+ */
+static int paraminfo_find_func(unsigned int offset)
+{
+ int lo = 0, hi = paraminfo_num_funcs - 1;
+ int result = -1;
+
+ if (!paraminfo_num_funcs)
+ return -1;
+
+ while (lo <= hi) {
+ int mid = lo + (hi - lo) / 2;
+
+ if (paraminfo_func_addrs[mid] <= offset) {
+ result = mid;
+ lo = mid + 1;
+ } else {
+ hi = mid - 1;
+ }
+ }
+
+ return result;
+}
+
+/*
+ * Show function parameter info for the faulting/warning instruction.
+ *
+ * Called from show_regs() on x86 when CONFIG_KALLSYMS_PARAMINFO is
+ * enabled. Works for both oops (page fault, GPF, etc.) and WARN(),
+ * since both paths provide full pt_regs at the exception point.
+ */
+void kallsyms_show_paraminfo(struct pt_regs *regs)
+{
+ unsigned long long raw_offset;
+ unsigned int offset;
+ int func_idx;
+ const u8 *data;
+ unsigned int num_params, i;
+ unsigned long ip, fault_addr;
+ char sym_name[KSYM_NAME_LEN];
+ unsigned long sym_size, sym_offset;
+
+ if (!regs || !paraminfo_num_funcs)
+ return;
+
+ ip = regs->ip;
+
+ /* Only handle kernel-mode faults */
+ if (user_mode(regs))
+ return;
+
+ if (ip < (unsigned long)_text)
+ return;
+
+ raw_offset = ip - (unsigned long)_text;
+ if (raw_offset > UINT_MAX)
+ return;
+ offset = (unsigned int)raw_offset;
+
+ func_idx = paraminfo_find_func(offset);
+ if (func_idx < 0)
+ return;
+
+ /*
+ * Verify the IP is within a reasonable range of the function
+ * start. paraminfo_func_addrs[] contains function start offsets;
+ * check that we're not too far past the start. Use kallsyms to
+ * verify we're in the right function.
+ */
+ if (!kallsyms_lookup_size_offset(ip, &sym_size, &sym_offset))
+ return;
+
+ /* Decode the function's parameter data */
+ data = paraminfo_func_data + paraminfo_func_offsets[func_idx];
+ num_params = *data++;
+
+ if (num_params == 0 || num_params > MAX_PARAMINFO_PARAMS)
+ return;
+
+ /* Look up function name for the header */
+ if (lookup_symbol_name(ip - sym_offset, sym_name))
+ return;
+
+ /*
+ * Read the fault address for highlighting. On x86, CR2 holds
+ * the page fault linear address. On other architectures this
+ * would need a different mechanism.
+ */
+#ifdef CONFIG_X86
+ fault_addr = read_cr2();
+#else
+ fault_addr = 0;
+#endif
+
+ printk(KERN_DEFAULT "Function parameters (%s):\n", sym_name);
+
+ for (i = 0; i < num_params; i++) {
+ u32 name_off, type_off;
+ const char *pname, *ptype;
+ unsigned long val;
+ bool is_fault_addr;
+
+ memcpy(&name_off, data, sizeof(u32));
+ data += sizeof(u32);
+ memcpy(&type_off, data, sizeof(u32));
+ data += sizeof(u32);
+
+ pname = paraminfo_strings + name_off;
+ ptype = paraminfo_strings + type_off;
+ val = paraminfo_get_reg(regs, i);
+
+ is_fault_addr = fault_addr && (val == fault_addr);
+
+ printk(KERN_DEFAULT " %-8s (%-20s) = 0x%016lx%s\n",
+ pname, ptype, val,
+ is_fault_addr ? " <-- fault address" : "");
+ }
+}
+EXPORT_SYMBOL_GPL(kallsyms_show_paraminfo);
+
+#endif /* CONFIG_KALLSYMS_PARAMINFO */
+
/* Look up a kernel symbol and return it in a text buffer. */
static int __sprint_symbol(char *buffer, unsigned long address,
int symbol_offset, int add_offset, int add_buildid)
diff --git a/kernel/kallsyms_internal.h b/kernel/kallsyms_internal.h
index ffe4c658067ec..7287ee0859515 100644
--- a/kernel/kallsyms_internal.h
+++ b/kernel/kallsyms_internal.h
@@ -26,4 +26,10 @@ extern const u32 lineinfo_file_offsets[];
extern const u32 lineinfo_filenames_size;
extern const char lineinfo_filenames[];
+extern const u32 paraminfo_num_funcs;
+extern const u32 paraminfo_func_addrs[];
+extern const u32 paraminfo_func_offsets[];
+extern const u8 paraminfo_func_data[];
+extern const char paraminfo_strings[];
+
#endif // LINUX_KALLSYMS_INTERNAL_H_
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 688bbcb3eaa62..be8cee0985fbd 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -3058,6 +3058,17 @@ config LINEINFO_KUNIT_TEST
If unsure, say N.
+config PARAMINFO_KUNIT_TEST
+ tristate "KUnit tests for kallsyms paraminfo" if !KUNIT_ALL_TESTS
+ depends on KUNIT && KALLSYMS_PARAMINFO
+ default KUNIT_ALL_TESTS
+ help
+ KUnit tests for the kallsyms function parameter info feature.
+ Verifies that paraminfo tables correctly map functions to their
+ parameter names and types.
+
+ If unsure, say N.
+
config HW_BREAKPOINT_KUNIT_TEST
bool "Test hw_breakpoint constraints accounting" if !KUNIT_ALL_TESTS
depends on HAVE_HW_BREAKPOINT
diff --git a/lib/tests/Makefile b/lib/tests/Makefile
index c6add3b04bbd5..70452942baf45 100644
--- a/lib/tests/Makefile
+++ b/lib/tests/Makefile
@@ -39,6 +39,9 @@ obj-$(CONFIG_LONGEST_SYM_KUNIT_TEST) += longest_symbol_kunit.o
CFLAGS_lineinfo_kunit.o += $(call cc-option,-fno-inline-functions-called-once)
obj-$(CONFIG_LINEINFO_KUNIT_TEST) += lineinfo_kunit.o
+CFLAGS_paraminfo_kunit.o += $(call cc-option,-fno-inline-functions-called-once)
+obj-$(CONFIG_PARAMINFO_KUNIT_TEST) += paraminfo_kunit.o
+
obj-$(CONFIG_MEMCPY_KUNIT_TEST) += memcpy_kunit.o
obj-$(CONFIG_MIN_HEAP_KUNIT_TEST) += min_heap_kunit.o
CFLAGS_overflow_kunit.o = $(call cc-disable-warning,
tautological-constant-out-of-range-compare)
diff --git a/lib/tests/paraminfo_kunit.c b/lib/tests/paraminfo_kunit.c
new file mode 100644
index 0000000000000..e09efc4ddeb0e
--- /dev/null
+++ b/lib/tests/paraminfo_kunit.c
@@ -0,0 +1,256 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for kallsyms paraminfo (CONFIG_KALLSYMS_PARAMINFO).
+ *
+ * Copyright (c) 2026 Sasha Levin <[email protected]>
+ *
+ * Verifies that the paraminfo tables correctly map function addresses
+ * to their parameter names and types.
+ *
+ * Build with: CONFIG_PARAMINFO_KUNIT_TEST=m (or =y)
+ */
+
+#include <kunit/test.h>
+#include <linux/kallsyms.h>
+#include <linux/module.h>
+#include <linux/string.h>
+#include <linux/slab.h>
+
+/* ---- Test target functions with known signatures ---- */
+
+static noinline int paraminfo_test_two_args(struct kunit *test, int value)
+{
+ /* Prevent optimization */
+ return test ? value + 1 : 0;
+}
+
+static noinline void *paraminfo_test_ptr_arg(void *ptr, unsigned long size)
+{
+ if (ptr && size > 0)
+ return ptr;
+ return NULL;
+}
+
+static noinline long paraminfo_test_many_args(int a, int b, int c,
+ int d, int e, int f)
+{
+ return (long)a + b + c + d + e + f;
+}
+
+static noinline void paraminfo_test_no_args(void)
+{
+ /* Function with no parameters */
+ barrier();
+}
+
+/* ---- Helpers to query paraminfo tables directly ---- */
+
+/*
+ * These access the raw paraminfo tables to verify correctness.
+ * The tables are defined in kernel/kallsyms_internal.h.
+ */
+extern const u32 paraminfo_num_funcs;
+extern const u32 paraminfo_func_addrs[];
+extern const u32 paraminfo_func_offsets[];
+extern const u8 paraminfo_func_data[];
+extern const char paraminfo_strings[];
+
+struct param_result {
+ unsigned int num_params;
+ const char *names[6];
+ const char *types[6];
+};
+
+/*
+ * Look up paraminfo for a given kernel address.
+ * Returns true if found, filling in @result.
+ */
+static bool lookup_paraminfo(unsigned long addr, struct param_result *result)
+{
+ unsigned long long raw_offset;
+ unsigned int offset;
+ int lo, hi, func_idx;
+ const u8 *data;
+ unsigned int i;
+
+ if (!paraminfo_num_funcs)
+ return false;
+
+ if (addr < (unsigned long)_text)
+ return false;
+
+ raw_offset = addr - (unsigned long)_text;
+ if (raw_offset > UINT_MAX)
+ return false;
+ offset = (unsigned int)raw_offset;
+
+ /* Binary search for the function */
+ lo = 0;
+ hi = paraminfo_num_funcs - 1;
+ func_idx = -1;
+ while (lo <= hi) {
+ int mid = lo + (hi - lo) / 2;
+
+ if (paraminfo_func_addrs[mid] <= offset) {
+ func_idx = mid;
+ lo = mid + 1;
+ } else {
+ hi = mid - 1;
+ }
+ }
+
+ if (func_idx < 0)
+ return false;
+
+ /* Verify we're not too far from the function start */
+ if (offset - paraminfo_func_addrs[func_idx] > 0x10000)
+ return false;
+
+ data = paraminfo_func_data + paraminfo_func_offsets[func_idx];
+ result->num_params = *data++;
+
+ if (result->num_params > 6)
+ return false;
+
+ for (i = 0; i < result->num_params; i++) {
+ u32 name_off, type_off;
+
+ memcpy(&name_off, data, sizeof(u32));
+ data += sizeof(u32);
+ memcpy(&type_off, data, sizeof(u32));
+ data += sizeof(u32);
+
+ result->names[i] = paraminfo_strings + name_off;
+ result->types[i] = paraminfo_strings + type_off;
+ }
+
+ return true;
+}
+
+/* ---- Test cases ---- */
+
+static void test_paraminfo_two_args(struct kunit *test)
+{
+ struct param_result res;
+ bool found;
+
+ found = lookup_paraminfo((unsigned long)paraminfo_test_two_args, &res);
+
+ if (!IS_ENABLED(CONFIG_KALLSYMS_PARAMINFO)) {
+ KUNIT_EXPECT_FALSE(test, found);
+ return;
+ }
+
+ KUNIT_ASSERT_TRUE(test, found);
+ KUNIT_EXPECT_EQ(test, res.num_params, 2U);
+ KUNIT_EXPECT_STREQ(test, res.names[0], "test");
+ KUNIT_EXPECT_STREQ(test, res.names[1], "value");
+ KUNIT_EXPECT_TRUE(test, strstr(res.types[1], "int") != NULL);
+}
+
+static void test_paraminfo_ptr_arg(struct kunit *test)
+{
+ struct param_result res;
+ bool found;
+
+ found = lookup_paraminfo((unsigned long)paraminfo_test_ptr_arg, &res);
+
+ if (!IS_ENABLED(CONFIG_KALLSYMS_PARAMINFO)) {
+ KUNIT_EXPECT_FALSE(test, found);
+ return;
+ }
+
+ KUNIT_ASSERT_TRUE(test, found);
+ KUNIT_EXPECT_EQ(test, res.num_params, 2U);
+ KUNIT_EXPECT_STREQ(test, res.names[0], "ptr");
+ KUNIT_EXPECT_STREQ(test, res.names[1], "size");
+ /* First param should be a pointer type */
+ KUNIT_EXPECT_TRUE(test, strstr(res.types[0], "*") != NULL);
+}
+
+static void test_paraminfo_many_args(struct kunit *test)
+{
+ struct param_result res;
+ bool found;
+
+ found = lookup_paraminfo((unsigned long)paraminfo_test_many_args, &res);
+
+ if (!IS_ENABLED(CONFIG_KALLSYMS_PARAMINFO)) {
+ KUNIT_EXPECT_FALSE(test, found);
+ return;
+ }
+
+ KUNIT_ASSERT_TRUE(test, found);
+ KUNIT_EXPECT_EQ(test, res.num_params, 6U);
+ KUNIT_EXPECT_STREQ(test, res.names[0], "a");
+ KUNIT_EXPECT_STREQ(test, res.names[1], "b");
+ KUNIT_EXPECT_STREQ(test, res.names[2], "c");
+ KUNIT_EXPECT_STREQ(test, res.names[3], "d");
+ KUNIT_EXPECT_STREQ(test, res.names[4], "e");
+ KUNIT_EXPECT_STREQ(test, res.names[5], "f");
+}
+
+static void test_paraminfo_no_args(struct kunit *test)
+{
+ struct param_result res;
+ bool found;
+
+ /*
+ * Functions with no parameters should not have entries in the
+ * paraminfo table (they are filtered out at build time).
+ * The lookup may find a nearby function instead, but if it
+ * does find our function exactly, num_params should be 0.
+ */
+ found = lookup_paraminfo((unsigned long)paraminfo_test_no_args, &res);
+
+ if (found) {
+ /* If it matched our exact function, it should have 0 params.
+ * But it may have matched a preceding function instead.
+ */
+ kunit_info(test, "lookup found func with %u params\n",
+ res.num_params);
+ }
+}
+
+static void test_paraminfo_bogus_addr(struct kunit *test)
+{
+ struct param_result res;
+
+ /* Address 0 should not match anything */
+ KUNIT_EXPECT_FALSE(test, lookup_paraminfo(0, &res));
+
+ /* Address in userspace should not match */
+ KUNIT_EXPECT_FALSE(test, lookup_paraminfo(0x1000, &res));
+}
+
+static void test_paraminfo_tables_present(struct kunit *test)
+{
+ if (!IS_ENABLED(CONFIG_KALLSYMS_PARAMINFO)) {
+ kunit_skip(test, "CONFIG_KALLSYMS_PARAMINFO not enabled");
+ return;
+ }
+
+ KUNIT_EXPECT_GT(test, paraminfo_num_funcs, 0U);
+ kunit_info(test, "paraminfo: %u functions in table\n",
+ paraminfo_num_funcs);
+}
+
+static struct kunit_case paraminfo_test_cases[] = {
+ KUNIT_CASE(test_paraminfo_tables_present),
+ KUNIT_CASE(test_paraminfo_two_args),
+ KUNIT_CASE(test_paraminfo_ptr_arg),
+ KUNIT_CASE(test_paraminfo_many_args),
+ KUNIT_CASE(test_paraminfo_no_args),
+ KUNIT_CASE(test_paraminfo_bogus_addr),
+ {}
+};
+
+static struct kunit_suite paraminfo_test_suite = {
+ .name = "paraminfo",
+ .test_cases = paraminfo_test_cases,
+};
+
+kunit_test_suite(paraminfo_test_suite);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("KUnit tests for kallsyms paraminfo");
diff --git a/scripts/Makefile b/scripts/Makefile
index ffe89875b3295..f681b94c6d9e7 100644
--- a/scripts/Makefile
+++ b/scripts/Makefile
@@ -5,6 +5,7 @@
hostprogs-always-$(CONFIG_KALLSYMS) += kallsyms
hostprogs-always-$(CONFIG_KALLSYMS_LINEINFO) += gen_lineinfo
+hostprogs-always-$(CONFIG_KALLSYMS_PARAMINFO) += gen_paraminfo
hostprogs-always-$(BUILD_C_RECORDMCOUNT) += recordmcount
hostprogs-always-$(CONFIG_BUILDTIME_TABLE_SORT) += sorttable
hostprogs-always-$(CONFIG_ASN1) += asn1_compiler
@@ -39,6 +40,8 @@ HOSTCFLAGS_sign-file.o = $(shell $(HOSTPKG_CONFIG) --cflags
libcrypto 2> /dev/nu
HOSTLDLIBS_sign-file = $(shell $(HOSTPKG_CONFIG) --libs libcrypto 2> /dev/null
|| echo -lcrypto)
HOSTCFLAGS_gen_lineinfo.o = $(shell $(HOSTPKG_CONFIG) --cflags libdw 2>
/dev/null)
HOSTLDLIBS_gen_lineinfo = $(shell $(HOSTPKG_CONFIG) --libs libdw 2> /dev/null
|| echo -ldw -lelf -lz)
+HOSTCFLAGS_gen_paraminfo.o = $(shell $(HOSTPKG_CONFIG) --cflags libdw 2>
/dev/null)
+HOSTLDLIBS_gen_paraminfo = $(shell $(HOSTPKG_CONFIG) --libs libdw 2> /dev/null
|| echo -ldw -lelf -lz)
ifdef CONFIG_UNWINDER_ORC
ifeq ($(ARCH),x86_64)
diff --git a/scripts/empty_paraminfo.S b/scripts/empty_paraminfo.S
new file mode 100644
index 0000000000000..a0e39dbfbd3cb
--- /dev/null
+++ b/scripts/empty_paraminfo.S
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Empty paraminfo stub for the initial vmlinux link.
+ * The real paraminfo is generated from .tmp_vmlinux1 by gen_paraminfo.
+ */
+ .section .rodata, "a"
+ .globl paraminfo_num_funcs
+ .balign 4
+paraminfo_num_funcs:
+ .long 0
+ .globl paraminfo_func_addrs
+paraminfo_func_addrs:
+ .globl paraminfo_func_offsets
+paraminfo_func_offsets:
+ .globl paraminfo_func_data
+paraminfo_func_data:
+ .globl paraminfo_strings
+paraminfo_strings:
diff --git a/scripts/gen_paraminfo.c b/scripts/gen_paraminfo.c
new file mode 100644
index 0000000000000..ea1d23f3ddd9a
--- /dev/null
+++ b/scripts/gen_paraminfo.c
@@ -0,0 +1,549 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * gen_paraminfo.c - Generate function parameter info tables from DWARF
+ *
+ * Copyright (C) 2026 Sasha Levin <[email protected]>
+ *
+ * Reads DWARF .debug_info from a vmlinux ELF file and outputs an assembly
+ * file containing function parameter name/type tables that the kernel uses
+ * to annotate oops/WARN dumps with typed parameter values.
+ *
+ * Requires libdw from elfutils.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <elfutils/libdw.h>
+#include <dwarf.h>
+#include <elf.h>
+#include <gelf.h>
+#include <limits.h>
+
+/* Maximum register-passed parameters on x86-64 */
+#define MAX_PARAMS 6
+
+/* Maximum length for a type name string */
+#define MAX_TYPE_LEN 64
+
+struct param_info {
+ char name[64];
+ char type[MAX_TYPE_LEN];
+};
+
+struct func_entry {
+ unsigned int offset; /* offset from _text */
+ unsigned int num_params;
+ struct param_info params[MAX_PARAMS];
+};
+
+static struct func_entry *funcs;
+static unsigned int num_funcs;
+static unsigned int funcs_capacity;
+
+/*
+ * String table for parameter names and type names.
+ * Deduplicated via a hash table.
+ */
+struct str_entry {
+ char *str;
+ unsigned int offset; /* byte offset in concatenated output */
+};
+
+static struct str_entry *strtab;
+static unsigned int num_strings;
+static unsigned int strtab_capacity;
+static unsigned int strtab_total_size;
+
+#define STR_HASH_BITS 14
+#define STR_HASH_SIZE (1 << STR_HASH_BITS)
+
+struct str_hash_entry {
+ const char *str;
+ unsigned int idx; /* index into strtab[] */
+};
+
+static struct str_hash_entry str_hash[STR_HASH_SIZE];
+
+static unsigned int hash_str(const char *s)
+{
+ unsigned int h = 5381;
+
+ for (; *s; s++)
+ h = h * 33 + (unsigned char)*s;
+ return h & (STR_HASH_SIZE - 1);
+}
+
+static unsigned int find_or_add_string(const char *s)
+{
+ unsigned int h = hash_str(s);
+
+ while (str_hash[h].str) {
+ if (!strcmp(str_hash[h].str, s))
+ return str_hash[h].idx;
+ h = (h + 1) & (STR_HASH_SIZE - 1);
+ }
+
+ if (num_strings >= strtab_capacity) {
+ strtab_capacity = strtab_capacity ? strtab_capacity * 2 : 8192;
+ strtab = realloc(strtab, strtab_capacity * sizeof(*strtab));
+ if (!strtab) {
+ fprintf(stderr, "out of memory\n");
+ exit(1);
+ }
+ }
+
+ strtab[num_strings].str = strdup(s);
+ strtab[num_strings].offset = strtab_total_size;
+ strtab_total_size += strlen(s) + 1;
+
+ str_hash[h].str = strtab[num_strings].str;
+ str_hash[h].idx = num_strings;
+
+ num_strings++;
+ return num_strings - 1;
+}
+
+static void add_func(struct func_entry *f)
+{
+ if (num_funcs >= funcs_capacity) {
+ funcs_capacity = funcs_capacity ? funcs_capacity * 2 : 16384;
+ funcs = realloc(funcs, funcs_capacity * sizeof(*funcs));
+ if (!funcs) {
+ fprintf(stderr, "out of memory\n");
+ exit(1);
+ }
+ }
+ funcs[num_funcs++] = *f;
+}
+
+/*
+ * Build a human-readable type name string from a DWARF type DIE.
+ * Follows the type chain (pointers, const, etc.) to produce strings like:
+ * "struct file *", "const char *", "unsigned long", "void *"
+ */
+static void build_type_name(Dwarf_Die *type_die, char *buf, size_t bufsz)
+{
+ Dwarf_Die child;
+ Dwarf_Attribute attr;
+ const char *name;
+ int tag;
+
+ if (!type_die) {
+ snprintf(buf, bufsz, "void");
+ return;
+ }
+
+ tag = dwarf_tag(type_die);
+
+ switch (tag) {
+ case DW_TAG_base_type:
+ name = dwarf_diename(type_die);
+ snprintf(buf, bufsz, "%s", name ? name : "?");
+ break;
+
+ case DW_TAG_pointer_type:
+ if (dwarf_attr(type_die, DW_AT_type, &attr) &&
+ dwarf_formref_die(&attr, &child)) {
+ build_type_name(&child, buf, bufsz);
+ if (strlen(buf) + 3 < bufsz)
+ strcat(buf, " *");
+ } else {
+ snprintf(buf, bufsz, "void *");
+ }
+ break;
+
+ case DW_TAG_const_type:
+ if (dwarf_attr(type_die, DW_AT_type, &attr) &&
+ dwarf_formref_die(&attr, &child)) {
+ char tmp[MAX_TYPE_LEN - 10];
+
+ build_type_name(&child, tmp, sizeof(tmp));
+ snprintf(buf, bufsz, "const %s", tmp);
+ } else {
+ snprintf(buf, bufsz, "const void");
+ }
+ break;
+
+ case DW_TAG_volatile_type:
+ if (dwarf_attr(type_die, DW_AT_type, &attr) &&
+ dwarf_formref_die(&attr, &child)) {
+ char tmp[MAX_TYPE_LEN - 10];
+
+ build_type_name(&child, tmp, sizeof(tmp));
+ snprintf(buf, bufsz, "volatile %s", tmp);
+ } else {
+ snprintf(buf, bufsz, "volatile void");
+ }
+ break;
+
+ case DW_TAG_restrict_type:
+ if (dwarf_attr(type_die, DW_AT_type, &attr) &&
+ dwarf_formref_die(&attr, &child)) {
+ build_type_name(&child, buf, bufsz);
+ } else {
+ snprintf(buf, bufsz, "void");
+ }
+ break;
+
+ case DW_TAG_typedef:
+ name = dwarf_diename(type_die);
+ if (name) {
+ snprintf(buf, bufsz, "%s", name);
+ } else if (dwarf_attr(type_die, DW_AT_type, &attr) &&
+ dwarf_formref_die(&attr, &child)) {
+ build_type_name(&child, buf, bufsz);
+ } else {
+ snprintf(buf, bufsz, "?");
+ }
+ break;
+
+ case DW_TAG_structure_type:
+ name = dwarf_diename(type_die);
+ snprintf(buf, bufsz, "struct %s", name ? name : "(anon)");
+ break;
+
+ case DW_TAG_union_type:
+ name = dwarf_diename(type_die);
+ snprintf(buf, bufsz, "union %s", name ? name : "(anon)");
+ break;
+
+ case DW_TAG_enumeration_type:
+ name = dwarf_diename(type_die);
+ snprintf(buf, bufsz, "enum %s", name ? name : "(anon)");
+ break;
+
+ case DW_TAG_array_type:
+ if (dwarf_attr(type_die, DW_AT_type, &attr) &&
+ dwarf_formref_die(&attr, &child)) {
+ build_type_name(&child, buf, bufsz);
+ if (strlen(buf) + 3 < bufsz)
+ strcat(buf, "[]");
+ } else {
+ snprintf(buf, bufsz, "?[]");
+ }
+ break;
+
+ case DW_TAG_subroutine_type:
+ snprintf(buf, bufsz, "func_ptr");
+ break;
+
+ default:
+ snprintf(buf, bufsz, "?");
+ break;
+ }
+}
+
+static unsigned long long find_text_addr(Elf *elf)
+{
+ size_t nsyms, i;
+ Elf_Scn *scn = NULL;
+ GElf_Shdr shdr;
+
+ while ((scn = elf_nextscn(elf, scn)) != NULL) {
+ Elf_Data *data;
+
+ if (!gelf_getshdr(scn, &shdr))
+ continue;
+ if (shdr.sh_type != SHT_SYMTAB)
+ continue;
+
+ data = elf_getdata(scn, NULL);
+ if (!data)
+ continue;
+
+ nsyms = shdr.sh_size / shdr.sh_entsize;
+ for (i = 0; i < nsyms; i++) {
+ GElf_Sym sym;
+ const char *name;
+
+ if (!gelf_getsym(data, i, &sym))
+ continue;
+ name = elf_strptr(elf, shdr.sh_link, sym.st_name);
+ if (name && !strcmp(name, "_text"))
+ return sym.st_value;
+ }
+ }
+
+ fprintf(stderr, "Cannot find _text symbol\n");
+ exit(1);
+}
+
+static int compare_funcs(const void *a, const void *b)
+{
+ const struct func_entry *fa = a;
+ const struct func_entry *fb = b;
+
+ if (fa->offset < fb->offset)
+ return -1;
+ if (fa->offset > fb->offset)
+ return 1;
+ return 0;
+}
+
+static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
+{
+ Dwarf_Off off = 0, next_off;
+ size_t hdr_size;
+
+ while (dwarf_nextcu(dwarf, off, &next_off, &hdr_size,
+ NULL, NULL, NULL) == 0) {
+ Dwarf_Die cudie, child;
+
+ if (!dwarf_offdie(dwarf, off + hdr_size, &cudie))
+ goto next;
+
+ if (dwarf_child(&cudie, &child) != 0)
+ goto next;
+
+ do {
+ Dwarf_Die param;
+ Dwarf_Attribute attr;
+ Dwarf_Addr low_pc;
+ struct func_entry func;
+ int tag;
+
+ tag = dwarf_tag(&child);
+ if (tag != DW_TAG_subprogram)
+ continue;
+
+ /* Skip declarations (no body) */
+ if (dwarf_attr(&child, DW_AT_declaration, &attr))
+ continue;
+
+ /* Get function start address */
+ if (dwarf_lowpc(&child, &low_pc) != 0)
+ continue;
+
+ if (low_pc < text_addr)
+ continue;
+
+ {
+ unsigned long long raw_offset = low_pc -
text_addr;
+
+ if (raw_offset > UINT_MAX)
+ continue;
+ func.offset = (unsigned int)raw_offset;
+ }
+
+ /* Iterate formal parameters */
+ func.num_params = 0;
+ if (dwarf_child(&child, ¶m) == 0) {
+ do {
+ Dwarf_Die type_die;
+ const char *pname;
+
+ if (dwarf_tag(¶m) !=
DW_TAG_formal_parameter)
+ continue;
+ if (func.num_params >= MAX_PARAMS)
+ break;
+
+ pname = dwarf_diename(¶m);
+ if (!pname)
+ pname = "?";
+
+
snprintf(func.params[func.num_params].name,
+ sizeof(func.params[0].name),
+ "%s", pname);
+
+ /* Resolve type */
+ if (dwarf_attr(¶m, DW_AT_type,
&attr) &&
+ dwarf_formref_die(&attr,
&type_die)) {
+ build_type_name(&type_die,
+
func.params[func.num_params].type,
+ MAX_TYPE_LEN);
+ } else {
+
snprintf(func.params[func.num_params].type,
+ MAX_TYPE_LEN, "?");
+ }
+
+ func.num_params++;
+ } while (dwarf_siblingof(¶m, ¶m) == 0);
+ }
+
+ /* Skip functions with no parameters */
+ if (func.num_params == 0)
+ continue;
+
+ add_func(&func);
+ } while (dwarf_siblingof(&child, &child) == 0);
+next:
+ off = next_off;
+ }
+}
+
+static void deduplicate(void)
+{
+ unsigned int i, j;
+
+ if (num_funcs < 2)
+ return;
+
+ /* Sort by offset */
+ qsort(funcs, num_funcs, sizeof(*funcs), compare_funcs);
+
+ /* Remove duplicates (same offset — keep first) */
+ j = 0;
+ for (i = 1; i < num_funcs; i++) {
+ if (funcs[i].offset == funcs[j].offset)
+ continue;
+ j++;
+ if (j != i)
+ funcs[j] = funcs[i];
+ }
+ num_funcs = j + 1;
+}
+
+static void print_escaped_asciz(const char *s)
+{
+ printf("\t.asciz \"");
+ for (; *s; s++) {
+ if (*s == '"' || *s == '\\')
+ putchar('\\');
+ putchar(*s);
+ }
+ printf("\"\n");
+}
+
+static void output_assembly(void)
+{
+ unsigned int i, j;
+
+ printf("/* SPDX-License-Identifier: GPL-2.0 */\n");
+ printf("/*\n");
+ printf(" * Automatically generated by scripts/gen_paraminfo\n");
+ printf(" * Do not edit.\n");
+ printf(" */\n\n");
+
+ printf("\t.section .rodata, \"a\"\n\n");
+
+ /* Number of functions */
+ printf("\t.globl paraminfo_num_funcs\n");
+ printf("\t.balign 4\n");
+ printf("paraminfo_num_funcs:\n");
+ printf("\t.long %u\n\n", num_funcs);
+
+ /* Function address offsets (sorted, for binary search) */
+ printf("\t.globl paraminfo_func_addrs\n");
+ printf("\t.balign 4\n");
+ printf("paraminfo_func_addrs:\n");
+ for (i = 0; i < num_funcs; i++)
+ printf("\t.long 0x%x\n", funcs[i].offset);
+ printf("\n");
+
+ /*
+ * Function data offsets — byte offset into paraminfo_func_data
+ * for each function's parameter list.
+ */
+ printf("\t.globl paraminfo_func_offsets\n");
+ printf("\t.balign 4\n");
+ printf("paraminfo_func_offsets:\n");
+ for (i = 0; i < num_funcs; i++)
+ printf("\t.long .Lfunc_%u - paraminfo_func_data\n", i);
+ printf("\n");
+
+ /*
+ * Register strings in the string table and build the func data.
+ * Func data format per function:
+ * u8 num_params
+ * For each param:
+ * u32 name_str_offset
+ * u32 type_str_offset
+ */
+ /* First pass: register all strings */
+ for (i = 0; i < num_funcs; i++) {
+ for (j = 0; j < funcs[i].num_params; j++) {
+ find_or_add_string(funcs[i].params[j].name);
+ find_or_add_string(funcs[i].params[j].type);
+ }
+ }
+
+ /* Function parameter data */
+ printf("\t.globl paraminfo_func_data\n");
+ printf("paraminfo_func_data:\n");
+ for (i = 0; i < num_funcs; i++) {
+ printf(".Lfunc_%u:\n", i);
+ printf("\t.byte %u\n", funcs[i].num_params);
+ for (j = 0; j < funcs[i].num_params; j++) {
+ unsigned int name_idx =
find_or_add_string(funcs[i].params[j].name);
+ unsigned int type_idx =
find_or_add_string(funcs[i].params[j].type);
+
+ printf("\t.long %u, %u\n",
+ strtab[name_idx].offset,
+ strtab[type_idx].offset);
+ }
+ }
+ printf("\n");
+
+ /* String table */
+ printf("\t.globl paraminfo_strings\n");
+ printf("paraminfo_strings:\n");
+ for (i = 0; i < num_strings; i++)
+ print_escaped_asciz(strtab[i].str);
+ printf("\n");
+}
+
+int main(int argc, char *argv[])
+{
+ int fd;
+ Elf *elf;
+ Dwarf *dwarf;
+ unsigned long long text_addr;
+
+ if (argc != 2) {
+ fprintf(stderr, "Usage: %s <vmlinux ELF>\n", argv[0]);
+ return 1;
+ }
+
+ fd = open(argv[1], O_RDONLY);
+ if (fd < 0) {
+ fprintf(stderr, "Cannot open %s: %s\n", argv[1],
+ strerror(errno));
+ return 1;
+ }
+
+ elf_version(EV_CURRENT);
+ elf = elf_begin(fd, ELF_C_READ, NULL);
+ if (!elf) {
+ fprintf(stderr, "elf_begin failed: %s\n",
+ elf_errmsg(elf_errno()));
+ close(fd);
+ return 1;
+ }
+
+ text_addr = find_text_addr(elf);
+
+ dwarf = dwarf_begin_elf(elf, DWARF_C_READ, NULL);
+ if (!dwarf) {
+ fprintf(stderr, "dwarf_begin_elf failed: %s\n",
+ dwarf_errmsg(dwarf_errno()));
+ fprintf(stderr, "Is %s built with CONFIG_DEBUG_INFO?\n",
+ argv[1]);
+ elf_end(elf);
+ close(fd);
+ return 1;
+ }
+
+ process_dwarf(dwarf, text_addr);
+ deduplicate();
+
+ fprintf(stderr, "paraminfo: %u functions, %u strings\n",
+ num_funcs, num_strings);
+
+ output_assembly();
+
+ dwarf_end(dwarf);
+ elf_end(elf);
+ close(fd);
+
+ /* Cleanup */
+ free(funcs);
+ for (unsigned int i = 0; i < num_strings; i++)
+ free(strtab[i].str);
+ free(strtab);
+
+ return 0;
+}
diff --git a/scripts/link-vmlinux.sh b/scripts/link-vmlinux.sh
index 39ca44fbb259b..d41b31dde4ef9 100755
--- a/scripts/link-vmlinux.sh
+++ b/scripts/link-vmlinux.sh
@@ -103,7 +103,7 @@ vmlinux_link()
${ld} ${ldflags} -o ${output} \
${wl}--whole-archive ${objs} ${wl}--no-whole-archive \
${wl}--start-group ${libs} ${wl}--end-group \
- ${kallsymso} ${lineinfo_o} ${btf_vmlinux_bin_o}
${arch_vmlinux_o} ${ldlibs}
+ ${kallsymso} ${lineinfo_o} ${paraminfo_o} ${btf_vmlinux_bin_o}
${arch_vmlinux_o} ${ldlibs}
}
# Create ${2}.o file with all symbols from the ${1} object file
@@ -149,6 +149,26 @@ gen_lineinfo()
lineinfo_o=.tmp_lineinfo.o
}
+# Generate paraminfo tables from DWARF debug info in a temporary vmlinux.
+# ${1} - temporary vmlinux with debug info
+# Output: sets paraminfo_o to the generated .o file
+gen_paraminfo()
+{
+ info PARAMINFO .tmp_paraminfo.S
+ if ! scripts/gen_paraminfo "${1}" > .tmp_paraminfo.S; then
+ echo >&2 "Failed to generate paraminfo from ${1}"
+ echo >&2 "Try to disable CONFIG_KALLSYMS_PARAMINFO"
+ exit 1
+ fi
+
+ info AS .tmp_paraminfo.o
+ ${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \
+ ${KBUILD_AFLAGS} ${KBUILD_AFLAGS_KERNEL} \
+ -c -o .tmp_paraminfo.o .tmp_paraminfo.S
+
+ paraminfo_o=.tmp_paraminfo.o
+}
+
# Perform kallsyms for the given temporary vmlinux.
sysmap_and_kallsyms()
{
@@ -176,6 +196,7 @@ cleanup()
{
rm -f .btf.*
rm -f .tmp_lineinfo.*
+ rm -f .tmp_paraminfo.*
rm -f .tmp_vmlinux.nm-sort
rm -f System.map
rm -f vmlinux
@@ -205,6 +226,7 @@ btf_vmlinux_bin_o=
btfids_vmlinux=
kallsymso=
lineinfo_o=
+paraminfo_o=
strip_debug=
generate_map=
@@ -229,12 +251,22 @@ if is_enabled CONFIG_KALLSYMS_LINEINFO; then
lineinfo_o=.tmp_lineinfo.o
fi
+if is_enabled CONFIG_KALLSYMS_PARAMINFO; then
+ # Assemble an empty paraminfo stub for the initial link.
+ # The real paraminfo is generated from .tmp_vmlinux1 by gen_paraminfo.
+ ${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \
+ ${KBUILD_AFLAGS} ${KBUILD_AFLAGS_KERNEL} \
+ -c -o .tmp_paraminfo.o "${srctree}/scripts/empty_paraminfo.S"
+ paraminfo_o=.tmp_paraminfo.o
+fi
+
if is_enabled CONFIG_KALLSYMS || is_enabled CONFIG_DEBUG_INFO_BTF; then
- # The kallsyms linking does not need debug symbols, but BTF and
- # lineinfo generation do.
+ # The kallsyms linking does not need debug symbols, but BTF,
+ # lineinfo and paraminfo generation do.
if ! is_enabled CONFIG_DEBUG_INFO_BTF &&
- ! is_enabled CONFIG_KALLSYMS_LINEINFO; then
+ ! is_enabled CONFIG_KALLSYMS_LINEINFO &&
+ ! is_enabled CONFIG_KALLSYMS_PARAMINFO; then
strip_debug=1
fi
@@ -256,6 +288,10 @@ if is_enabled CONFIG_KALLSYMS_LINEINFO; then
gen_lineinfo .tmp_vmlinux1
fi
+if is_enabled CONFIG_KALLSYMS_PARAMINFO; then
+ gen_paraminfo .tmp_vmlinux1
+fi
+
if is_enabled CONFIG_KALLSYMS; then
# kallsyms support
--
2.51.0