public inbox for linux-kernel@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/3] kallsyms: embed source file:line info in kernel stack traces
@ 2026-03-03 18:21 Sasha Levin
  2026-03-03 18:21 ` [PATCH 1/3] " Sasha Levin
                   ` (2 more replies)
  0 siblings, 3 replies; 21+ messages in thread
From: Sasha Levin @ 2026-03-03 18:21 UTC (permalink / raw)
  To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley
  Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
	Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
	Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, linux-kernel,
	linux-kbuild, linux-modules, linux-doc, Sasha Levin

This series adds CONFIG_KALLSYMS_LINEINFO, which embeds source file:line
information directly in the kernel image so that stack traces annotate
every frame with the originating source location - no external tools, no
debug symbols at runtime, and safe to use in NMI/panic context.

Motivation
==========

The recent "slowly decommission bugzilla?" thread [1] surfaced a recurring
problem: when users encounter kernel crashes they see stack traces like
`func+0x1ec/0x240` but have no way to identify which subsystem or
maintainer to contact.  Richard Weinberger proposed building a database
mapping symbols to source files using nm/DWARF.  Linus pointed to
scripts/decode_stacktrace.sh as the existing solution.  But as the
discussion progressed, it became clear that decode_stacktrace.sh has
significant practical barriers that prevent it from being useful in the
common case.

Problems with scripts/decode_stacktrace.sh
==========================================

- Requires debug symbols: the script needs vmlinux with DWARF debug info.
  Many distros don't retain debug symbols for older or security kernels,
  and even when available, asking users to obtain matching debuginfo
  packages is a significant hurdle.

- Requires toolchain: users need addr2line and nm installed.

- Version-matching requirement: debug symbols must exactly match the
  running kernel binary.

What this series does
=====================

Patch 1: CONFIG_KALLSYMS_LINEINFO

  At build time, a host tool (scripts/gen_lineinfo) reads DWARF
  .debug_line from vmlinux, extracts address-to-file:line mappings,
  and embeds them as sorted lookup tables in .rodata.  At runtime,
  kallsyms_lookup_lineinfo() binary-searches the table and
  __sprint_symbol() appends "(file:line)" to each stack frame.
  NMI/panic-safe (no locks, no allocations), KASLR-compatible.

Patch 2: CONFIG_KALLSYMS_LINEINFO_MODULES

  Extends lineinfo to loadable modules.  Each .ko gets a .mod_lineinfo
  section embedded at build time.  The module loader picks it up at load
  time.  Same zero-allocation, NMI-safe lookup.

Patch 3: delta compression

  Block-indexed delta-encoding with ULEB128 varints, implementing
  the approach suggested by Juergen Gross in the RFC review.  Reduces
  overhead from ~44 MiB to ~11 MiB (~3.7 bytes/entry), addressing the
  primary size concern from the RFC.

Example output
==============

  [   11.206749]  dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:94)
  [   11.207403]  vpanic+0x36e/0x620 (kernel/panic.c:650)
  [   11.209324]  panic+0xc9/0xd0 (kernel/panic.c:787)
  [   11.213312]  sysrq_handle_crash+0x1a/0x20 (drivers/tty/sysrq.c:154)
  [   11.214005]  __handle_sysrq.cold+0x66/0x256 (drivers/tty/sysrq.c:611)
  [   11.214712]  write_sysrq_trigger+0x65/0x80 (drivers/tty/sysrq.c:1221)
  [   11.215424]  proc_reg_write+0x1bd/0x3c0 (fs/proc/inode.c:330)
  [   11.216061]  vfs_write+0x1c6/0xff0 (fs/read_write.c:686)
  [   11.218848]  ksys_write+0xfa/0x200 (fs/read_write.c:740)
  [   11.222394]  do_syscall_64+0xf3/0x690 (arch/x86/entry/syscall_64.c:63)

Size impact
===========

With delta compression, a defconfig+debug x86_64 build adds ~11 MiB
to the kernel image (~3.7 bytes per DWARF line entry).  This is a
fraction of the cost of shipping full DWARF debug info (hundreds of
MiB), which distros must store and serve for every kernel version.

For distros, maintaining debug symbol repositories is expensive:
storage, mirrors, and CDN bandwidth for hundreds of MiB per kernel
build add up quickly.  An ~11 MiB increase in the kernel image itself
is a modest cost that eliminates the need for users to find, download,
and version-match debuginfo packages just to make a crash report
useful.

For developers, the file:line annotations appear immediately in crash
traces - no post-processing with decode_stacktrace.sh needed.

Changes since RFC
=================

- Added module support (patch 2)
- Added delta compression (patch 3), reducing size from ~44 MiB to
  ~11 MiB, addressing the primary concern from RFC review
- Added documentation (Documentation/admin-guide/kallsyms-lineinfo.rst)
- Added MAINTAINERS entry

Link: https://lore.kernel.org/all/1786920159.1633.1772291851870.JavaMail.zimbra@nod.at/ [1]

Sasha Levin (3):
  kallsyms: embed source file:line info in kernel stack traces
  kallsyms: extend lineinfo to loadable modules
  kallsyms: delta-compress lineinfo tables for ~2.7x size reduction

 Documentation/admin-guide/index.rst           |   1 +
 .../admin-guide/kallsyms-lineinfo.rst         |  97 +++
 MAINTAINERS                                   |   8 +
 include/linux/kallsyms.h                      |  32 +-
 include/linux/mod_lineinfo.h                  | 137 ++++
 include/linux/module.h                        |  19 +
 init/Kconfig                                  |  35 +
 kernel/kallsyms.c                             | 132 ++++
 kernel/kallsyms_internal.h                    |  11 +
 kernel/module/kallsyms.c                      | 156 ++++
 kernel/module/main.c                          |   4 +
 scripts/.gitignore                            |   1 +
 scripts/Makefile                              |   4 +
 scripts/Makefile.modfinal                     |   6 +
 scripts/gen-mod-lineinfo.sh                   |  48 ++
 scripts/gen_lineinfo.c                        | 702 ++++++++++++++++++
 scripts/kallsyms.c                            |  17 +
 scripts/link-vmlinux.sh                       |  70 +-
 18 files changed, 1476 insertions(+), 4 deletions(-)
 create mode 100644 Documentation/admin-guide/kallsyms-lineinfo.rst
 create mode 100644 include/linux/mod_lineinfo.h
 create mode 100755 scripts/gen-mod-lineinfo.sh
 create mode 100644 scripts/gen_lineinfo.c

-- 
2.51.0


^ permalink raw reply	[flat|nested] 21+ messages in thread

* [PATCH 1/3] kallsyms: embed source file:line info in kernel stack traces
  2026-03-03 18:21 [PATCH 0/3] kallsyms: embed source file:line info in kernel stack traces Sasha Levin
@ 2026-03-03 18:21 ` Sasha Levin
  2026-03-04 20:17   ` Helge Deller
                     ` (2 more replies)
  2026-03-03 18:21 ` [PATCH 2/3] kallsyms: extend lineinfo to loadable modules Sasha Levin
  2026-03-03 18:21 ` [PATCH 3/3] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction Sasha Levin
  2 siblings, 3 replies; 21+ messages in thread
From: Sasha Levin @ 2026-03-03 18:21 UTC (permalink / raw)
  To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley
  Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
	Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
	Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, linux-kernel,
	linux-kbuild, linux-modules, linux-doc, Sasha Levin

Add CONFIG_KALLSYMS_LINEINFO, which embeds a compact address-to-line
lookup table in the kernel image so stack traces directly print source
file and line number information:

  root@localhost:~# echo c > /proc/sysrq-trigger
  [   11.201987] sysrq: Trigger a crash
  [   11.202831] Kernel panic - not syncing: sysrq triggered crash
  [   11.206218] Call Trace:
  [   11.206501]  <TASK>
  [   11.206749]  dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:94)
  [   11.207403]  vpanic+0x36e/0x620 (kernel/panic.c:650)
  [   11.208565]  ? __lock_acquire+0x465/0x2240 (kernel/locking/lockdep.c:4674)
  [   11.209324]  panic+0xc9/0xd0 (kernel/panic.c:787)
  [   11.211873]  ? find_held_lock+0x2b/0x80 (kernel/locking/lockdep.c:5350)
  [   11.212597]  ? lock_release+0xd3/0x300 (kernel/locking/lockdep.c:5535)
  [   11.213312]  sysrq_handle_crash+0x1a/0x20 (drivers/tty/sysrq.c:154)
  [   11.214005]  __handle_sysrq.cold+0x66/0x256 (drivers/tty/sysrq.c:611)
  [   11.214712]  write_sysrq_trigger+0x65/0x80 (drivers/tty/sysrq.c:1221)
  [   11.215424]  proc_reg_write+0x1bd/0x3c0 (fs/proc/inode.c:330)
  [   11.216061]  vfs_write+0x1c6/0xff0 (fs/read_write.c:686)
  [   11.218848]  ksys_write+0xfa/0x200 (fs/read_write.c:740)
  [   11.222394]  do_syscall_64+0xf3/0x690 (arch/x86/entry/syscall_64.c:63)
  [   11.223942]  entry_SYSCALL_64_after_hwframe+0x77/0x7f (arch/x86/entry/entry_64.S:121)

At build time, a new host tool (scripts/gen_lineinfo) reads DWARF
.debug_line from vmlinux using libdw (elfutils), extracts all
address-to-file:line mappings, and generates an assembly file with
sorted parallel arrays (offsets from _text, file IDs, and line
numbers). These are linked into vmlinux as .rodata.

At runtime, kallsyms_lookup_lineinfo() does a binary search on the
table and __sprint_symbol() appends "(file:line)" to each stack frame.
The lookup uses offsets from _text so it works with KASLR, requires no
locks or allocations, and is safe in any context including panic.

The feature requires CONFIG_DEBUG_INFO (for DWARF data) and
elfutils (libdw-dev) on the build host.

Memory footprint measured with a 1852-option x86_64 config:

  Table: 4,597,583 entries from 4,841 source files
    lineinfo_addrs[]     4,597,583 x u32  = 17.5 MiB
    lineinfo_file_ids[]  4,597,583 x u16  =  8.8 MiB
    lineinfo_lines[]     4,597,583 x u32  = 17.5 MiB
    file_offsets + filenames              ~  0.1 MiB
    Total .rodata increase:              ~ 44.0 MiB

  vmlinux (stripped):  529 MiB -> 573 MiB  (+44 MiB / +8.3%)

Note: this probably won't be something we roll into "production", but
it might be useful for the average user given the relatively low memory
footprint, in canary deployments for hyperscalers, or by default for
folks who run tests/fuzzing/etc.

Disclaimer: this was vibe coded over an afternoon with an AI coding
assistant.

The .config used for testing is a simple KVM guest configuration for
local development and testing.

Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
 Documentation/admin-guide/index.rst           |   1 +
 .../admin-guide/kallsyms-lineinfo.rst         |  72 +++
 MAINTAINERS                                   |   6 +
 include/linux/kallsyms.h                      |  32 +-
 init/Kconfig                                  |  20 +
 kernel/kallsyms.c                             |  69 +++
 kernel/kallsyms_internal.h                    |  10 +
 scripts/.gitignore                            |   1 +
 scripts/Makefile                              |   3 +
 scripts/gen_lineinfo.c                        | 444 ++++++++++++++++++
 scripts/kallsyms.c                            |  16 +
 scripts/link-vmlinux.sh                       |  66 ++-
 12 files changed, 736 insertions(+), 4 deletions(-)
 create mode 100644 Documentation/admin-guide/kallsyms-lineinfo.rst
 create mode 100644 scripts/gen_lineinfo.c

diff --git a/Documentation/admin-guide/index.rst b/Documentation/admin-guide/index.rst
index b734f8a2a2c48..1801b9880aeb7 100644
--- a/Documentation/admin-guide/index.rst
+++ b/Documentation/admin-guide/index.rst
@@ -73,6 +73,7 @@ problems and bugs in particular.
    ramoops
    dynamic-debug-howto
    init
+   kallsyms-lineinfo
    kdump/index
    perf/index
    pstore-blk
diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
new file mode 100644
index 0000000000000..4dffc18dbcf5a
--- /dev/null
+++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
@@ -0,0 +1,72 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+==================================
+Kallsyms Source Line Info (LINEINFO)
+==================================
+
+Overview
+========
+
+``CONFIG_KALLSYMS_LINEINFO`` embeds DWARF-derived source file and line number
+mappings into the kernel image so that stack traces include
+``(file.c:123)`` annotations next to each symbol.  This makes it significantly
+easier to pinpoint the exact source location during debugging, without needing
+to manually cross-reference addresses with ``addr2line``.
+
+Enabling the Feature
+====================
+
+Enable the following kernel configuration options::
+
+    CONFIG_KALLSYMS=y
+    CONFIG_DEBUG_INFO=y
+    CONFIG_KALLSYMS_LINEINFO=y
+
+Build dependency: the host tool ``scripts/gen_lineinfo`` requires ``libdw``
+from elfutils.  Install the development package:
+
+- Debian/Ubuntu: ``apt install libdw-dev``
+- Fedora/RHEL: ``dnf install elfutils-devel``
+- Arch Linux: ``pacman -S elfutils``
+
+Example Output
+==============
+
+Without ``CONFIG_KALLSYMS_LINEINFO``::
+
+    Call Trace:
+     <TASK>
+     dump_stack_lvl+0x5d/0x80
+     do_syscall_64+0x82/0x190
+     entry_SYSCALL_64_after_hwframe+0x76/0x7e
+
+With ``CONFIG_KALLSYMS_LINEINFO``::
+
+    Call Trace:
+     <TASK>
+     dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:123)
+     do_syscall_64+0x82/0x190 (arch/x86/entry/common.c:52)
+     entry_SYSCALL_64_after_hwframe+0x76/0x7e
+
+Note that assembly routines (such as ``entry_SYSCALL_64_after_hwframe``) are
+not annotated because they lack DWARF debug information.
+
+Memory Overhead
+===============
+
+The lineinfo tables are stored in ``.rodata`` and typically add approximately
+44 MiB to the kernel image for a standard configuration (~4.6 million DWARF
+line entries, ~10 bytes per entry after deduplication).
+
+Known Limitations
+=================
+
+- **vmlinux only**: Only symbols in the core kernel image are annotated.
+  Module symbols are not covered.
+- **4 GiB offset limit**: Address offsets from ``_text`` are stored as 32-bit
+  values.  Entries beyond 4 GiB from ``_text`` are skipped at build time with
+  a warning.
+- **65535 file limit**: Source file IDs are stored as 16-bit values.  Builds
+  with more than 65535 unique source files will fail with an error.
+- **No assembly annotations**: Functions implemented in assembly that lack
+  DWARF ``.debug_line`` data are not annotated.
diff --git a/MAINTAINERS b/MAINTAINERS
index 61bf550fd37c2..ab987e74bb0f5 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14278,6 +14278,12 @@ F:	lib/Kconfig.kmsan
 F:	mm/kmsan/
 F:	scripts/Makefile.kmsan
 
+KALLSYMS LINEINFO
+M:	Sasha Levin <sashal@kernel.org>
+S:	Maintained
+F:	Documentation/admin-guide/kallsyms-lineinfo.rst
+F:	scripts/gen_lineinfo.c
+
 KPROBES
 M:	Naveen N Rao <naveen@kernel.org>
 M:	"David S. Miller" <davem@davemloft.net>
diff --git a/include/linux/kallsyms.h b/include/linux/kallsyms.h
index d5dd54c53ace6..e1d00e1373779 100644
--- a/include/linux/kallsyms.h
+++ b/include/linux/kallsyms.h
@@ -16,10 +16,19 @@
 #include <asm/sections.h>
 
 #define KSYM_NAME_LEN 512
+
+#ifdef CONFIG_KALLSYMS_LINEINFO
+/* Extra space for " (path/to/file.c:12345)" suffix */
+#define KSYM_LINEINFO_LEN 128
+#else
+#define KSYM_LINEINFO_LEN 0
+#endif
+
 #define KSYM_SYMBOL_LEN (sizeof("%s+%#lx/%#lx [%s %s]") + \
 			(KSYM_NAME_LEN - 1) + \
 			2*(BITS_PER_LONG*3/10) + (MODULE_NAME_LEN - 1) + \
-			(BUILD_ID_SIZE_MAX * 2) + 1)
+			(BUILD_ID_SIZE_MAX * 2) + 1 + \
+			KSYM_LINEINFO_LEN)
 
 struct cred;
 struct module;
@@ -96,6 +105,19 @@ extern int sprint_backtrace_build_id(char *buffer, unsigned long address);
 
 int lookup_symbol_name(unsigned long addr, char *symname);
 
+#ifdef CONFIG_KALLSYMS_LINEINFO
+bool kallsyms_lookup_lineinfo(unsigned long addr, unsigned long sym_start,
+			      const char **file, unsigned int *line);
+#else
+static inline bool kallsyms_lookup_lineinfo(unsigned long addr,
+					    unsigned long sym_start,
+					    const char **file,
+					    unsigned int *line)
+{
+	return false;
+}
+#endif
+
 #else /* !CONFIG_KALLSYMS */
 
 static inline unsigned long kallsyms_lookup_name(const char *name)
@@ -164,6 +186,14 @@ static inline int kallsyms_on_each_match_symbol(int (*fn)(void *, unsigned long)
 {
 	return -EOPNOTSUPP;
 }
+
+static inline bool kallsyms_lookup_lineinfo(unsigned long addr,
+					    unsigned long sym_start,
+					    const char **file,
+					    unsigned int *line)
+{
+	return false;
+}
 #endif /*CONFIG_KALLSYMS*/
 
 static inline void print_ip_sym(const char *loglvl, unsigned long ip)
diff --git a/init/Kconfig b/init/Kconfig
index b55deae9256c7..c39f27e6393a8 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2050,6 +2050,26 @@ config KALLSYMS_ALL
 
 	  Say N unless you really need all symbols, or kernel live patching.
 
+config KALLSYMS_LINEINFO
+	bool "Embed source file:line information in stack traces"
+	depends on KALLSYMS && DEBUG_INFO
+	help
+	  Embeds an address-to-source-line mapping table in the kernel
+	  image so that stack traces directly include file:line information,
+	  similar to what scripts/decode_stacktrace.sh provides but without
+	  needing external tools or a vmlinux with debug info at runtime.
+
+	  When enabled, stack traces will look like:
+
+	    kmem_cache_alloc_noprof+0x60/0x630 (mm/slub.c:3456)
+	    anon_vma_clone+0x2ed/0xcf0 (mm/rmap.c:412)
+
+	  This requires elfutils (libdw-dev/elfutils-devel) on the build host.
+	  Adds approximately 44MB to a typical kernel image (10 bytes per
+	  DWARF line-table entry, ~4.6M entries for a typical config).
+
+	  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 aec2f06858afd..2b9c9d6322a3e 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -467,6 +467,62 @@ static int append_buildid(char *buffer,   const char *modname,
 
 #endif /* CONFIG_STACKTRACE_BUILD_ID */
 
+#ifdef CONFIG_KALLSYMS_LINEINFO
+bool kallsyms_lookup_lineinfo(unsigned long addr, unsigned long sym_start,
+			      const char **file, unsigned int *line)
+{
+	unsigned long long raw_offset;
+	unsigned int offset, low, high, mid, file_id;
+	unsigned long line_addr;
+
+	if (!lineinfo_num_entries)
+		return false;
+
+	/* Compute offset from _text */
+	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 largest entry <= offset */
+	low = 0;
+	high = lineinfo_num_entries;
+	while (low < high) {
+		mid = low + (high - low) / 2;
+		if (lineinfo_addrs[mid] <= offset)
+			low = mid + 1;
+		else
+			high = mid;
+	}
+
+	if (low == 0)
+		return false;
+	low--;
+
+	/*
+	 * Validate that the matched lineinfo entry belongs to the same
+	 * symbol.  Without this check, assembly routines or other
+	 * functions lacking DWARF data would inherit the file:line of
+	 * a preceding C function.
+	 */
+	line_addr = (unsigned long)_text + lineinfo_addrs[low];
+	if (line_addr < sym_start)
+		return false;
+
+	file_id = lineinfo_file_ids[low];
+	*line = lineinfo_lines[low];
+
+	if (file_id >= lineinfo_num_files)
+		return false;
+
+	*file = &lineinfo_filenames[lineinfo_file_offsets[file_id]];
+	return true;
+}
+#endif /* CONFIG_KALLSYMS_LINEINFO */
+
 /* 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)
@@ -497,6 +553,19 @@ static int __sprint_symbol(char *buffer, unsigned long address,
 		len += sprintf(buffer + len, "]");
 	}
 
+#ifdef CONFIG_KALLSYMS_LINEINFO
+	if (!modname) {
+		const char *li_file;
+		unsigned int li_line;
+		unsigned long sym_start = address - offset;
+
+		if (kallsyms_lookup_lineinfo(address, sym_start,
+					     &li_file, &li_line))
+			len += snprintf(buffer + len, KSYM_SYMBOL_LEN - len,
+					" (%s:%u)", li_file, li_line);
+	}
+#endif
+
 	return len;
 }
 
diff --git a/kernel/kallsyms_internal.h b/kernel/kallsyms_internal.h
index 81a867dbe57d4..868a1d5035212 100644
--- a/kernel/kallsyms_internal.h
+++ b/kernel/kallsyms_internal.h
@@ -15,4 +15,14 @@ extern const u16 kallsyms_token_index[];
 extern const unsigned int kallsyms_markers[];
 extern const u8 kallsyms_seqs_of_names[];
 
+#ifdef CONFIG_KALLSYMS_LINEINFO
+extern const u32 lineinfo_num_entries;
+extern const u32 lineinfo_addrs[];
+extern const u16 lineinfo_file_ids[];
+extern const u32 lineinfo_lines[];
+extern const u32 lineinfo_num_files;
+extern const u32 lineinfo_file_offsets[];
+extern const char lineinfo_filenames[];
+#endif
+
 #endif // LINUX_KALLSYMS_INTERNAL_H_
diff --git a/scripts/.gitignore b/scripts/.gitignore
index 4215c2208f7e4..e175714c18b61 100644
--- a/scripts/.gitignore
+++ b/scripts/.gitignore
@@ -1,5 +1,6 @@
 # SPDX-License-Identifier: GPL-2.0-only
 /asn1_compiler
+/gen_lineinfo
 /gen_packed_field_checks
 /generate_rust_target
 /insert-sys-cert
diff --git a/scripts/Makefile b/scripts/Makefile
index 0941e5ce7b575..ffe89875b3295 100644
--- a/scripts/Makefile
+++ b/scripts/Makefile
@@ -4,6 +4,7 @@
 # the kernel for the build process.
 
 hostprogs-always-$(CONFIG_KALLSYMS)			+= kallsyms
+hostprogs-always-$(CONFIG_KALLSYMS_LINEINFO)		+= gen_lineinfo
 hostprogs-always-$(BUILD_C_RECORDMCOUNT)		+= recordmcount
 hostprogs-always-$(CONFIG_BUILDTIME_TABLE_SORT)		+= sorttable
 hostprogs-always-$(CONFIG_ASN1)				+= asn1_compiler
@@ -36,6 +37,8 @@ HOSTLDLIBS_sorttable = -lpthread
 HOSTCFLAGS_asn1_compiler.o = -I$(srctree)/include
 HOSTCFLAGS_sign-file.o = $(shell $(HOSTPKG_CONFIG) --cflags libcrypto 2> /dev/null)
 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)
 
 ifdef CONFIG_UNWINDER_ORC
 ifeq ($(ARCH),x86_64)
diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c
new file mode 100644
index 0000000000000..9eebfaca5857c
--- /dev/null
+++ b/scripts/gen_lineinfo.c
@@ -0,0 +1,444 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * gen_lineinfo.c - Generate address-to-source-line lookup tables from DWARF
+ *
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Reads DWARF .debug_line from a vmlinux ELF file and outputs an assembly
+ * file containing sorted lookup tables that the kernel uses to annotate
+ * stack traces with source file:line information.
+ *
+ * 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>
+
+static unsigned int skipped_overflow;
+
+struct line_entry {
+	unsigned int offset;	/* offset from _text */
+	unsigned int file_id;
+	unsigned int line;
+};
+
+struct file_entry {
+	char *name;
+	unsigned int id;
+	unsigned int str_offset;
+};
+
+static struct line_entry *entries;
+static unsigned int num_entries;
+static unsigned int entries_capacity;
+
+static struct file_entry *files;
+static unsigned int num_files;
+static unsigned int files_capacity;
+
+#define FILE_HASH_BITS 13
+#define FILE_HASH_SIZE (1 << FILE_HASH_BITS)
+
+struct file_hash_entry {
+	const char *name;
+	unsigned int id;
+};
+
+static struct file_hash_entry file_hash[FILE_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 & (FILE_HASH_SIZE - 1);
+}
+
+static void add_entry(unsigned int offset, unsigned int file_id,
+		      unsigned int line)
+{
+	if (num_entries >= entries_capacity) {
+		entries_capacity = entries_capacity ? entries_capacity * 2 : 65536;
+		entries = realloc(entries, entries_capacity * sizeof(*entries));
+		if (!entries) {
+			fprintf(stderr, "out of memory\n");
+			exit(1);
+		}
+	}
+	entries[num_entries].offset = offset;
+	entries[num_entries].file_id = file_id;
+	entries[num_entries].line = line;
+	num_entries++;
+}
+
+static unsigned int find_or_add_file(const char *name)
+{
+	unsigned int h = hash_str(name);
+
+	/* Open-addressing lookup with linear probing */
+	while (file_hash[h].name) {
+		if (!strcmp(file_hash[h].name, name))
+			return file_hash[h].id;
+		h = (h + 1) & (FILE_HASH_SIZE - 1);
+	}
+
+	if (num_files >= 65535) {
+		fprintf(stderr,
+			"gen_lineinfo: too many source files (%u > 65535)\n",
+			num_files);
+		exit(1);
+	}
+
+	if (num_files >= files_capacity) {
+		files_capacity = files_capacity ? files_capacity * 2 : 4096;
+		files = realloc(files, files_capacity * sizeof(*files));
+		if (!files) {
+			fprintf(stderr, "out of memory\n");
+			exit(1);
+		}
+	}
+	files[num_files].name = strdup(name);
+	files[num_files].id = num_files;
+
+	/* Insert into hash table (points to files[] entry) */
+	file_hash[h].name = files[num_files].name;
+	file_hash[h].id = num_files;
+
+	num_files++;
+	return num_files - 1;
+}
+
+/*
+ * Strip a filename to a kernel-relative path.
+ *
+ * For absolute paths, strip the comp_dir prefix (from DWARF) to get
+ * a kernel-tree-relative path, or fall back to the basename.
+ */
+static const char *make_relative(const char *path, const char *comp_dir)
+{
+	const char *p;
+
+	/* If already relative, use as-is */
+	if (path[0] != '/')
+		return path;
+
+	/* comp_dir from DWARF is the most reliable method */
+	if (comp_dir) {
+		size_t len = strlen(comp_dir);
+
+		if (!strncmp(path, comp_dir, len) && path[len] == '/')
+			return path + len + 1;
+	}
+
+	/* Fall back to basename */
+	p = strrchr(path, '/');
+	return p ? p + 1 : path;
+}
+
+static int compare_entries(const void *a, const void *b)
+{
+	const struct line_entry *ea = a;
+	const struct line_entry *eb = b;
+
+	if (ea->offset != eb->offset)
+		return ea->offset < eb->offset ? -1 : 1;
+	if (ea->file_id != eb->file_id)
+		return ea->file_id < eb->file_id ? -1 : 1;
+	if (ea->line != eb->line)
+		return ea->line < eb->line ? -1 : 1;
+	return 0;
+}
+
+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 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;
+		Dwarf_Lines *lines;
+		size_t nlines;
+		Dwarf_Attribute attr;
+		const char *comp_dir = NULL;
+
+		if (!dwarf_offdie(dwarf, off + hdr_size, &cudie))
+			goto next;
+
+		if (dwarf_attr(&cudie, DW_AT_comp_dir, &attr))
+			comp_dir = dwarf_formstring(&attr);
+
+		if (dwarf_getsrclines(&cudie, &lines, &nlines) != 0)
+			goto next;
+
+		for (size_t i = 0; i < nlines; i++) {
+			Dwarf_Line *line = dwarf_onesrcline(lines, i);
+			Dwarf_Addr addr;
+			const char *src;
+			const char *rel;
+			unsigned int file_id, loffset;
+			int lineno;
+
+			if (!line)
+				continue;
+
+			if (dwarf_lineaddr(line, &addr) != 0)
+				continue;
+			if (dwarf_lineno(line, &lineno) != 0)
+				continue;
+			if (lineno == 0)
+				continue;
+
+			src = dwarf_linesrc(line, NULL, NULL);
+			if (!src)
+				continue;
+
+			if (addr < text_addr)
+				continue;
+
+			{
+				unsigned long long raw_offset = addr - text_addr;
+
+				if (raw_offset > UINT_MAX) {
+					skipped_overflow++;
+					continue;
+				}
+				loffset = (unsigned int)raw_offset;
+			}
+
+			rel = make_relative(src, comp_dir);
+			file_id = find_or_add_file(rel);
+
+			add_entry(loffset, file_id, (unsigned int)lineno);
+		}
+next:
+		off = next_off;
+	}
+}
+
+static void deduplicate(void)
+{
+	unsigned int i, j;
+
+	if (num_entries < 2)
+		return;
+
+	/* Sort by offset, then file_id, then line for stability */
+	qsort(entries, num_entries, sizeof(*entries), compare_entries);
+
+	/*
+	 * Remove duplicate entries:
+	 * - Same offset: keep first (deterministic from stable sort keys)
+	 * - Same file:line as previous kept entry: redundant for binary
+	 *   search -- any address between them resolves to the earlier one
+	 */
+	j = 0;
+	for (i = 1; i < num_entries; i++) {
+		if (entries[i].offset == entries[j].offset)
+			continue;
+		if (entries[i].file_id == entries[j].file_id &&
+		    entries[i].line == entries[j].line)
+			continue;
+		j++;
+		if (j != i)
+			entries[j] = entries[i];
+	}
+	num_entries = j + 1;
+}
+
+static void compute_file_offsets(void)
+{
+	unsigned int offset = 0;
+
+	for (unsigned int i = 0; i < num_files; i++) {
+		files[i].str_offset = offset;
+		offset += strlen(files[i].name) + 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)
+{
+	printf("/* SPDX-License-Identifier: GPL-2.0 */\n");
+	printf("/*\n");
+	printf(" * Automatically generated by scripts/gen_lineinfo\n");
+	printf(" * Do not edit.\n");
+	printf(" */\n\n");
+
+	printf("\t.section .rodata, \"a\"\n\n");
+
+	/* Number of entries */
+	printf("\t.globl lineinfo_num_entries\n");
+	printf("\t.balign 4\n");
+	printf("lineinfo_num_entries:\n");
+	printf("\t.long %u\n\n", num_entries);
+
+	/* Number of files */
+	printf("\t.globl lineinfo_num_files\n");
+	printf("\t.balign 4\n");
+	printf("lineinfo_num_files:\n");
+	printf("\t.long %u\n\n", num_files);
+
+	/* Sorted address offsets from _text */
+	printf("\t.globl lineinfo_addrs\n");
+	printf("\t.balign 4\n");
+	printf("lineinfo_addrs:\n");
+	for (unsigned int i = 0; i < num_entries; i++)
+		printf("\t.long 0x%x\n", entries[i].offset);
+	printf("\n");
+
+	/* File IDs, parallel to addrs (u16 -- supports up to 65535 files) */
+	printf("\t.globl lineinfo_file_ids\n");
+	printf("\t.balign 2\n");
+	printf("lineinfo_file_ids:\n");
+	for (unsigned int i = 0; i < num_entries; i++)
+		printf("\t.short %u\n", entries[i].file_id);
+	printf("\n");
+
+	/* Line numbers, parallel to addrs */
+	printf("\t.globl lineinfo_lines\n");
+	printf("\t.balign 4\n");
+	printf("lineinfo_lines:\n");
+	for (unsigned int i = 0; i < num_entries; i++)
+		printf("\t.long %u\n", entries[i].line);
+	printf("\n");
+
+	/* File string offset table */
+	printf("\t.globl lineinfo_file_offsets\n");
+	printf("\t.balign 4\n");
+	printf("lineinfo_file_offsets:\n");
+	for (unsigned int i = 0; i < num_files; i++)
+		printf("\t.long %u\n", files[i].str_offset);
+	printf("\n");
+
+	/* Concatenated NUL-terminated filenames */
+	printf("\t.globl lineinfo_filenames\n");
+	printf("lineinfo_filenames:\n");
+	for (unsigned int i = 0; i < num_files; i++)
+		print_escaped_asciz(files[i].name);
+	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>\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);
+
+	if (skipped_overflow)
+		fprintf(stderr,
+			"lineinfo: warning: %u entries skipped (offset > 4 GiB from _text)\n",
+			skipped_overflow);
+
+	deduplicate();
+	compute_file_offsets();
+
+	fprintf(stderr, "lineinfo: %u entries, %u files\n",
+		num_entries, num_files);
+
+	output_assembly();
+
+	dwarf_end(dwarf);
+	elf_end(elf);
+	close(fd);
+
+	/* Cleanup */
+	free(entries);
+	for (unsigned int i = 0; i < num_files; i++)
+		free(files[i].name);
+	free(files);
+
+	return 0;
+}
diff --git a/scripts/kallsyms.c b/scripts/kallsyms.c
index 37d5c095ad22a..42662c4fbc6c9 100644
--- a/scripts/kallsyms.c
+++ b/scripts/kallsyms.c
@@ -78,6 +78,17 @@ static char *sym_name(const struct sym_entry *s)
 
 static bool is_ignored_symbol(const char *name, char type)
 {
+	/* Ignore lineinfo symbols for kallsyms pass stability */
+	static const char * const lineinfo_syms[] = {
+		"lineinfo_addrs",
+		"lineinfo_file_ids",
+		"lineinfo_file_offsets",
+		"lineinfo_filenames",
+		"lineinfo_lines",
+		"lineinfo_num_entries",
+		"lineinfo_num_files",
+	};
+
 	if (type == 'u' || type == 'n')
 		return true;
 
@@ -90,6 +101,11 @@ static bool is_ignored_symbol(const char *name, char type)
 			return true;
 	}
 
+	for (size_t i = 0; i < ARRAY_SIZE(lineinfo_syms); i++) {
+		if (!strcmp(name, lineinfo_syms[i]))
+			return true;
+	}
+
 	return false;
 }
 
diff --git a/scripts/link-vmlinux.sh b/scripts/link-vmlinux.sh
index f99e196abeea4..640209f2e9eb9 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} ${btf_vmlinux_bin_o} ${arch_vmlinux_o} ${ldlibs}
+		${kallsymso} ${lineinfo_o} ${btf_vmlinux_bin_o} ${arch_vmlinux_o} ${ldlibs}
 }
 
 # Create ${2}.o file with all symbols from the ${1} object file
@@ -129,6 +129,26 @@ kallsyms()
 	kallsymso=${2}.o
 }
 
+# Generate lineinfo tables from DWARF debug info in a temporary vmlinux.
+# ${1} - temporary vmlinux with debug info
+# Output: sets lineinfo_o to the generated .o file
+gen_lineinfo()
+{
+	info LINEINFO .tmp_lineinfo.S
+	if ! scripts/gen_lineinfo "${1}" > .tmp_lineinfo.S; then
+		echo >&2 "Failed to generate lineinfo from ${1}"
+		echo >&2 "Try to disable CONFIG_KALLSYMS_LINEINFO"
+		exit 1
+	fi
+
+	info AS .tmp_lineinfo.o
+	${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \
+	      ${KBUILD_AFLAGS} ${KBUILD_AFLAGS_KERNEL} \
+	      -c -o .tmp_lineinfo.o .tmp_lineinfo.S
+
+	lineinfo_o=.tmp_lineinfo.o
+}
+
 # Perform kallsyms for the given temporary vmlinux.
 sysmap_and_kallsyms()
 {
@@ -155,6 +175,7 @@ sorttable()
 cleanup()
 {
 	rm -f .btf.*
+	rm -f .tmp_lineinfo.*
 	rm -f .tmp_vmlinux.nm-sort
 	rm -f System.map
 	rm -f vmlinux
@@ -183,6 +204,7 @@ fi
 btf_vmlinux_bin_o=
 btfids_vmlinux=
 kallsymso=
+lineinfo_o=
 strip_debug=
 generate_map=
 
@@ -198,10 +220,44 @@ if is_enabled CONFIG_KALLSYMS; then
 	kallsyms .tmp_vmlinux0.syms .tmp_vmlinux0.kallsyms
 fi
 
+if is_enabled CONFIG_KALLSYMS_LINEINFO; then
+	# Generate a dummy empty lineinfo object for the initial link,
+	# same pattern as the dummy kallsyms above.  The real lineinfo
+	# is generated from .tmp_vmlinux1 after it has been linked with
+	# debug info.
+	cat > .tmp_lineinfo.S <<'EOAS'
+	.section .rodata, "a"
+	.globl lineinfo_num_entries
+	.balign 4
+lineinfo_num_entries:
+	.long 0
+	.globl lineinfo_num_files
+	.balign 4
+lineinfo_num_files:
+	.long 0
+	.globl lineinfo_addrs
+lineinfo_addrs:
+	.globl lineinfo_file_ids
+lineinfo_file_ids:
+	.globl lineinfo_lines
+lineinfo_lines:
+	.globl lineinfo_file_offsets
+lineinfo_file_offsets:
+	.globl lineinfo_filenames
+lineinfo_filenames:
+EOAS
+	${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \
+	      ${KBUILD_AFLAGS} ${KBUILD_AFLAGS_KERNEL} \
+	      -c -o .tmp_lineinfo.o .tmp_lineinfo.S
+	lineinfo_o=.tmp_lineinfo.o
+fi
+
 if is_enabled CONFIG_KALLSYMS || is_enabled CONFIG_DEBUG_INFO_BTF; then
 
-	# The kallsyms linking does not need debug symbols, but the BTF does.
-	if ! is_enabled CONFIG_DEBUG_INFO_BTF; then
+	# The kallsyms linking does not need debug symbols, but BTF and
+	# lineinfo generation do.
+	if ! is_enabled CONFIG_DEBUG_INFO_BTF &&
+	   ! is_enabled CONFIG_KALLSYMS_LINEINFO; then
 		strip_debug=1
 	fi
 
@@ -219,6 +275,10 @@ if is_enabled CONFIG_DEBUG_INFO_BTF; then
 	btfids_vmlinux=.tmp_vmlinux1.BTF_ids
 fi
 
+if is_enabled CONFIG_KALLSYMS_LINEINFO; then
+	gen_lineinfo .tmp_vmlinux1
+fi
+
 if is_enabled CONFIG_KALLSYMS; then
 
 	# kallsyms support
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 21+ messages in thread

* [PATCH 2/3] kallsyms: extend lineinfo to loadable modules
  2026-03-03 18:21 [PATCH 0/3] kallsyms: embed source file:line info in kernel stack traces Sasha Levin
  2026-03-03 18:21 ` [PATCH 1/3] " Sasha Levin
@ 2026-03-03 18:21 ` Sasha Levin
  2026-03-03 18:21 ` [PATCH 3/3] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction Sasha Levin
  2 siblings, 0 replies; 21+ messages in thread
From: Sasha Levin @ 2026-03-03 18:21 UTC (permalink / raw)
  To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley
  Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
	Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
	Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, linux-kernel,
	linux-kbuild, linux-modules, linux-doc, Sasha Levin

Add CONFIG_KALLSYMS_LINEINFO_MODULES, which extends the
CONFIG_KALLSYMS_LINEINFO feature to loadable kernel modules.

At build time, each .ko is post-processed by scripts/gen-mod-lineinfo.sh
(modeled on gen-btf.sh) which runs scripts/gen_lineinfo --module on the
.ko, generates a .mod_lineinfo section containing a compact binary table
of .text-relative offsets, file IDs, line numbers, and filenames, and
embeds it back into the .ko via objcopy.

At runtime, module_lookup_lineinfo() performs a binary search on the
module's .mod_lineinfo section, and __sprint_symbol() calls it for
addresses that fall within a module.  The lookup is NMI/panic-safe
(no locks, no allocations) - the data lives in read-only module memory
and is freed automatically when the module is unloaded.

Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
 .../admin-guide/kallsyms-lineinfo.rst         |  40 ++++-
 MAINTAINERS                                   |   2 +
 include/linux/mod_lineinfo.h                  |  68 +++++++
 include/linux/module.h                        |  19 ++
 init/Kconfig                                  |  13 ++
 kernel/kallsyms.c                             |  22 ++-
 kernel/module/kallsyms.c                      |  95 ++++++++++
 kernel/module/main.c                          |   4 +
 scripts/Makefile                              |   1 +
 scripts/Makefile.modfinal                     |   6 +
 scripts/gen-mod-lineinfo.sh                   |  48 +++++
 scripts/gen_lineinfo.c                        | 166 ++++++++++++++++--
 12 files changed, 458 insertions(+), 26 deletions(-)
 create mode 100644 include/linux/mod_lineinfo.h
 create mode 100755 scripts/gen-mod-lineinfo.sh

diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
index 4dffc18dbcf5a..21450569d5324 100644
--- a/Documentation/admin-guide/kallsyms-lineinfo.rst
+++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
@@ -51,22 +51,46 @@ With ``CONFIG_KALLSYMS_LINEINFO``::
 Note that assembly routines (such as ``entry_SYSCALL_64_after_hwframe``) are
 not annotated because they lack DWARF debug information.
 
+Module Support
+==============
+
+``CONFIG_KALLSYMS_LINEINFO_MODULES`` extends the feature to loadable kernel
+modules.  When enabled, each ``.ko`` is post-processed at build time to embed
+a ``.mod_lineinfo`` section containing the same kind of address-to-source
+mapping.
+
+Enable in addition to the base options::
+
+    CONFIG_MODULES=y
+    CONFIG_KALLSYMS_LINEINFO_MODULES=y
+
+Stack traces from module code will then include annotations::
+
+    my_driver_func+0x30/0x100 [my_driver] (drivers/foo/bar.c:123)
+
+The ``.mod_lineinfo`` section is loaded into read-only module memory alongside
+the module text.  No additional runtime memory allocation is required; the data
+is freed when the module is unloaded.
+
 Memory Overhead
 ===============
 
-The lineinfo tables are stored in ``.rodata`` and typically add approximately
-44 MiB to the kernel image for a standard configuration (~4.6 million DWARF
-line entries, ~10 bytes per entry after deduplication).
+The vmlinux lineinfo tables are stored in ``.rodata`` and typically add
+approximately 44 MiB to the kernel image for a standard configuration
+(~4.6 million DWARF line entries, ~10 bytes per entry after deduplication).
+
+Per-module lineinfo adds approximately 10 bytes per DWARF line entry to each
+``.ko`` file.
 
 Known Limitations
 =================
 
-- **vmlinux only**: Only symbols in the core kernel image are annotated.
-  Module symbols are not covered.
-- **4 GiB offset limit**: Address offsets from ``_text`` are stored as 32-bit
-  values.  Entries beyond 4 GiB from ``_text`` are skipped at build time with
-  a warning.
+- **4 GiB offset limit**: Address offsets from ``_text`` (vmlinux) or
+  ``.text`` base (modules) are stored as 32-bit values.  Entries beyond
+  4 GiB are skipped at build time with a warning.
 - **65535 file limit**: Source file IDs are stored as 16-bit values.  Builds
   with more than 65535 unique source files will fail with an error.
 - **No assembly annotations**: Functions implemented in assembly that lack
   DWARF ``.debug_line`` data are not annotated.
+- **No init text**: For modules, functions in ``.init.text`` are not annotated
+  because that memory is freed after module initialization.
diff --git a/MAINTAINERS b/MAINTAINERS
index ab987e74bb0f5..d04abafd9eb77 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14282,6 +14282,8 @@ KALLSYMS LINEINFO
 M:	Sasha Levin <sashal@kernel.org>
 S:	Maintained
 F:	Documentation/admin-guide/kallsyms-lineinfo.rst
+F:	include/linux/mod_lineinfo.h
+F:	scripts/gen-mod-lineinfo.sh
 F:	scripts/gen_lineinfo.c
 
 KPROBES
diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h
new file mode 100644
index 0000000000000..d62e9608f0f82
--- /dev/null
+++ b/include/linux/mod_lineinfo.h
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * mod_lineinfo.h - Binary format for per-module source line information
+ *
+ * This header defines the layout of the .mod_lineinfo section embedded
+ * in loadable kernel modules.  It is dual-use: included from both the
+ * kernel and the userspace gen_lineinfo tool.
+ *
+ * Section layout (all values in target-native endianness):
+ *
+ *   struct mod_lineinfo_header     (16 bytes)
+ *   u32 addrs[num_entries]         -- offsets from .text base, sorted
+ *   u16 file_ids[num_entries]      -- parallel to addrs
+ *   <2-byte pad if num_entries is odd>
+ *   u32 lines[num_entries]         -- parallel to addrs
+ *   u32 file_offsets[num_files]    -- byte offset into filenames[]
+ *   char filenames[filenames_size] -- concatenated NUL-terminated strings
+ */
+#ifndef _LINUX_MOD_LINEINFO_H
+#define _LINUX_MOD_LINEINFO_H
+
+#ifdef __KERNEL__
+#include <linux/types.h>
+#else
+#include <stdint.h>
+typedef uint32_t u32;
+typedef uint16_t u16;
+#endif
+
+struct mod_lineinfo_header {
+	u32 num_entries;
+	u32 num_files;
+	u32 filenames_size;	/* total bytes of concatenated filenames */
+	u32 reserved;		/* padding, must be 0 */
+};
+
+/* Offset helpers: compute byte offset from start of section to each array */
+
+static inline u32 mod_lineinfo_addrs_off(void)
+{
+	return sizeof(struct mod_lineinfo_header);
+}
+
+static inline u32 mod_lineinfo_file_ids_off(u32 num_entries)
+{
+	return mod_lineinfo_addrs_off() + num_entries * sizeof(u32);
+}
+
+static inline u32 mod_lineinfo_lines_off(u32 num_entries)
+{
+	/* u16 file_ids[] may need 2-byte padding to align lines[] to 4 bytes */
+	u32 off = mod_lineinfo_file_ids_off(num_entries) +
+		  num_entries * sizeof(u16);
+	return (off + 3) & ~3u;
+}
+
+static inline u32 mod_lineinfo_file_offsets_off(u32 num_entries)
+{
+	return mod_lineinfo_lines_off(num_entries) + num_entries * sizeof(u32);
+}
+
+static inline u32 mod_lineinfo_filenames_off(u32 num_entries, u32 num_files)
+{
+	return mod_lineinfo_file_offsets_off(num_entries) +
+	       num_files * sizeof(u32);
+}
+
+#endif /* _LINUX_MOD_LINEINFO_H */
diff --git a/include/linux/module.h b/include/linux/module.h
index 14f391b186c6d..1c5840e736ec7 100644
--- a/include/linux/module.h
+++ b/include/linux/module.h
@@ -508,6 +508,10 @@ struct module {
 	void *btf_data;
 	void *btf_base_data;
 #endif
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+	void *lineinfo_data;		/* .mod_lineinfo section in MOD_RODATA */
+	unsigned int lineinfo_data_size;
+#endif
 #ifdef CONFIG_JUMP_LABEL
 	struct jump_entry *jump_entries;
 	unsigned int num_jump_entries;
@@ -1021,6 +1025,21 @@ static inline unsigned long find_kallsyms_symbol_value(struct module *mod,
 
 #endif  /* CONFIG_MODULES && CONFIG_KALLSYMS */
 
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
+			    unsigned long sym_start,
+			    const char **file, unsigned int *line);
+#else
+static inline bool module_lookup_lineinfo(struct module *mod,
+					  unsigned long addr,
+					  unsigned long sym_start,
+					  const char **file,
+					  unsigned int *line)
+{
+	return false;
+}
+#endif
+
 /* Define __free(module_put) macro for struct module *. */
 DEFINE_FREE(module_put, struct module *, if (_T) module_put(_T))
 
diff --git a/init/Kconfig b/init/Kconfig
index c39f27e6393a8..bf53275bc405a 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2070,6 +2070,19 @@ config KALLSYMS_LINEINFO
 
 	  If unsure, say N.
 
+config KALLSYMS_LINEINFO_MODULES
+	bool "Embed source file:line information in module stack traces"
+	depends on KALLSYMS_LINEINFO && MODULES
+	help
+	  Extends KALLSYMS_LINEINFO to loadable kernel modules.  Each .ko
+	  gets a lineinfo table generated from its DWARF data at build time,
+	  so stack traces from module code include (file.c:123) annotations.
+
+	  Requires elfutils (libdw-dev/elfutils-devel) on the build host.
+	  Increases .ko sizes by approximately 10 bytes per DWARF line entry.
+
+	  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 2b9c9d6322a3e..cea74992e5427 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -554,13 +554,27 @@ static int __sprint_symbol(char *buffer, unsigned long address,
 	}
 
 #ifdef CONFIG_KALLSYMS_LINEINFO
-	if (!modname) {
+	{
 		const char *li_file;
 		unsigned int li_line;
 		unsigned long sym_start = address - offset;
-
-		if (kallsyms_lookup_lineinfo(address, sym_start,
-					     &li_file, &li_line))
+		bool found = false;
+
+		if (!modname)
+			found = kallsyms_lookup_lineinfo(address, sym_start,
+							 &li_file, &li_line);
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+		else {
+			struct module *mod = __module_address(address);
+
+			if (mod)
+				found = module_lookup_lineinfo(mod, address,
+							      sym_start,
+							      &li_file,
+							      &li_line);
+		}
+#endif
+		if (found)
 			len += snprintf(buffer + len, KSYM_SYMBOL_LEN - len,
 					" (%s:%u)", li_file, li_line);
 	}
diff --git a/kernel/module/kallsyms.c b/kernel/module/kallsyms.c
index 0fc11e45df9b9..7af414bd65e79 100644
--- a/kernel/module/kallsyms.c
+++ b/kernel/module/kallsyms.c
@@ -494,3 +494,98 @@ int module_kallsyms_on_each_symbol(const char *modname,
 	mutex_unlock(&module_mutex);
 	return ret;
 }
+
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+#include <linux/mod_lineinfo.h>
+
+/*
+ * Look up source file:line for an address within a loaded module.
+ * Uses the .mod_lineinfo section embedded in the .ko at build time.
+ *
+ * Safe in NMI/panic context: no locks, no allocations.
+ * Caller must hold RCU read lock (or be in a context where the module
+ * cannot be unloaded).
+ */
+bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
+			    unsigned long sym_start,
+			    const char **file, unsigned int *line)
+{
+	const struct mod_lineinfo_header *hdr;
+	const void *base;
+	const u32 *addrs, *lines, *file_offsets;
+	const u16 *file_ids;
+	const char *filenames;
+	u32 num_entries, num_files, filenames_size;
+	unsigned long text_base;
+	unsigned int offset;
+	unsigned long long raw_offset;
+	unsigned int low, high, mid;
+	u16 file_id;
+
+	base = mod->lineinfo_data;
+	if (!base)
+		return false;
+
+	if (mod->lineinfo_data_size < sizeof(*hdr))
+		return false;
+
+	hdr = base;
+	num_entries = hdr->num_entries;
+	num_files = hdr->num_files;
+	filenames_size = hdr->filenames_size;
+
+	if (num_entries == 0)
+		return false;
+
+	/* Validate section is large enough for all arrays */
+	if (mod->lineinfo_data_size <
+	    mod_lineinfo_filenames_off(num_entries, num_files) + filenames_size)
+		return false;
+
+	addrs = base + mod_lineinfo_addrs_off();
+	file_ids = base + mod_lineinfo_file_ids_off(num_entries);
+	lines = base + mod_lineinfo_lines_off(num_entries);
+	file_offsets = base + mod_lineinfo_file_offsets_off(num_entries);
+	filenames = base + mod_lineinfo_filenames_off(num_entries, num_files);
+
+	/* Compute offset from module .text base */
+	text_base = (unsigned long)mod->mem[MOD_TEXT].base;
+	if (addr < text_base)
+		return false;
+
+	raw_offset = addr - text_base;
+	if (raw_offset > UINT_MAX)
+		return false;
+	offset = (unsigned int)raw_offset;
+
+	/* Binary search for largest entry <= offset */
+	low = 0;
+	high = num_entries;
+	while (low < high) {
+		mid = low + (high - low) / 2;
+		if (addrs[mid] <= offset)
+			low = mid + 1;
+		else
+			high = mid;
+	}
+
+	if (low == 0)
+		return false;
+	low--;
+
+	/* Ensure the matched entry belongs to the same symbol */
+	if (text_base + addrs[low] < sym_start)
+		return false;
+
+	file_id = file_ids[low];
+	if (file_id >= num_files)
+		return false;
+
+	if (file_offsets[file_id] >= filenames_size)
+		return false;
+
+	*file = &filenames[file_offsets[file_id]];
+	*line = lines[low];
+	return true;
+}
+#endif /* CONFIG_KALLSYMS_LINEINFO_MODULES */
diff --git a/kernel/module/main.c b/kernel/module/main.c
index 2bac4c7cd019a..7b6ff9f7411b0 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -2648,6 +2648,10 @@ static int find_module_sections(struct module *mod, struct load_info *info)
 	mod->btf_base_data = any_section_objs(info, ".BTF.base", 1,
 					      &mod->btf_base_data_size);
 #endif
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+	mod->lineinfo_data = any_section_objs(info, ".mod_lineinfo", 1,
+					      &mod->lineinfo_data_size);
+#endif
 #ifdef CONFIG_JUMP_LABEL
 	mod->jump_entries = section_objs(info, "__jump_table",
 					sizeof(*mod->jump_entries),
diff --git a/scripts/Makefile b/scripts/Makefile
index ffe89875b3295..651df2a867ffb 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_LINEINFO_MODULES)	+= gen_lineinfo
 hostprogs-always-$(BUILD_C_RECORDMCOUNT)		+= recordmcount
 hostprogs-always-$(CONFIG_BUILDTIME_TABLE_SORT)		+= sorttable
 hostprogs-always-$(CONFIG_ASN1)				+= asn1_compiler
diff --git a/scripts/Makefile.modfinal b/scripts/Makefile.modfinal
index adcbcde16a071..3941cf624526b 100644
--- a/scripts/Makefile.modfinal
+++ b/scripts/Makefile.modfinal
@@ -46,6 +46,9 @@ quiet_cmd_btf_ko = BTF [M] $@
 		$(CONFIG_SHELL) $(srctree)/scripts/gen-btf.sh --btf_base $(objtree)/vmlinux $@; \
 	fi;
 
+quiet_cmd_lineinfo_ko = LINEINFO [M] $@
+      cmd_lineinfo_ko = $(CONFIG_SHELL) $(srctree)/scripts/gen-mod-lineinfo.sh $@
+
 # Same as newer-prereqs, but allows to exclude specified extra dependencies
 newer_prereqs_except = $(filter-out $(PHONY) $(1),$?)
 
@@ -59,6 +62,9 @@ if_changed_except = $(if $(call newer_prereqs_except,$(2))$(cmd-check),      \
 	+$(call if_changed_except,ld_ko_o,$(objtree)/vmlinux)
 ifdef CONFIG_DEBUG_INFO_BTF_MODULES
 	+$(if $(newer-prereqs),$(call cmd,btf_ko))
+endif
+ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+	+$(if $(newer-prereqs),$(call cmd,lineinfo_ko))
 endif
 	+$(call cmd,check_tracepoint)
 
diff --git a/scripts/gen-mod-lineinfo.sh b/scripts/gen-mod-lineinfo.sh
new file mode 100755
index 0000000000000..fa8a914b8363b
--- /dev/null
+++ b/scripts/gen-mod-lineinfo.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+#
+# gen-mod-lineinfo.sh - Embed source line info into a kernel module (.ko)
+#
+# Reads DWARF from the .ko, generates a .mod_lineinfo section, and
+# embeds it back into the .ko.  Modeled on scripts/gen-btf.sh.
+
+set -e
+
+if [ $# -ne 1 ]; then
+	echo "Usage: $0 <module.ko>" >&2
+	exit 1
+fi
+
+KO="$1"
+
+cleanup() {
+	rm -f "${KO}.lineinfo.S" "${KO}.lineinfo.o" "${KO}.lineinfo.bin"
+}
+trap cleanup EXIT
+
+case "${KBUILD_VERBOSE}" in
+*1*)
+	set -x
+	;;
+esac
+
+# Generate assembly from DWARF -- if it fails (no DWARF), silently skip
+if ! ${objtree}/scripts/gen_lineinfo --module "${KO}" > "${KO}.lineinfo.S" 2>/dev/null; then
+	exit 0
+fi
+
+# Compile assembly to object file
+${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \
+	${KBUILD_AFLAGS} ${KBUILD_AFLAGS_MODULE} \
+	-c -o "${KO}.lineinfo.o" "${KO}.lineinfo.S"
+
+# Extract raw section content
+${OBJCOPY} -O binary --only-section=.mod_lineinfo \
+	"${KO}.lineinfo.o" "${KO}.lineinfo.bin"
+
+# Embed into the .ko with alloc,readonly flags
+${OBJCOPY} --add-section ".mod_lineinfo=${KO}.lineinfo.bin" \
+	--set-section-flags .mod_lineinfo=alloc,readonly \
+	"${KO}"
+
+exit 0
diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c
index 9eebfaca5857c..609de59f47ffd 100644
--- a/scripts/gen_lineinfo.c
+++ b/scripts/gen_lineinfo.c
@@ -23,8 +23,16 @@
 #include <gelf.h>
 #include <limits.h>
 
+#include "../include/linux/mod_lineinfo.h"
+
+static int module_mode;
+
 static unsigned int skipped_overflow;
 
+/* .text range for module mode (keep only runtime code) */
+static unsigned long long text_section_start;
+static unsigned long long text_section_end;
+
 struct line_entry {
 	unsigned int offset;	/* offset from _text */
 	unsigned int file_id;
@@ -123,26 +131,46 @@ static unsigned int find_or_add_file(const char *name)
  *
  * For absolute paths, strip the comp_dir prefix (from DWARF) to get
  * a kernel-tree-relative path, or fall back to the basename.
+ *
+ * For relative paths (common in modules), libdw may produce a bogus
+ * doubled path like "net/foo/bar.c/net/foo/bar.c" due to ET_REL DWARF
+ * quirks.  Detect and strip such duplicates.
  */
 static const char *make_relative(const char *path, const char *comp_dir)
 {
 	const char *p;
 
-	/* If already relative, use as-is */
-	if (path[0] != '/')
-		return path;
+	if (path[0] == '/') {
+		/* Try comp_dir prefix from DWARF */
+		if (comp_dir) {
+			size_t len = strlen(comp_dir);
 
-	/* comp_dir from DWARF is the most reliable method */
-	if (comp_dir) {
-		size_t len = strlen(comp_dir);
+			if (!strncmp(path, comp_dir, len) && path[len] == '/')
+				return path + len + 1;
+		}
 
-		if (!strncmp(path, comp_dir, len) && path[len] == '/')
-			return path + len + 1;
+		/* Fall back to basename */
+		p = strrchr(path, '/');
+		return p ? p + 1 : path;
 	}
 
-	/* Fall back to basename */
-	p = strrchr(path, '/');
-	return p ? p + 1 : path;
+	/*
+	 * Relative path — check for duplicated-path quirk from libdw
+	 * on ET_REL files (e.g., "a/b.c/a/b.c" → "a/b.c").
+	 */
+	{
+		size_t len = strlen(path);
+
+		for (p = path; (p = strchr(p, '/')) != NULL; p++) {
+			size_t prefix = p - path;
+			size_t rest = len - prefix - 1;
+
+			if (rest == prefix && !memcmp(path, p + 1, prefix))
+				return p + 1;
+		}
+	}
+
+	return path;
 }
 
 static int compare_entries(const void *a, const void *b)
@@ -194,6 +222,29 @@ static unsigned long long find_text_addr(Elf *elf)
 	exit(1);
 }
 
+static void find_text_section_range(Elf *elf)
+{
+	Elf_Scn *scn = NULL;
+	GElf_Shdr shdr;
+	size_t shstrndx;
+
+	if (elf_getshdrstrndx(elf, &shstrndx) != 0)
+		return;
+
+	while ((scn = elf_nextscn(elf, scn)) != NULL) {
+		const char *name;
+
+		if (!gelf_getshdr(scn, &shdr))
+			continue;
+		name = elf_strptr(elf, shstrndx, shdr.sh_name);
+		if (name && !strcmp(name, ".text")) {
+			text_section_start = shdr.sh_addr;
+			text_section_end = shdr.sh_addr + shdr.sh_size;
+			return;
+		}
+	}
+}
+
 static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
 {
 	Dwarf_Off off = 0, next_off;
@@ -241,6 +292,16 @@ static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
 			if (addr < text_addr)
 				continue;
 
+			/*
+			 * In module mode, keep only .text addresses.
+			 * In ET_REL .ko files, .init.text/.exit.text may
+			 * overlap with .text address ranges, so we must
+			 * explicitly check against the .text bounds.
+			 */
+			if (module_mode && text_section_end > text_section_start &&
+			    (addr < text_section_start || addr >= text_section_end))
+				continue;
+
 			{
 				unsigned long long raw_offset = addr - text_addr;
 
@@ -374,6 +435,63 @@ static void output_assembly(void)
 	printf("\n");
 }
 
+static void output_module_assembly(void)
+{
+	unsigned int filenames_size = 0;
+
+	for (unsigned int i = 0; i < num_files; i++)
+		filenames_size += strlen(files[i].name) + 1;
+
+	printf("/* SPDX-License-Identifier: GPL-2.0 */\n");
+	printf("/*\n");
+	printf(" * Automatically generated by scripts/gen_lineinfo --module\n");
+	printf(" * Do not edit.\n");
+	printf(" */\n\n");
+
+	printf("\t.section .mod_lineinfo, \"a\"\n\n");
+
+	/* Header: num_entries, num_files, filenames_size, reserved */
+	printf("\t.balign 4\n");
+	printf("\t.long %u\n", num_entries);
+	printf("\t.long %u\n", num_files);
+	printf("\t.long %u\n", filenames_size);
+	printf("\t.long 0\n\n");
+
+	/* addrs[] */
+	for (unsigned int i = 0; i < num_entries; i++)
+		printf("\t.long 0x%x\n", entries[i].offset);
+	if (num_entries)
+		printf("\n");
+
+	/* file_ids[] */
+	for (unsigned int i = 0; i < num_entries; i++)
+		printf("\t.short %u\n", entries[i].file_id);
+
+	/* Padding to align lines[] to 4 bytes */
+	if (num_entries & 1)
+		printf("\t.short 0\n");
+	if (num_entries)
+		printf("\n");
+
+	/* lines[] */
+	for (unsigned int i = 0; i < num_entries; i++)
+		printf("\t.long %u\n", entries[i].line);
+	if (num_entries)
+		printf("\n");
+
+	/* file_offsets[] */
+	for (unsigned int i = 0; i < num_files; i++)
+		printf("\t.long %u\n", files[i].str_offset);
+	if (num_files)
+		printf("\n");
+
+	/* filenames[] */
+	for (unsigned int i = 0; i < num_files; i++)
+		print_escaped_asciz(files[i].name);
+	if (num_files)
+		printf("\n");
+}
+
 int main(int argc, char *argv[])
 {
 	int fd;
@@ -381,8 +499,14 @@ int main(int argc, char *argv[])
 	Dwarf *dwarf;
 	unsigned long long text_addr;
 
+	if (argc >= 2 && !strcmp(argv[1], "--module")) {
+		module_mode = 1;
+		argv++;
+		argc--;
+	}
+
 	if (argc != 2) {
-		fprintf(stderr, "Usage: %s <vmlinux>\n", argv[0]);
+		fprintf(stderr, "Usage: %s [--module] <ELF file>\n", argv[0]);
 		return 1;
 	}
 
@@ -402,7 +526,18 @@ int main(int argc, char *argv[])
 		return 1;
 	}
 
-	text_addr = find_text_addr(elf);
+	if (module_mode) {
+		/*
+		 * .ko files are ET_REL after ld -r.  libdw applies
+		 * relocations using section addresses, so DWARF addresses
+		 * include the .text sh_addr.  Use .text sh_addr as the
+		 * base so offsets are .text-relative.
+		 */
+		find_text_section_range(elf);
+		text_addr = text_section_start;
+	} else {
+		text_addr = find_text_addr(elf);
+	}
 
 	dwarf = dwarf_begin_elf(elf, DWARF_C_READ, NULL);
 	if (!dwarf) {
@@ -428,7 +563,10 @@ int main(int argc, char *argv[])
 	fprintf(stderr, "lineinfo: %u entries, %u files\n",
 		num_entries, num_files);
 
-	output_assembly();
+	if (module_mode)
+		output_module_assembly();
+	else
+		output_assembly();
 
 	dwarf_end(dwarf);
 	elf_end(elf);
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 21+ messages in thread

* [PATCH 3/3] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
  2026-03-03 18:21 [PATCH 0/3] kallsyms: embed source file:line info in kernel stack traces Sasha Levin
  2026-03-03 18:21 ` [PATCH 1/3] " Sasha Levin
  2026-03-03 18:21 ` [PATCH 2/3] kallsyms: extend lineinfo to loadable modules Sasha Levin
@ 2026-03-03 18:21 ` Sasha Levin
  2026-03-03 21:25   ` Geert Uytterhoeven
  2026-03-11  3:34   ` Vivian Wang
  2 siblings, 2 replies; 21+ messages in thread
From: Sasha Levin @ 2026-03-03 18:21 UTC (permalink / raw)
  To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley
  Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
	Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
	Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, linux-kernel,
	linux-kbuild, linux-modules, linux-doc, Sasha Levin

Replace the flat uncompressed parallel arrays (lineinfo_addrs[],
lineinfo_file_ids[], lineinfo_lines[]) with a block-indexed,
delta-encoded, ULEB128 varint compressed format.

The sorted address array has small deltas between consecutive entries
(typically 1-50 bytes), file IDs have high locality (delta often 0,
same file), and line numbers change slowly.  Delta-encoding followed
by ULEB128 varint compression shrinks most values from 4 bytes to 1.

Entries are grouped into blocks of 64.  A small uncompressed block
index (first addr + byte offset per block) enables O(log(N/64)) binary
search, followed by sequential decode of at most 64 varints within the
matching block.  All decode state lives on the stack -- zero
allocations, still safe for NMI/panic context.

Measured on a defconfig+debug x86_64 build (3,017,154 entries, 4,822
source files, 47,144 blocks):

  Before (flat arrays):
    lineinfo_addrs[]    12,068,616 bytes (u32 x 3.0M)
    lineinfo_file_ids[]  6,034,308 bytes (u16 x 3.0M)
    lineinfo_lines[]    12,068,616 bytes (u32 x 3.0M)
    Total:              30,171,540 bytes (28.8 MiB, 10.0 bytes/entry)

  After (block-indexed delta + ULEB128):
    lineinfo_block_addrs[]    188,576 bytes (184 KiB)
    lineinfo_block_offsets[]  188,576 bytes (184 KiB)
    lineinfo_data[]        10,926,128 bytes (10.4 MiB)
    Total:                 11,303,280 bytes (10.8 MiB, 3.7 bytes/entry)

  Savings: 18.0 MiB (2.7x reduction)

Booted in QEMU and verified with SysRq-l that annotations still work:

  default_idle+0x9/0x10 (arch/x86/kernel/process.c:767)
  default_idle_call+0x6c/0xb0 (kernel/sched/idle.c:122)
  do_idle+0x335/0x490 (kernel/sched/idle.c:191)
  cpu_startup_entry+0x4e/0x60 (kernel/sched/idle.c:429)
  rest_init+0x1aa/0x1b0 (init/main.c:760)

Suggested-by: Juergen Gross <jgross@suse.com>
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
 .../admin-guide/kallsyms-lineinfo.rst         |   7 +-
 include/linux/mod_lineinfo.h                  | 103 ++++++++--
 init/Kconfig                                  |   8 +-
 kernel/kallsyms.c                             |  91 +++++++--
 kernel/kallsyms_internal.h                    |   7 +-
 kernel/module/kallsyms.c                      | 107 +++++++---
 scripts/gen_lineinfo.c                        | 192 ++++++++++++++----
 scripts/kallsyms.c                            |   7 +-
 scripts/link-vmlinux.sh                       |  16 +-
 9 files changed, 423 insertions(+), 115 deletions(-)

diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
index 21450569d5324..fe92c5dde16b3 100644
--- a/Documentation/admin-guide/kallsyms-lineinfo.rst
+++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
@@ -76,10 +76,11 @@ Memory Overhead
 ===============
 
 The vmlinux lineinfo tables are stored in ``.rodata`` and typically add
-approximately 44 MiB to the kernel image for a standard configuration
-(~4.6 million DWARF line entries, ~10 bytes per entry after deduplication).
+approximately 10-15 MiB to the kernel image for a standard configuration
+(~4.6 million DWARF line entries, ~2-3 bytes per entry after delta
+compression).
 
-Per-module lineinfo adds approximately 10 bytes per DWARF line entry to each
+Per-module lineinfo adds approximately 2-3 bytes per DWARF line entry to each
 ``.ko`` file.
 
 Known Limitations
diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h
index d62e9608f0f82..ab758acfadceb 100644
--- a/include/linux/mod_lineinfo.h
+++ b/include/linux/mod_lineinfo.h
@@ -8,13 +8,19 @@
  *
  * Section layout (all values in target-native endianness):
  *
- *   struct mod_lineinfo_header     (16 bytes)
- *   u32 addrs[num_entries]         -- offsets from .text base, sorted
- *   u16 file_ids[num_entries]      -- parallel to addrs
- *   <2-byte pad if num_entries is odd>
- *   u32 lines[num_entries]         -- parallel to addrs
+ *   struct mod_lineinfo_header     (24 bytes)
+ *   u32 block_addrs[num_blocks]    -- first addr per block, for binary search
+ *   u32 block_offsets[num_blocks]  -- byte offset into compressed data stream
+ *   u8  data[data_size]            -- ULEB128 delta-compressed entries
  *   u32 file_offsets[num_files]    -- byte offset into filenames[]
  *   char filenames[filenames_size] -- concatenated NUL-terminated strings
+ *
+ * Compressed stream format (per block of LINEINFO_BLOCK_ENTRIES entries):
+ *   Entry 0: file_id (ULEB128), line (ULEB128)
+ *            addr is in block_addrs[]
+ *   Entry 1..N: addr_delta (ULEB128),
+ *               file_id_delta (zigzag-encoded ULEB128),
+ *               line_delta (zigzag-encoded ULEB128)
  */
 #ifndef _LINUX_MOD_LINEINFO_H
 #define _LINUX_MOD_LINEINFO_H
@@ -25,44 +31,107 @@
 #include <stdint.h>
 typedef uint32_t u32;
 typedef uint16_t u16;
+typedef uint8_t  u8;
 #endif
 
+#define LINEINFO_BLOCK_ENTRIES 64
+
 struct mod_lineinfo_header {
 	u32 num_entries;
 	u32 num_files;
 	u32 filenames_size;	/* total bytes of concatenated filenames */
+	u32 num_blocks;
+	u32 data_size;		/* total bytes of compressed data stream */
 	u32 reserved;		/* padding, must be 0 */
 };
 
 /* Offset helpers: compute byte offset from start of section to each array */
 
-static inline u32 mod_lineinfo_addrs_off(void)
+static inline u32 mod_lineinfo_block_addrs_off(void)
 {
 	return sizeof(struct mod_lineinfo_header);
 }
 
-static inline u32 mod_lineinfo_file_ids_off(u32 num_entries)
+static inline u32 mod_lineinfo_block_offsets_off(u32 num_blocks)
 {
-	return mod_lineinfo_addrs_off() + num_entries * sizeof(u32);
+	return mod_lineinfo_block_addrs_off() + num_blocks * sizeof(u32);
 }
 
-static inline u32 mod_lineinfo_lines_off(u32 num_entries)
+static inline u32 mod_lineinfo_data_off(u32 num_blocks)
 {
-	/* u16 file_ids[] may need 2-byte padding to align lines[] to 4 bytes */
-	u32 off = mod_lineinfo_file_ids_off(num_entries) +
-		  num_entries * sizeof(u16);
-	return (off + 3) & ~3u;
+	return mod_lineinfo_block_offsets_off(num_blocks) +
+	       num_blocks * sizeof(u32);
 }
 
-static inline u32 mod_lineinfo_file_offsets_off(u32 num_entries)
+static inline u32 mod_lineinfo_file_offsets_off(u32 num_blocks, u32 data_size)
 {
-	return mod_lineinfo_lines_off(num_entries) + num_entries * sizeof(u32);
+	return mod_lineinfo_data_off(num_blocks) + data_size;
 }
 
-static inline u32 mod_lineinfo_filenames_off(u32 num_entries, u32 num_files)
+static inline u32 mod_lineinfo_filenames_off(u32 num_blocks, u32 data_size,
+					     u32 num_files)
 {
-	return mod_lineinfo_file_offsets_off(num_entries) +
+	return mod_lineinfo_file_offsets_off(num_blocks, data_size) +
 	       num_files * sizeof(u32);
 }
 
+/* Zigzag encoding: map signed to unsigned so small magnitudes are small */
+static inline u32 zigzag_encode(int32_t v)
+{
+	return ((u32)v << 1) ^ (u32)(v >> 31);
+}
+
+static inline int32_t zigzag_decode(u32 v)
+{
+	return (int32_t)((v >> 1) ^ -(v & 1));
+}
+
+/*
+ * Read a ULEB128 varint from a byte stream.
+ * Returns the decoded value and advances *pos past the encoded bytes.
+ * If *pos would exceed 'end', returns 0 and sets *pos = end (safe for
+ * NMI/panic context -- no crash, just a missed annotation).
+ */
+static inline u32 lineinfo_read_uleb128(const u8 *data, u32 *pos, u32 end)
+{
+	u32 result = 0;
+	unsigned int shift = 0;
+
+	while (*pos < end) {
+		u8 byte = data[*pos];
+		(*pos)++;
+		result |= (u32)(byte & 0x7f) << shift;
+		if (!(byte & 0x80))
+			return result;
+		shift += 7;
+		if (shift >= 32) {
+			/* Malformed -- skip remaining continuation bytes */
+			while (*pos < end && (data[*pos] & 0x80))
+				(*pos)++;
+			if (*pos < end)
+				(*pos)++;
+			return result;
+		}
+	}
+	return result;
+}
+
+/* Write a ULEB128 varint -- build tool only */
+#ifndef __KERNEL__
+static inline unsigned int lineinfo_write_uleb128(u8 *buf, u32 value)
+{
+	unsigned int len = 0;
+
+	do {
+		u8 byte = value & 0x7f;
+
+		value >>= 7;
+		if (value)
+			byte |= 0x80;
+		buf[len++] = byte;
+	} while (value);
+	return len;
+}
+#endif /* !__KERNEL__ */
+
 #endif /* _LINUX_MOD_LINEINFO_H */
diff --git a/init/Kconfig b/init/Kconfig
index bf53275bc405a..6e3795b3dbd62 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2065,8 +2065,9 @@ config KALLSYMS_LINEINFO
 	    anon_vma_clone+0x2ed/0xcf0 (mm/rmap.c:412)
 
 	  This requires elfutils (libdw-dev/elfutils-devel) on the build host.
-	  Adds approximately 44MB to a typical kernel image (10 bytes per
-	  DWARF line-table entry, ~4.6M entries for a typical config).
+	  Adds approximately 10-15MB to a typical kernel image (~2-3 bytes
+	  per entry after delta compression, ~4.6M entries for a typical
+	  config).
 
 	  If unsure, say N.
 
@@ -2079,7 +2080,8 @@ config KALLSYMS_LINEINFO_MODULES
 	  so stack traces from module code include (file.c:123) annotations.
 
 	  Requires elfutils (libdw-dev/elfutils-devel) on the build host.
-	  Increases .ko sizes by approximately 10 bytes per DWARF line entry.
+	  Increases .ko sizes by approximately 2-3 bytes per DWARF line
+	  entry after delta compression.
 
 	  If unsure, say N.
 
diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
index cea74992e5427..de4aa8fcfd69d 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -468,14 +468,20 @@ static int append_buildid(char *buffer,   const char *modname,
 #endif /* CONFIG_STACKTRACE_BUILD_ID */
 
 #ifdef CONFIG_KALLSYMS_LINEINFO
+#include <linux/mod_lineinfo.h>
+
 bool kallsyms_lookup_lineinfo(unsigned long addr, unsigned long sym_start,
 			      const char **file, unsigned int *line)
 {
 	unsigned long long raw_offset;
-	unsigned int offset, low, high, mid, file_id;
-	unsigned long line_addr;
-
-	if (!lineinfo_num_entries)
+	unsigned int offset, low, high, mid, block;
+	unsigned int cur_addr, cur_file_id, cur_line;
+	unsigned int best_file_id = 0, best_line = 0;
+	unsigned int block_entries, data_end;
+	bool found = false;
+	u32 pos;
+
+	if (!lineinfo_num_entries || !lineinfo_num_blocks)
 		return false;
 
 	/* Compute offset from _text */
@@ -487,12 +493,12 @@ bool kallsyms_lookup_lineinfo(unsigned long addr, unsigned long sym_start,
 		return false;
 	offset = (unsigned int)raw_offset;
 
-	/* Binary search for largest entry <= offset */
+	/* Binary search on block_addrs[] to find the right block */
 	low = 0;
-	high = lineinfo_num_entries;
+	high = lineinfo_num_blocks;
 	while (low < high) {
 		mid = low + (high - low) / 2;
-		if (lineinfo_addrs[mid] <= offset)
+		if (lineinfo_block_addrs[mid] <= offset)
 			low = mid + 1;
 		else
 			high = mid;
@@ -500,25 +506,68 @@ bool kallsyms_lookup_lineinfo(unsigned long addr, unsigned long sym_start,
 
 	if (low == 0)
 		return false;
-	low--;
+	block = low - 1;
 
-	/*
-	 * Validate that the matched lineinfo entry belongs to the same
-	 * symbol.  Without this check, assembly routines or other
-	 * functions lacking DWARF data would inherit the file:line of
-	 * a preceding C function.
-	 */
-	line_addr = (unsigned long)_text + lineinfo_addrs[low];
-	if (line_addr < sym_start)
-		return false;
+	/* How many entries in this block? */
+	block_entries = LINEINFO_BLOCK_ENTRIES;
+	if (block == lineinfo_num_blocks - 1) {
+		unsigned int remaining = lineinfo_num_entries - block * LINEINFO_BLOCK_ENTRIES;
+
+		if (remaining < block_entries)
+			block_entries = remaining;
+	}
+
+	/* Determine end of this block's data in the compressed stream */
+	if (block + 1 < lineinfo_num_blocks)
+		data_end = lineinfo_block_offsets[block + 1];
+	else
+		data_end = UINT_MAX; /* last block: read to end */
+
+	/* Decode entry 0: addr from block_addrs, file_id and line from stream */
+	pos = lineinfo_block_offsets[block];
+	cur_addr = lineinfo_block_addrs[block];
+	cur_file_id = lineinfo_read_uleb128(lineinfo_data, &pos, data_end);
+	cur_line = lineinfo_read_uleb128(lineinfo_data, &pos, data_end);
+
+	/* Check entry 0 */
+	if (cur_addr <= offset &&
+	    (unsigned long)_text + cur_addr >= sym_start) {
+		best_file_id = cur_file_id;
+		best_line = cur_line;
+		found = true;
+	}
 
-	file_id = lineinfo_file_ids[low];
-	*line = lineinfo_lines[low];
+	/* Decode entries 1..N */
+	for (unsigned int i = 1; i < block_entries; i++) {
+		unsigned int addr_delta;
+		int32_t file_delta, line_delta;
+
+		addr_delta = lineinfo_read_uleb128(lineinfo_data, &pos, data_end);
+		file_delta = zigzag_decode(lineinfo_read_uleb128(lineinfo_data, &pos, data_end));
+		line_delta = zigzag_decode(lineinfo_read_uleb128(lineinfo_data, &pos, data_end));
+
+		cur_addr += addr_delta;
+		cur_file_id = (unsigned int)((int32_t)cur_file_id + file_delta);
+		cur_line = (unsigned int)((int32_t)cur_line + line_delta);
+
+		if (cur_addr > offset)
+			break;
+
+		if ((unsigned long)_text + cur_addr >= sym_start) {
+			best_file_id = cur_file_id;
+			best_line = cur_line;
+			found = true;
+		}
+	}
+
+	if (!found)
+		return false;
 
-	if (file_id >= lineinfo_num_files)
+	if (best_file_id >= lineinfo_num_files)
 		return false;
 
-	*file = &lineinfo_filenames[lineinfo_file_offsets[file_id]];
+	*file = &lineinfo_filenames[lineinfo_file_offsets[best_file_id]];
+	*line = best_line;
 	return true;
 }
 #endif /* CONFIG_KALLSYMS_LINEINFO */
diff --git a/kernel/kallsyms_internal.h b/kernel/kallsyms_internal.h
index 868a1d5035212..691be44440395 100644
--- a/kernel/kallsyms_internal.h
+++ b/kernel/kallsyms_internal.h
@@ -17,10 +17,11 @@ extern const u8 kallsyms_seqs_of_names[];
 
 #ifdef CONFIG_KALLSYMS_LINEINFO
 extern const u32 lineinfo_num_entries;
-extern const u32 lineinfo_addrs[];
-extern const u16 lineinfo_file_ids[];
-extern const u32 lineinfo_lines[];
 extern const u32 lineinfo_num_files;
+extern const u32 lineinfo_num_blocks;
+extern const u32 lineinfo_block_addrs[];
+extern const u32 lineinfo_block_offsets[];
+extern const u8  lineinfo_data[];
 extern const u32 lineinfo_file_offsets[];
 extern const char lineinfo_filenames[];
 #endif
diff --git a/kernel/module/kallsyms.c b/kernel/module/kallsyms.c
index 7af414bd65e79..0ead1bb69de4e 100644
--- a/kernel/module/kallsyms.c
+++ b/kernel/module/kallsyms.c
@@ -512,15 +512,19 @@ bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
 {
 	const struct mod_lineinfo_header *hdr;
 	const void *base;
-	const u32 *addrs, *lines, *file_offsets;
-	const u16 *file_ids;
+	const u32 *blk_addrs, *blk_offsets, *file_offsets;
+	const u8 *data;
 	const char *filenames;
-	u32 num_entries, num_files, filenames_size;
+	u32 num_entries, num_files, filenames_size, num_blocks, data_size;
 	unsigned long text_base;
 	unsigned int offset;
 	unsigned long long raw_offset;
-	unsigned int low, high, mid;
-	u16 file_id;
+	unsigned int low, high, mid, block;
+	unsigned int cur_addr, cur_file_id, cur_line;
+	unsigned int best_file_id = 0, best_line = 0;
+	unsigned int block_entries, data_end;
+	bool found = false;
+	u32 pos;
 
 	base = mod->lineinfo_data;
 	if (!base)
@@ -533,20 +537,24 @@ bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
 	num_entries = hdr->num_entries;
 	num_files = hdr->num_files;
 	filenames_size = hdr->filenames_size;
+	num_blocks = hdr->num_blocks;
+	data_size = hdr->data_size;
 
-	if (num_entries == 0)
+	if (num_entries == 0 || num_blocks == 0)
 		return false;
 
 	/* Validate section is large enough for all arrays */
 	if (mod->lineinfo_data_size <
-	    mod_lineinfo_filenames_off(num_entries, num_files) + filenames_size)
+	    mod_lineinfo_filenames_off(num_blocks, data_size, num_files) +
+	    filenames_size)
 		return false;
 
-	addrs = base + mod_lineinfo_addrs_off();
-	file_ids = base + mod_lineinfo_file_ids_off(num_entries);
-	lines = base + mod_lineinfo_lines_off(num_entries);
-	file_offsets = base + mod_lineinfo_file_offsets_off(num_entries);
-	filenames = base + mod_lineinfo_filenames_off(num_entries, num_files);
+	blk_addrs = base + mod_lineinfo_block_addrs_off();
+	blk_offsets = base + mod_lineinfo_block_offsets_off(num_blocks);
+	data = base + mod_lineinfo_data_off(num_blocks);
+	file_offsets = base + mod_lineinfo_file_offsets_off(num_blocks, data_size);
+	filenames = base + mod_lineinfo_filenames_off(num_blocks, data_size,
+						      num_files);
 
 	/* Compute offset from module .text base */
 	text_base = (unsigned long)mod->mem[MOD_TEXT].base;
@@ -558,12 +566,12 @@ bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
 		return false;
 	offset = (unsigned int)raw_offset;
 
-	/* Binary search for largest entry <= offset */
+	/* Binary search on block_addrs[] to find the right block */
 	low = 0;
-	high = num_entries;
+	high = num_blocks;
 	while (low < high) {
 		mid = low + (high - low) / 2;
-		if (addrs[mid] <= offset)
+		if (blk_addrs[mid] <= offset)
 			low = mid + 1;
 		else
 			high = mid;
@@ -571,21 +579,74 @@ bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
 
 	if (low == 0)
 		return false;
-	low--;
+	block = low - 1;
 
-	/* Ensure the matched entry belongs to the same symbol */
-	if (text_base + addrs[low] < sym_start)
+	/* How many entries in this block? */
+	block_entries = LINEINFO_BLOCK_ENTRIES;
+	if (block == num_blocks - 1) {
+		unsigned int remaining = num_entries - block * LINEINFO_BLOCK_ENTRIES;
+
+		if (remaining < block_entries)
+			block_entries = remaining;
+	}
+
+	/* Determine end of this block's data in the compressed stream */
+	if (block + 1 < num_blocks)
+		data_end = blk_offsets[block + 1];
+	else
+		data_end = data_size;
+
+	/* Decode entry 0: addr from block_addrs, file_id and line from stream */
+	pos = blk_offsets[block];
+	if (pos >= data_end)
+		return false;
+
+	cur_addr = blk_addrs[block];
+	cur_file_id = lineinfo_read_uleb128(data, &pos, data_end);
+	cur_line = lineinfo_read_uleb128(data, &pos, data_end);
+
+	/* Check entry 0 */
+	if (cur_addr <= offset &&
+	    text_base + cur_addr >= sym_start) {
+		best_file_id = cur_file_id;
+		best_line = cur_line;
+		found = true;
+	}
+
+	/* Decode entries 1..N */
+	for (unsigned int i = 1; i < block_entries; i++) {
+		unsigned int addr_delta;
+		int32_t file_delta, line_delta;
+
+		addr_delta = lineinfo_read_uleb128(data, &pos, data_end);
+		file_delta = zigzag_decode(lineinfo_read_uleb128(data, &pos, data_end));
+		line_delta = zigzag_decode(lineinfo_read_uleb128(data, &pos, data_end));
+
+		cur_addr += addr_delta;
+		cur_file_id = (unsigned int)((int32_t)cur_file_id + file_delta);
+		cur_line = (unsigned int)((int32_t)cur_line + line_delta);
+
+		if (cur_addr > offset)
+			break;
+
+		if (text_base + cur_addr >= sym_start) {
+			best_file_id = cur_file_id;
+			best_line = cur_line;
+			found = true;
+		}
+	}
+
+	if (!found)
 		return false;
 
-	file_id = file_ids[low];
-	if (file_id >= num_files)
+	if (best_file_id >= num_files)
 		return false;
 
-	if (file_offsets[file_id] >= filenames_size)
+	if (file_offsets[best_file_id] >= filenames_size)
 		return false;
 
-	*file = &filenames[file_offsets[file_id]];
-	*line = lines[low];
+	*file = &filenames[file_offsets[best_file_id]];
+	*line = best_line;
 	return true;
 }
 #endif /* CONFIG_KALLSYMS_LINEINFO_MODULES */
diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c
index 609de59f47ffd..9507ed9bcbe55 100644
--- a/scripts/gen_lineinfo.c
+++ b/scripts/gen_lineinfo.c
@@ -8,6 +8,9 @@
  * file containing sorted lookup tables that the kernel uses to annotate
  * stack traces with source file:line information.
  *
+ * The output uses a block-indexed, delta-encoded, ULEB128-compressed format
+ * for ~3-4x size reduction compared to flat arrays.
+ *
  * Requires libdw from elfutils.
  */
 
@@ -53,6 +56,15 @@ static struct file_entry *files;
 static unsigned int num_files;
 static unsigned int files_capacity;
 
+/* Compressed output */
+static unsigned char *compressed_data;
+static unsigned int compressed_size;
+static unsigned int compressed_capacity;
+
+static unsigned int *block_addrs;
+static unsigned int *block_offsets;
+static unsigned int num_blocks;
+
 #define FILE_HASH_BITS 13
 #define FILE_HASH_SIZE (1 << FILE_HASH_BITS)
 
@@ -352,6 +364,93 @@ static void deduplicate(void)
 	num_entries = j + 1;
 }
 
+static void compressed_ensure(unsigned int need)
+{
+	if (compressed_size + need <= compressed_capacity)
+		return;
+	compressed_capacity = compressed_capacity ? compressed_capacity * 2 : 1024 * 1024;
+	while (compressed_capacity < compressed_size + need)
+		compressed_capacity *= 2;
+	compressed_data = realloc(compressed_data, compressed_capacity);
+	if (!compressed_data) {
+		fprintf(stderr, "out of memory\n");
+		exit(1);
+	}
+}
+
+static void compress_entries(void)
+{
+	unsigned int i, block;
+
+	if (num_entries == 0) {
+		num_blocks = 0;
+		return;
+	}
+
+	num_blocks = (num_entries + LINEINFO_BLOCK_ENTRIES - 1) / LINEINFO_BLOCK_ENTRIES;
+	block_addrs = calloc(num_blocks, sizeof(*block_addrs));
+	block_offsets = calloc(num_blocks, sizeof(*block_offsets));
+	if (!block_addrs || !block_offsets) {
+		fprintf(stderr, "out of memory\n");
+		exit(1);
+	}
+
+	for (block = 0; block < num_blocks; block++) {
+		unsigned int base = block * LINEINFO_BLOCK_ENTRIES;
+		unsigned int count = num_entries - base;
+		unsigned int prev_addr, prev_file_id, prev_line;
+		unsigned char buf[10]; /* max 5 bytes per ULEB128 */
+
+		if (count > LINEINFO_BLOCK_ENTRIES)
+			count = LINEINFO_BLOCK_ENTRIES;
+
+		block_addrs[block] = entries[base].offset;
+		block_offsets[block] = compressed_size;
+
+		/* Entry 0: file_id (ULEB128), line (ULEB128) */
+		compressed_ensure(20);
+		compressed_size += lineinfo_write_uleb128(
+			compressed_data + compressed_size,
+			entries[base].file_id);
+		compressed_size += lineinfo_write_uleb128(
+			compressed_data + compressed_size,
+			entries[base].line);
+
+		prev_addr = entries[base].offset;
+		prev_file_id = entries[base].file_id;
+		prev_line = entries[base].line;
+
+		/* Entries 1..N: deltas */
+		for (i = 1; i < count; i++) {
+			unsigned int idx = base + i;
+			unsigned int addr_delta;
+			int32_t file_delta, line_delta;
+			unsigned int n;
+
+			addr_delta = entries[idx].offset - prev_addr;
+			file_delta = (int32_t)entries[idx].file_id - (int32_t)prev_file_id;
+			line_delta = (int32_t)entries[idx].line - (int32_t)prev_line;
+
+			compressed_ensure(15);
+			n = lineinfo_write_uleb128(buf, addr_delta);
+			memcpy(compressed_data + compressed_size, buf, n);
+			compressed_size += n;
+
+			n = lineinfo_write_uleb128(buf, zigzag_encode(file_delta));
+			memcpy(compressed_data + compressed_size, buf, n);
+			compressed_size += n;
+
+			n = lineinfo_write_uleb128(buf, zigzag_encode(line_delta));
+			memcpy(compressed_data + compressed_size, buf, n);
+			compressed_size += n;
+
+			prev_addr = entries[idx].offset;
+			prev_file_id = entries[idx].file_id;
+			prev_line = entries[idx].line;
+		}
+	}
+}
+
 static void compute_file_offsets(void)
 {
 	unsigned int offset = 0;
@@ -395,28 +494,40 @@ static void output_assembly(void)
 	printf("lineinfo_num_files:\n");
 	printf("\t.long %u\n\n", num_files);
 
-	/* Sorted address offsets from _text */
-	printf("\t.globl lineinfo_addrs\n");
+	/* Number of blocks */
+	printf("\t.globl lineinfo_num_blocks\n");
 	printf("\t.balign 4\n");
-	printf("lineinfo_addrs:\n");
-	for (unsigned int i = 0; i < num_entries; i++)
-		printf("\t.long 0x%x\n", entries[i].offset);
-	printf("\n");
+	printf("lineinfo_num_blocks:\n");
+	printf("\t.long %u\n\n", num_blocks);
 
-	/* File IDs, parallel to addrs (u16 -- supports up to 65535 files) */
-	printf("\t.globl lineinfo_file_ids\n");
-	printf("\t.balign 2\n");
-	printf("lineinfo_file_ids:\n");
-	for (unsigned int i = 0; i < num_entries; i++)
-		printf("\t.short %u\n", entries[i].file_id);
+	/* Block first-addresses for binary search */
+	printf("\t.globl lineinfo_block_addrs\n");
+	printf("\t.balign 4\n");
+	printf("lineinfo_block_addrs:\n");
+	for (unsigned int i = 0; i < num_blocks; i++)
+		printf("\t.long 0x%x\n", block_addrs[i]);
 	printf("\n");
 
-	/* Line numbers, parallel to addrs */
-	printf("\t.globl lineinfo_lines\n");
+	/* Block byte offsets into compressed stream */
+	printf("\t.globl lineinfo_block_offsets\n");
 	printf("\t.balign 4\n");
-	printf("lineinfo_lines:\n");
-	for (unsigned int i = 0; i < num_entries; i++)
-		printf("\t.long %u\n", entries[i].line);
+	printf("lineinfo_block_offsets:\n");
+	for (unsigned int i = 0; i < num_blocks; i++)
+		printf("\t.long %u\n", block_offsets[i]);
+	printf("\n");
+
+	/* Compressed data stream */
+	printf("\t.globl lineinfo_data\n");
+	printf("lineinfo_data:\n");
+	for (unsigned int i = 0; i < compressed_size; i++) {
+		if ((i % 16) == 0)
+			printf("\t.byte ");
+		else
+			printf(",");
+		printf("0x%02x", compressed_data[i]);
+		if ((i % 16) == 15 || i == compressed_size - 1)
+			printf("\n");
+	}
 	printf("\n");
 
 	/* File string offset table */
@@ -450,33 +561,38 @@ static void output_module_assembly(void)
 
 	printf("\t.section .mod_lineinfo, \"a\"\n\n");
 
-	/* Header: num_entries, num_files, filenames_size, reserved */
+	/* Header: num_entries, num_files, filenames_size, num_blocks, data_size, reserved */
 	printf("\t.balign 4\n");
 	printf("\t.long %u\n", num_entries);
 	printf("\t.long %u\n", num_files);
 	printf("\t.long %u\n", filenames_size);
+	printf("\t.long %u\n", num_blocks);
+	printf("\t.long %u\n", compressed_size);
 	printf("\t.long 0\n\n");
 
-	/* addrs[] */
-	for (unsigned int i = 0; i < num_entries; i++)
-		printf("\t.long 0x%x\n", entries[i].offset);
-	if (num_entries)
+	/* block_addrs[] */
+	for (unsigned int i = 0; i < num_blocks; i++)
+		printf("\t.long 0x%x\n", block_addrs[i]);
+	if (num_blocks)
 		printf("\n");
 
-	/* file_ids[] */
-	for (unsigned int i = 0; i < num_entries; i++)
-		printf("\t.short %u\n", entries[i].file_id);
-
-	/* Padding to align lines[] to 4 bytes */
-	if (num_entries & 1)
-		printf("\t.short 0\n");
-	if (num_entries)
+	/* block_offsets[] */
+	for (unsigned int i = 0; i < num_blocks; i++)
+		printf("\t.long %u\n", block_offsets[i]);
+	if (num_blocks)
 		printf("\n");
 
-	/* lines[] */
-	for (unsigned int i = 0; i < num_entries; i++)
-		printf("\t.long %u\n", entries[i].line);
-	if (num_entries)
+	/* compressed data[] */
+	for (unsigned int i = 0; i < compressed_size; i++) {
+		if ((i % 16) == 0)
+			printf("\t.byte ");
+		else
+			printf(",");
+		printf("0x%02x", compressed_data[i]);
+		if ((i % 16) == 15 || i == compressed_size - 1)
+			printf("\n");
+	}
+	if (compressed_size)
 		printf("\n");
 
 	/* file_offsets[] */
@@ -558,10 +674,11 @@ int main(int argc, char *argv[])
 			skipped_overflow);
 
 	deduplicate();
+	compress_entries();
 	compute_file_offsets();
 
-	fprintf(stderr, "lineinfo: %u entries, %u files\n",
-		num_entries, num_files);
+	fprintf(stderr, "lineinfo: %u entries, %u files, %u blocks, %u compressed bytes\n",
+		num_entries, num_files, num_blocks, compressed_size);
 
 	if (module_mode)
 		output_module_assembly();
@@ -577,6 +694,9 @@ int main(int argc, char *argv[])
 	for (unsigned int i = 0; i < num_files; i++)
 		free(files[i].name);
 	free(files);
+	free(compressed_data);
+	free(block_addrs);
+	free(block_offsets);
 
 	return 0;
 }
diff --git a/scripts/kallsyms.c b/scripts/kallsyms.c
index 42662c4fbc6c9..94fbdad3df7c6 100644
--- a/scripts/kallsyms.c
+++ b/scripts/kallsyms.c
@@ -80,11 +80,12 @@ static bool is_ignored_symbol(const char *name, char type)
 {
 	/* Ignore lineinfo symbols for kallsyms pass stability */
 	static const char * const lineinfo_syms[] = {
-		"lineinfo_addrs",
-		"lineinfo_file_ids",
+		"lineinfo_block_addrs",
+		"lineinfo_block_offsets",
+		"lineinfo_data",
 		"lineinfo_file_offsets",
 		"lineinfo_filenames",
-		"lineinfo_lines",
+		"lineinfo_num_blocks",
 		"lineinfo_num_entries",
 		"lineinfo_num_files",
 	};
diff --git a/scripts/link-vmlinux.sh b/scripts/link-vmlinux.sh
index 640209f2e9eb9..3c122cf9b95c5 100755
--- a/scripts/link-vmlinux.sh
+++ b/scripts/link-vmlinux.sh
@@ -235,12 +235,16 @@ lineinfo_num_entries:
 	.balign 4
 lineinfo_num_files:
 	.long 0
-	.globl lineinfo_addrs
-lineinfo_addrs:
-	.globl lineinfo_file_ids
-lineinfo_file_ids:
-	.globl lineinfo_lines
-lineinfo_lines:
+	.globl lineinfo_num_blocks
+	.balign 4
+lineinfo_num_blocks:
+	.long 0
+	.globl lineinfo_block_addrs
+lineinfo_block_addrs:
+	.globl lineinfo_block_offsets
+lineinfo_block_offsets:
+	.globl lineinfo_data
+lineinfo_data:
 	.globl lineinfo_file_offsets
 lineinfo_file_offsets:
 	.globl lineinfo_filenames
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 21+ messages in thread

* Re: [PATCH 3/3] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
  2026-03-03 18:21 ` [PATCH 3/3] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction Sasha Levin
@ 2026-03-03 21:25   ` Geert Uytterhoeven
  2026-03-04  1:11     ` Sasha Levin
  2026-03-11  3:34   ` Vivian Wang
  1 sibling, 1 reply; 21+ messages in thread
From: Geert Uytterhoeven @ 2026-03-03 21:25 UTC (permalink / raw)
  To: Sasha Levin
  Cc: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, James Bottomley,
	Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
	Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
	Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, linux-kernel,
	linux-kbuild, linux-modules, linux-doc

Hi Sasha,

On Tue, 3 Mar 2026 at 19:22, Sasha Levin <sashal@kernel.org> wrote:
> Replace the flat uncompressed parallel arrays (lineinfo_addrs[],
> lineinfo_file_ids[], lineinfo_lines[]) with a block-indexed,
> delta-encoded, ULEB128 varint compressed format.
>
> The sorted address array has small deltas between consecutive entries
> (typically 1-50 bytes), file IDs have high locality (delta often 0,
> same file), and line numbers change slowly.  Delta-encoding followed
> by ULEB128 varint compression shrinks most values from 4 bytes to 1.
>
> Entries are grouped into blocks of 64.  A small uncompressed block
> index (first addr + byte offset per block) enables O(log(N/64)) binary
> search, followed by sequential decode of at most 64 varints within the
> matching block.  All decode state lives on the stack -- zero
> allocations, still safe for NMI/panic context.
>
> Measured on a defconfig+debug x86_64 build (3,017,154 entries, 4,822
> source files, 47,144 blocks):
>
>   Before (flat arrays):
>     lineinfo_addrs[]    12,068,616 bytes (u32 x 3.0M)
>     lineinfo_file_ids[]  6,034,308 bytes (u16 x 3.0M)
>     lineinfo_lines[]    12,068,616 bytes (u32 x 3.0M)
>     Total:              30,171,540 bytes (28.8 MiB, 10.0 bytes/entry)
>
>   After (block-indexed delta + ULEB128):
>     lineinfo_block_addrs[]    188,576 bytes (184 KiB)
>     lineinfo_block_offsets[]  188,576 bytes (184 KiB)
>     lineinfo_data[]        10,926,128 bytes (10.4 MiB)
>     Total:                 11,303,280 bytes (10.8 MiB, 3.7 bytes/entry)
>
>   Savings: 18.0 MiB (2.7x reduction)
>
> Booted in QEMU and verified with SysRq-l that annotations still work:
>
>   default_idle+0x9/0x10 (arch/x86/kernel/process.c:767)
>   default_idle_call+0x6c/0xb0 (kernel/sched/idle.c:122)
>   do_idle+0x335/0x490 (kernel/sched/idle.c:191)
>   cpu_startup_entry+0x4e/0x60 (kernel/sched/idle.c:429)
>   rest_init+0x1aa/0x1b0 (init/main.c:760)
>
> Suggested-by: Juergen Gross <jgross@suse.com>
> Assisted-by: Claude:claude-opus-4-6
> Signed-off-by: Sasha Levin <sashal@kernel.org>

Thanks for your patch!

> --- a/include/linux/mod_lineinfo.h
> +++ b/include/linux/mod_lineinfo.h

> +/*
> + * Read a ULEB128 varint from a byte stream.
> + * Returns the decoded value and advances *pos past the encoded bytes.
> + * If *pos would exceed 'end', returns 0 and sets *pos = end (safe for
> + * NMI/panic context -- no crash, just a missed annotation).
> + */
> +static inline u32 lineinfo_read_uleb128(const u8 *data, u32 *pos, u32 end)
> +{
> +       u32 result = 0;
> +       unsigned int shift = 0;
> +
> +       while (*pos < end) {
> +               u8 byte = data[*pos];
> +               (*pos)++;
> +               result |= (u32)(byte & 0x7f) << shift;
> +               if (!(byte & 0x80))
> +                       return result;
> +               shift += 7;
> +               if (shift >= 32) {
> +                       /* Malformed -- skip remaining continuation bytes */
> +                       while (*pos < end && (data[*pos] & 0x80))
> +                               (*pos)++;
> +                       if (*pos < end)
> +                               (*pos)++;
> +                       return result;
> +               }
> +       }
> +       return result;
> +}

FTR, arch/arc/kernel/unwind.c, arch/sh/kernel/dwarf.c, and
tools/perf/util/genelf_debug.calready have (different) LEB128 accessors,
so there is an opportunity for consolidation.

Gr{oetje,eeting}s,

                        Geert

-- 
Geert Uytterhoeven -- There's lots of Linux beyond ia32 -- geert@linux-m68k.org

In personal conversations with technical people, I call myself a hacker. But
when I'm talking to journalists I just say "programmer" or something like that.
                                -- Linus Torvalds

^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 3/3] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
  2026-03-03 21:25   ` Geert Uytterhoeven
@ 2026-03-04  1:11     ` Sasha Levin
  0 siblings, 0 replies; 21+ messages in thread
From: Sasha Levin @ 2026-03-04  1:11 UTC (permalink / raw)
  To: Geert Uytterhoeven
  Cc: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, James Bottomley,
	Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
	Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
	Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, linux-kernel,
	linux-kbuild, linux-modules, linux-doc

On Tue, Mar 03, 2026 at 10:25:19PM +0100, Geert Uytterhoeven wrote:
>FTR, arch/arc/kernel/unwind.c, arch/sh/kernel/dwarf.c, and
>tools/perf/util/genelf_debug.calready have (different) LEB128 accessors,
>so there is an opportunity for consolidation.

Looks like this happens in few other places too...

I've created the following (build-tested-only) series on top of the LINEINFO
patches to try and consolidate this:
https://git.kernel.org/pub/scm/linux/kernel/git/sashal/linux.git/log/?h=dedup-lsb128

I can send it out after we figure out what to do with the LINEINFO patches :)

-- 
Thanks,
Sasha

^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 1/3] kallsyms: embed source file:line info in kernel stack traces
  2026-03-03 18:21 ` [PATCH 1/3] " Sasha Levin
@ 2026-03-04 20:17   ` Helge Deller
  2026-03-05  2:18     ` Sasha Levin
  2026-03-06  5:28   ` Randy Dunlap
  2026-03-06 16:36   ` Petr Mladek
  2 siblings, 1 reply; 21+ messages in thread
From: Helge Deller @ 2026-03-04 20:17 UTC (permalink / raw)
  To: Sasha Levin, Andrew Morton, Masahiro Yamada, Luis Chamberlain,
	Linus Torvalds, Richard Weinberger, Juergen Gross,
	Geert Uytterhoeven, James Bottomley
  Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
	Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
	Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, linux-kernel,
	linux-kbuild, linux-modules, linux-doc

On 3/3/26 19:21, Sasha Levin wrote:
> Add CONFIG_KALLSYMS_LINEINFO, which embeds a compact address-to-line
> lookup table in the kernel image so stack traces directly print source
> file and line number information:
> 
>    root@localhost:~# echo c > /proc/sysrq-trigger
>    [   11.201987] sysrq: Trigger a crash
>    [   11.202831] Kernel panic - not syncing: sysrq triggered crash
>    [   11.206218] Call Trace:
>    [   11.206501]  <TASK>
>    [   11.206749]  dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:94)
>    [   11.207403]  vpanic+0x36e/0x620 (kernel/panic.c:650)
>    [   11.208565]  ? __lock_acquire+0x465/0x2240 (kernel/locking/lockdep.c:4674)
>    [   11.209324]  panic+0xc9/0xd0 (kernel/panic.c:787)
>    [   11.211873]  ? find_held_lock+0x2b/0x80 (kernel/locking/lockdep.c:5350)
>    [   11.212597]  ? lock_release+0xd3/0x300 (kernel/locking/lockdep.c:5535)
>    [   11.213312]  sysrq_handle_crash+0x1a/0x20 (drivers/tty/sysrq.c:154)
>    [   11.214005]  __handle_sysrq.cold+0x66/0x256 (drivers/tty/sysrq.c:611)
>    [   11.214712]  write_sysrq_trigger+0x65/0x80 (drivers/tty/sysrq.c:1221)
>    [   11.215424]  proc_reg_write+0x1bd/0x3c0 (fs/proc/inode.c:330)
>    [   11.216061]  vfs_write+0x1c6/0xff0 (fs/read_write.c:686)
>    [   11.218848]  ksys_write+0xfa/0x200 (fs/read_write.c:740)
>    [   11.222394]  do_syscall_64+0xf3/0x690 (arch/x86/entry/syscall_64.c:63)
>    [   11.223942]  entry_SYSCALL_64_after_hwframe+0x77/0x7f (arch/x86/entry/entry_64.S:121)

As mentioned in the other series, I really like this patch series.

I tested this series again on the parisc architecture, and the relative
directories are now stripped with this version of your patch.
IIRC, the previous patch did show the subdirectory names.
[  132.840382] Backtrace:
[  132.840382]  [<104254d8>] show_stack+0x50/0x64 (traps.c:212)
[  132.840382]  [<1041c0c8>] dump_stack_lvl+0x6c/0xa0 (dump_stack.c:122)
[  132.840382]  [<1041c118>] dump_stack+0x1c/0x2c (dump_stack.c:130)
[  132.840382]  [<10402218>] vpanic+0x154/0x344 (panic.c:550)
[  132.840382]  [<10402438>] panic+0x30/0x34 (panic.c:787)
[  132.840382]  [<10bebea8>] sysrq_handle_crash+0x30/0x34 (rcupdate.h:110)
[  132.840382]  [<10bec720>] __handle_sysrq+0xc0/0x1e4 (preempt.h:14)

Helge

^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 1/3] kallsyms: embed source file:line info in kernel stack traces
  2026-03-04 20:17   ` Helge Deller
@ 2026-03-05  2:18     ` Sasha Levin
  2026-03-05 22:26       ` Helge Deller
  0 siblings, 1 reply; 21+ messages in thread
From: Sasha Levin @ 2026-03-05  2:18 UTC (permalink / raw)
  To: Helge Deller
  Cc: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley, Jonathan Corbet, Nathan Chancellor,
	Nicolas Schier, Petr Pavlu, Daniel Gomez, Greg KH, Petr Mladek,
	Steven Rostedt, Kees Cook, Peter Zijlstra, Thorsten Leemhuis,
	Vlastimil Babka, linux-kernel, linux-kbuild, linux-modules,
	linux-doc

On Wed, Mar 04, 2026 at 09:17:37PM +0100, Helge Deller wrote:
>On 3/3/26 19:21, Sasha Levin wrote:
>>Add CONFIG_KALLSYMS_LINEINFO, which embeds a compact address-to-line
>>lookup table in the kernel image so stack traces directly print source
>>file and line number information:
>>
>>   root@localhost:~# echo c > /proc/sysrq-trigger
>>   [   11.201987] sysrq: Trigger a crash
>>   [   11.202831] Kernel panic - not syncing: sysrq triggered crash
>>   [   11.206218] Call Trace:
>>   [   11.206501]  <TASK>
>>   [   11.206749]  dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:94)
>>   [   11.207403]  vpanic+0x36e/0x620 (kernel/panic.c:650)
>>   [   11.208565]  ? __lock_acquire+0x465/0x2240 (kernel/locking/lockdep.c:4674)
>>   [   11.209324]  panic+0xc9/0xd0 (kernel/panic.c:787)
>>   [   11.211873]  ? find_held_lock+0x2b/0x80 (kernel/locking/lockdep.c:5350)
>>   [   11.212597]  ? lock_release+0xd3/0x300 (kernel/locking/lockdep.c:5535)
>>   [   11.213312]  sysrq_handle_crash+0x1a/0x20 (drivers/tty/sysrq.c:154)
>>   [   11.214005]  __handle_sysrq.cold+0x66/0x256 (drivers/tty/sysrq.c:611)
>>   [   11.214712]  write_sysrq_trigger+0x65/0x80 (drivers/tty/sysrq.c:1221)
>>   [   11.215424]  proc_reg_write+0x1bd/0x3c0 (fs/proc/inode.c:330)
>>   [   11.216061]  vfs_write+0x1c6/0xff0 (fs/read_write.c:686)
>>   [   11.218848]  ksys_write+0xfa/0x200 (fs/read_write.c:740)
>>   [   11.222394]  do_syscall_64+0xf3/0x690 (arch/x86/entry/syscall_64.c:63)
>>   [   11.223942]  entry_SYSCALL_64_after_hwframe+0x77/0x7f (arch/x86/entry/entry_64.S:121)
>
>As mentioned in the other series, I really like this patch series.
>
>I tested this series again on the parisc architecture, and the relative
>directories are now stripped with this version of your patch.
>IIRC, the previous patch did show the subdirectory names.
>[  132.840382] Backtrace:
>[  132.840382]  [<104254d8>] show_stack+0x50/0x64 (traps.c:212)
>[  132.840382]  [<1041c0c8>] dump_stack_lvl+0x6c/0xa0 (dump_stack.c:122)
>[  132.840382]  [<1041c118>] dump_stack+0x1c/0x2c (dump_stack.c:130)
>[  132.840382]  [<10402218>] vpanic+0x154/0x344 (panic.c:550)
>[  132.840382]  [<10402438>] panic+0x30/0x34 (panic.c:787)
>[  132.840382]  [<10bebea8>] sysrq_handle_crash+0x30/0x34 (rcupdate.h:110)
>[  132.840382]  [<10bec720>] __handle_sysrq+0xc0/0x1e4 (preempt.h:14)

Ugh... Can you confirm that you've build this kernel with O=?

The RFC had a dirty dirty hack around how we turn these absolute paths into
relative ones, but I tried to re-do it so no one would yell at me :)

-- 
Thanks,
Sasha

^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 1/3] kallsyms: embed source file:line info in kernel stack traces
  2026-03-05  2:18     ` Sasha Levin
@ 2026-03-05 22:26       ` Helge Deller
  2026-03-06  5:31         ` Randy Dunlap
  0 siblings, 1 reply; 21+ messages in thread
From: Helge Deller @ 2026-03-05 22:26 UTC (permalink / raw)
  To: Sasha Levin
  Cc: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley, Jonathan Corbet, Nathan Chancellor,
	Nicolas Schier, Petr Pavlu, Daniel Gomez, Greg KH, Petr Mladek,
	Steven Rostedt, Kees Cook, Peter Zijlstra, Thorsten Leemhuis,
	Vlastimil Babka, linux-kernel, linux-kbuild, linux-modules,
	linux-doc

On 3/5/26 03:18, Sasha Levin wrote:
> On Wed, Mar 04, 2026 at 09:17:37PM +0100, Helge Deller wrote:
>> On 3/3/26 19:21, Sasha Levin wrote:
>>> Add CONFIG_KALLSYMS_LINEINFO, which embeds a compact address-to-line
>>> lookup table in the kernel image so stack traces directly print source
>>> file and line number information:
>>>
>>>   root@localhost:~# echo c > /proc/sysrq-trigger
>>>   [   11.201987] sysrq: Trigger a crash
>>>   [   11.202831] Kernel panic - not syncing: sysrq triggered crash
>>>   [   11.206218] Call Trace:
>>>   [   11.206501]  <TASK>
>>>   [   11.206749]  dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:94)
>>>   [   11.207403]  vpanic+0x36e/0x620 (kernel/panic.c:650)
>>>   [   11.208565]  ? __lock_acquire+0x465/0x2240 (kernel/locking/lockdep.c:4674)
>>>   [   11.209324]  panic+0xc9/0xd0 (kernel/panic.c:787)
>>>   [   11.211873]  ? find_held_lock+0x2b/0x80 (kernel/locking/lockdep.c:5350)
>>>   [   11.212597]  ? lock_release+0xd3/0x300 (kernel/locking/lockdep.c:5535)
>>>   [   11.213312]  sysrq_handle_crash+0x1a/0x20 (drivers/tty/sysrq.c:154)
>>>   [   11.214005]  __handle_sysrq.cold+0x66/0x256 (drivers/tty/sysrq.c:611)
>>>   [   11.214712]  write_sysrq_trigger+0x65/0x80 (drivers/tty/sysrq.c:1221)
>>>   [   11.215424]  proc_reg_write+0x1bd/0x3c0 (fs/proc/inode.c:330)
>>>   [   11.216061]  vfs_write+0x1c6/0xff0 (fs/read_write.c:686)
>>>   [   11.218848]  ksys_write+0xfa/0x200 (fs/read_write.c:740)
>>>   [   11.222394]  do_syscall_64+0xf3/0x690 (arch/x86/entry/syscall_64.c:63)
>>>   [   11.223942]  entry_SYSCALL_64_after_hwframe+0x77/0x7f (arch/x86/entry/entry_64.S:121)
>>
>> As mentioned in the other series, I really like this patch series.
>>
>> I tested this series again on the parisc architecture, and the relative
>> directories are now stripped with this version of your patch.
>> IIRC, the previous patch did show the subdirectory names.
>> [  132.840382] Backtrace:
>> [  132.840382]  [<104254d8>] show_stack+0x50/0x64 (traps.c:212)
>> [  132.840382]  [<1041c0c8>] dump_stack_lvl+0x6c/0xa0 (dump_stack.c:122)
>> [  132.840382]  [<1041c118>] dump_stack+0x1c/0x2c (dump_stack.c:130)
>> [  132.840382]  [<10402218>] vpanic+0x154/0x344 (panic.c:550)
>> [  132.840382]  [<10402438>] panic+0x30/0x34 (panic.c:787)
>> [  132.840382]  [<10bebea8>] sysrq_handle_crash+0x30/0x34 (rcupdate.h:110)
>> [  132.840382]  [<10bec720>] __handle_sysrq+0xc0/0x1e4 (preempt.h:14)
> 
> Ugh... Can you confirm that you've build this kernel with O=?

Yes. Both -Os and -O2 do not show the relative path.
  
> The RFC had a dirty dirty hack around how we turn these absolute paths into
> relative ones, but I tried to re-do it so no one would yell at me :)

Seems it is needed...

Helge

^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 1/3] kallsyms: embed source file:line info in kernel stack traces
  2026-03-03 18:21 ` [PATCH 1/3] " Sasha Levin
  2026-03-04 20:17   ` Helge Deller
@ 2026-03-06  5:28   ` Randy Dunlap
  2026-03-06 16:36   ` Petr Mladek
  2 siblings, 0 replies; 21+ messages in thread
From: Randy Dunlap @ 2026-03-06  5:28 UTC (permalink / raw)
  To: Sasha Levin, Andrew Morton, Masahiro Yamada, Luis Chamberlain,
	Linus Torvalds, Richard Weinberger, Juergen Gross,
	Geert Uytterhoeven, James Bottomley
  Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
	Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
	Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, linux-kernel,
	linux-kbuild, linux-modules, linux-doc



On 3/3/26 10:21 AM, Sasha Levin wrote:
> diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
> new file mode 100644
> index 0000000000000..4dffc18dbcf5a
> --- /dev/null
> +++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
> @@ -0,0 +1,72 @@
> +.. SPDX-License-Identifier: GPL-2.0
> +
> +==================================
> +Kallsyms Source Line Info (LINEINFO)
> +==================================

Heading over/under lines must be at least as long as the heading.

> +
> +Overview
> +========
> +
> +``CONFIG_KALLSYMS_LINEINFO`` embeds DWARF-derived source file and line number
> +mappings into the kernel image so that stack traces include
> +``(file.c:123)`` annotations next to each symbol.  This makes it significantly
> +easier to pinpoint the exact source location during debugging, without needing
> +to manually cross-reference addresses with ``addr2line``.


> diff --git a/MAINTAINERS b/MAINTAINERS
> index 61bf550fd37c2..ab987e74bb0f5 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -14278,6 +14278,12 @@ F:	lib/Kconfig.kmsan
>  F:	mm/kmsan/
>  F:	scripts/Makefile.kmsan
>  
> +KALLSYMS LINEINFO
> +M:	Sasha Levin <sashal@kernel.org>
> +S:	Maintained
> +F:	Documentation/admin-guide/kallsyms-lineinfo.rst
> +F:	scripts/gen_lineinfo.c

This entry should be in alphabetical order, just before KASAN.

> +
>  KPROBES
>  M:	Naveen N Rao <naveen@kernel.org>
>  M:	"David S. Miller" <davem@davemloft.net>

-- 
~Randy


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 1/3] kallsyms: embed source file:line info in kernel stack traces
  2026-03-05 22:26       ` Helge Deller
@ 2026-03-06  5:31         ` Randy Dunlap
  2026-03-06 17:53           ` Helge Deller
  0 siblings, 1 reply; 21+ messages in thread
From: Randy Dunlap @ 2026-03-06  5:31 UTC (permalink / raw)
  To: Helge Deller, Sasha Levin
  Cc: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley, Jonathan Corbet, Nathan Chancellor,
	Nicolas Schier, Petr Pavlu, Daniel Gomez, Greg KH, Petr Mladek,
	Steven Rostedt, Kees Cook, Peter Zijlstra, Thorsten Leemhuis,
	Vlastimil Babka, linux-kernel, linux-kbuild, linux-modules,
	linux-doc



On 3/5/26 2:26 PM, Helge Deller wrote:
> On 3/5/26 03:18, Sasha Levin wrote:
>> On Wed, Mar 04, 2026 at 09:17:37PM +0100, Helge Deller wrote:
>>> On 3/3/26 19:21, Sasha Levin wrote:
>>>> Add CONFIG_KALLSYMS_LINEINFO, which embeds a compact address-to-line
>>>> lookup table in the kernel image so stack traces directly print source
>>>> file and line number information:
>>>>
>>>>   root@localhost:~# echo c > /proc/sysrq-trigger
>>>>   [   11.201987] sysrq: Trigger a crash
>>>>   [   11.202831] Kernel panic - not syncing: sysrq triggered crash
>>>>   [   11.206218] Call Trace:
>>>>   [   11.206501]  <TASK>
>>>>   [   11.206749]  dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:94)
>>>>   [   11.207403]  vpanic+0x36e/0x620 (kernel/panic.c:650)
>>>>   [   11.208565]  ? __lock_acquire+0x465/0x2240 (kernel/locking/lockdep.c:4674)
>>>>   [   11.209324]  panic+0xc9/0xd0 (kernel/panic.c:787)
>>>>   [   11.211873]  ? find_held_lock+0x2b/0x80 (kernel/locking/lockdep.c:5350)
>>>>   [   11.212597]  ? lock_release+0xd3/0x300 (kernel/locking/lockdep.c:5535)
>>>>   [   11.213312]  sysrq_handle_crash+0x1a/0x20 (drivers/tty/sysrq.c:154)
>>>>   [   11.214005]  __handle_sysrq.cold+0x66/0x256 (drivers/tty/sysrq.c:611)
>>>>   [   11.214712]  write_sysrq_trigger+0x65/0x80 (drivers/tty/sysrq.c:1221)
>>>>   [   11.215424]  proc_reg_write+0x1bd/0x3c0 (fs/proc/inode.c:330)
>>>>   [   11.216061]  vfs_write+0x1c6/0xff0 (fs/read_write.c:686)
>>>>   [   11.218848]  ksys_write+0xfa/0x200 (fs/read_write.c:740)
>>>>   [   11.222394]  do_syscall_64+0xf3/0x690 (arch/x86/entry/syscall_64.c:63)
>>>>   [   11.223942]  entry_SYSCALL_64_after_hwframe+0x77/0x7f (arch/x86/entry/entry_64.S:121)
>>>
>>> As mentioned in the other series, I really like this patch series.
>>>
>>> I tested this series again on the parisc architecture, and the relative
>>> directories are now stripped with this version of your patch.
>>> IIRC, the previous patch did show the subdirectory names.
>>> [  132.840382] Backtrace:
>>> [  132.840382]  [<104254d8>] show_stack+0x50/0x64 (traps.c:212)
>>> [  132.840382]  [<1041c0c8>] dump_stack_lvl+0x6c/0xa0 (dump_stack.c:122)
>>> [  132.840382]  [<1041c118>] dump_stack+0x1c/0x2c (dump_stack.c:130)
>>> [  132.840382]  [<10402218>] vpanic+0x154/0x344 (panic.c:550)
>>> [  132.840382]  [<10402438>] panic+0x30/0x34 (panic.c:787)
>>> [  132.840382]  [<10bebea8>] sysrq_handle_crash+0x30/0x34 (rcupdate.h:110)
>>> [  132.840382]  [<10bec720>] __handle_sysrq+0xc0/0x1e4 (preempt.h:14)
>>
>> Ugh... Can you confirm that you've build this kernel with O=?
> 
> Yes. Both -Os and -O2 do not show the relative path.

Helge,
I'm fairly sure that Sasha meant with O=build_dir_name,
not -O for optimization levels.

>> The RFC had a dirty dirty hack around how we turn these absolute paths into
>> relative ones, but I tried to re-do it so no one would yell at me :)
> 
> Seems it is needed...
> 
> Helge
> 

-- 
~Randy


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 1/3] kallsyms: embed source file:line info in kernel stack traces
  2026-03-03 18:21 ` [PATCH 1/3] " Sasha Levin
  2026-03-04 20:17   ` Helge Deller
  2026-03-06  5:28   ` Randy Dunlap
@ 2026-03-06 16:36   ` Petr Mladek
  2026-03-06 17:14     ` Sasha Levin
  2 siblings, 1 reply; 21+ messages in thread
From: Petr Mladek @ 2026-03-06 16:36 UTC (permalink / raw)
  To: Sasha Levin
  Cc: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley, Jonathan Corbet, Nathan Chancellor,
	Nicolas Schier, Petr Pavlu, Daniel Gomez, Greg KH, Steven Rostedt,
	Kees Cook, Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka,
	linux-kernel, linux-kbuild, linux-modules, linux-doc

On Tue 2026-03-03 13:21:01, Sasha Levin wrote:
> Add CONFIG_KALLSYMS_LINEINFO, which embeds a compact address-to-line
> lookup table in the kernel image so stack traces directly print source
> file and line number information:
> 
>   root@localhost:~# echo c > /proc/sysrq-trigger
>   [   11.201987] sysrq: Trigger a crash
>   [   11.202831] Kernel panic - not syncing: sysrq triggered crash
>   [   11.206218] Call Trace:
>   [   11.206501]  <TASK>
>   [   11.206749]  dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:94)
>   [   11.207403]  vpanic+0x36e/0x620 (kernel/panic.c:650)
>   [   11.208565]  ? __lock_acquire+0x465/0x2240 (kernel/locking/lockdep.c:4674)
>   [   11.209324]  panic+0xc9/0xd0 (kernel/panic.c:787)
>   [   11.211873]  ? find_held_lock+0x2b/0x80 (kernel/locking/lockdep.c:5350)
>   [   11.212597]  ? lock_release+0xd3/0x300 (kernel/locking/lockdep.c:5535)
>   [   11.213312]  sysrq_handle_crash+0x1a/0x20 (drivers/tty/sysrq.c:154)
>   [   11.214005]  __handle_sysrq.cold+0x66/0x256 (drivers/tty/sysrq.c:611)
>   [   11.214712]  write_sysrq_trigger+0x65/0x80 (drivers/tty/sysrq.c:1221)
>   [   11.215424]  proc_reg_write+0x1bd/0x3c0 (fs/proc/inode.c:330)
>   [   11.216061]  vfs_write+0x1c6/0xff0 (fs/read_write.c:686)
>   [   11.218848]  ksys_write+0xfa/0x200 (fs/read_write.c:740)
>   [   11.222394]  do_syscall_64+0xf3/0x690 (arch/x86/entry/syscall_64.c:63)
>   [   11.223942]  entry_SYSCALL_64_after_hwframe+0x77/0x7f (arch/x86/entry/entry_64.S:121)
> 
> --- a/include/linux/kallsyms.h
> +++ b/include/linux/kallsyms.h
> @@ -16,10 +16,19 @@
>  #include <asm/sections.h>
>  
>  #define KSYM_NAME_LEN 512
> +
> +#ifdef CONFIG_KALLSYMS_LINEINFO
> +/* Extra space for " (path/to/file.c:12345)" suffix */
> +#define KSYM_LINEINFO_LEN 128
> +#else
> +#define KSYM_LINEINFO_LEN 0
> +#endif
> +
>  #define KSYM_SYMBOL_LEN (sizeof("%s+%#lx/%#lx [%s %s]") + \

I guess that this is used also in ftrace where there formatting
is delayed. We might want:

  #define KSYM_SYMBOL_LEN (sizeof("%s+%#lx/%#lx [%s %s] (%s:%u)") + \

>  			(KSYM_NAME_LEN - 1) + \
>  			2*(BITS_PER_LONG*3/10) + (MODULE_NAME_LEN - 1) + \
> -			(BUILD_ID_SIZE_MAX * 2) + 1)
> +			(BUILD_ID_SIZE_MAX * 2) + 1 + \
> +			KSYM_LINEINFO_LEN)
>  
>  struct cred;
>  struct module;
> --- a/kernel/kallsyms.c
> +++ b/kernel/kallsyms.c
> @@ -467,6 +467,62 @@ static int append_buildid(char *buffer,   const char *modname,
>  
>  #endif /* CONFIG_STACKTRACE_BUILD_ID */
>  
> +#ifdef CONFIG_KALLSYMS_LINEINFO
> +bool kallsyms_lookup_lineinfo(unsigned long addr, unsigned long sym_start,
> +			      const char **file, unsigned int *line)
> +{
> +	unsigned long long raw_offset;
> +	unsigned int offset, low, high, mid, file_id;
> +	unsigned long line_addr;
> +
> +	if (!lineinfo_num_entries)
> +		return false;
> +
> +	/* Compute offset from _text */
> +	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 largest entry <= offset */
> +	low = 0;
> +	high = lineinfo_num_entries;
> +	while (low < high) {
> +		mid = low + (high - low) / 2;
> +		if (lineinfo_addrs[mid] <= offset)
> +			low = mid + 1;
> +		else
> +			high = mid;
> +	}
> +
> +	if (low == 0)
> +		return false;
> +	low--;
> +
> +	/*
> +	 * Validate that the matched lineinfo entry belongs to the same
> +	 * symbol.  Without this check, assembly routines or other
> +	 * functions lacking DWARF data would inherit the file:line of
> +	 * a preceding C function.
> +	 */
> +	line_addr = (unsigned long)_text + lineinfo_addrs[low];
> +	if (line_addr < sym_start)
> +		return false;

This is suspicious. The binary search does "low = mid + 1".
I would expect that lineinfo_addrs[low] would point to
a higher address when the exact match is not found.

Anyway, I think that we should accept only the exact match and do:

	if (lineinfo_addrs[low] != offset)
		return false;

Or do I miss something? (Friday evening here ;-)

> +	file_id = lineinfo_file_ids[low];
> +	*line = lineinfo_lines[low];
> +
> +	if (file_id >= lineinfo_num_files)
> +		return false;
> +
> +	*file = &lineinfo_filenames[lineinfo_file_offsets[file_id]];
> +	return true;
> +}
> +#endif /* CONFIG_KALLSYMS_LINEINFO */
> +
>  /* 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)
> @@ -497,6 +553,19 @@ static int __sprint_symbol(char *buffer, unsigned long address,
>  		len += sprintf(buffer + len, "]");
>  	}
>  
> +#ifdef CONFIG_KALLSYMS_LINEINFO
> +	if (!modname) {
> +		const char *li_file;
> +		unsigned int li_line;
> +		unsigned long sym_start = address - offset;
> +
> +		if (kallsyms_lookup_lineinfo(address, sym_start,
> +					     &li_file, &li_line))
> +			len += snprintf(buffer + len, KSYM_SYMBOL_LEN - len,

s/KSYM_SYMBOL_LEN/KSYM_LINEINFO_LEN/

> +					" (%s:%u)", li_file, li_line);
> +	}
> +#endif
> +
>  	return len;
>  }


I was rather curious how the code looked like and the mentioned things
caught my eyes. And I focused on the kernel/kallsyms code.

Unfortunately, I do not have time for a proper full review at the
moment.

The code seems to work. And it generates relative paths for me, for example:

[  305.678609] sysrq: Show backtrace of all active CPUs
[  305.680615] NMI backtrace for cpu 0
[  305.680620] CPU: 0 UID: 0 PID: 1540 Comm: bash Kdump: loaded Not tainted 7.0.0-rc2-default+ #561 PREEMPT(full)  0d0ba470fd9bf64113a65472ab47c033a2658d88
[  305.680626] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS rel-1.17.0-2-g4f253b9b-prebuilt.qemu.org 04/01/2014
[  305.680628] Call Trace:
[  305.680631]  <TASK>
[  305.680640]  dump_stack_lvl+0x6c/0xa0 (lib/dump_stack.c:94)
[  305.680680]  nmi_cpu_backtrace.cold+0x51/0x6a (lib/nmi_backtrace.c:113)
[  305.680689]  ? __pfx_nmi_raise_cpu_backtrace+0x10/0x10
[  305.680702]  nmi_trigger_cpumask_backtrace+0x113/0x130 (lib/nmi_backtrace.c:62)
[  305.680720]  __handle_sysrq.cold+0x9b/0xde (drivers/tty/sysrq.c:611)
[  305.680734]  write_sysrq_trigger+0x6a/0xb0 (drivers/tty/sysrq.c:1221)
[  305.680750]  proc_reg_write+0x59/0xa0 (fs/proc/inode.c:330)
[  305.680763]  vfs_write+0xd0/0x570 (fs/read_write.c:686)
[  305.680771]  ? srso_alias_return_thunk+0x5/0xfbef5 (arch/x86/lib/retpoline.S:220)
[  305.680776]  ? srso_alias_return_thunk+0x5/0xfbef5 (arch/x86/lib/retpoline.S:220)
[  305.680779]  ? __lock_release.isra.0+0x1c9/0x300 (kernel/locking/lockdep.c:342)
[  305.680796]  ? srso_alias_return_thunk+0x5/0xfbef5 (arch/x86/lib/retpoline.S:220)
[  305.680813]  ksys_write+0x70/0xf0 (fs/read_write.c:738)
[  305.680826]  do_syscall_64+0x11d/0x660 (arch/x86/entry/syscall_64.c:63)
[  305.680832]  ? irqentry_exit+0x94/0x5f0 (./include/linux/irq-entry-common.h:298)
[  305.680846]  entry_SYSCALL_64_after_hwframe+0x76/0x7e (arch/x86/entry/entry_64.S:121)

HTH,
Petr

^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 1/3] kallsyms: embed source file:line info in kernel stack traces
  2026-03-06 16:36   ` Petr Mladek
@ 2026-03-06 17:14     ` Sasha Levin
  2026-03-10 15:20       ` Petr Mladek
  0 siblings, 1 reply; 21+ messages in thread
From: Sasha Levin @ 2026-03-06 17:14 UTC (permalink / raw)
  To: Petr Mladek
  Cc: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley, Jonathan Corbet, Nathan Chancellor,
	Nicolas Schier, Petr Pavlu, Daniel Gomez, Greg KH, Steven Rostedt,
	Kees Cook, Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka,
	linux-kernel, linux-kbuild, linux-modules, linux-doc

On Fri, Mar 06, 2026 at 05:36:36PM +0100, Petr Mladek wrote:
>On Tue 2026-03-03 13:21:01, Sasha Levin wrote:
>> Add CONFIG_KALLSYMS_LINEINFO, which embeds a compact address-to-line
>> lookup table in the kernel image so stack traces directly print source
>> file and line number information:
>>
>>   root@localhost:~# echo c > /proc/sysrq-trigger
>>   [   11.201987] sysrq: Trigger a crash
>>   [   11.202831] Kernel panic - not syncing: sysrq triggered crash
>>   [   11.206218] Call Trace:
>>   [   11.206501]  <TASK>
>>   [   11.206749]  dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:94)
>>   [   11.207403]  vpanic+0x36e/0x620 (kernel/panic.c:650)
>>   [   11.208565]  ? __lock_acquire+0x465/0x2240 (kernel/locking/lockdep.c:4674)
>>   [   11.209324]  panic+0xc9/0xd0 (kernel/panic.c:787)
>>   [   11.211873]  ? find_held_lock+0x2b/0x80 (kernel/locking/lockdep.c:5350)
>>   [   11.212597]  ? lock_release+0xd3/0x300 (kernel/locking/lockdep.c:5535)
>>   [   11.213312]  sysrq_handle_crash+0x1a/0x20 (drivers/tty/sysrq.c:154)
>>   [   11.214005]  __handle_sysrq.cold+0x66/0x256 (drivers/tty/sysrq.c:611)
>>   [   11.214712]  write_sysrq_trigger+0x65/0x80 (drivers/tty/sysrq.c:1221)
>>   [   11.215424]  proc_reg_write+0x1bd/0x3c0 (fs/proc/inode.c:330)
>>   [   11.216061]  vfs_write+0x1c6/0xff0 (fs/read_write.c:686)
>>   [   11.218848]  ksys_write+0xfa/0x200 (fs/read_write.c:740)
>>   [   11.222394]  do_syscall_64+0xf3/0x690 (arch/x86/entry/syscall_64.c:63)
>>   [   11.223942]  entry_SYSCALL_64_after_hwframe+0x77/0x7f (arch/x86/entry/entry_64.S:121)
>>
>> --- a/include/linux/kallsyms.h
>> +++ b/include/linux/kallsyms.h
>> @@ -16,10 +16,19 @@
>>  #include <asm/sections.h>
>>
>>  #define KSYM_NAME_LEN 512
>> +
>> +#ifdef CONFIG_KALLSYMS_LINEINFO
>> +/* Extra space for " (path/to/file.c:12345)" suffix */
>> +#define KSYM_LINEINFO_LEN 128
>> +#else
>> +#define KSYM_LINEINFO_LEN 0
>> +#endif
>> +
>>  #define KSYM_SYMBOL_LEN (sizeof("%s+%#lx/%#lx [%s %s]") + \
>
>I guess that this is used also in ftrace where there formatting
>is delayed. We might want:
>
>  #define KSYM_SYMBOL_LEN (sizeof("%s+%#lx/%#lx [%s %s] (%s:%u)") + \

KSYM_LINEINFO_LEN already covers the full expansion of the path and line
number, not just the literal format characters. ftrace stores raw addresses and
formats via %pS at print time into a KSYM_SYMBOL_LEN-sized buffer, so there
shouldn't be an issue here.

>>  			(KSYM_NAME_LEN - 1) + \
>>  			2*(BITS_PER_LONG*3/10) + (MODULE_NAME_LEN - 1) + \
>> -			(BUILD_ID_SIZE_MAX * 2) + 1)
>> +			(BUILD_ID_SIZE_MAX * 2) + 1 + \
>> +			KSYM_LINEINFO_LEN)
>>
>>  struct cred;
>>  struct module;
>> --- a/kernel/kallsyms.c
>> +++ b/kernel/kallsyms.c
>> @@ -467,6 +467,62 @@ static int append_buildid(char *buffer,   const char *modname,
>>
>>  #endif /* CONFIG_STACKTRACE_BUILD_ID */
>>
>> +#ifdef CONFIG_KALLSYMS_LINEINFO
>> +bool kallsyms_lookup_lineinfo(unsigned long addr, unsigned long sym_start,
>> +			      const char **file, unsigned int *line)
>> +{
>> +	unsigned long long raw_offset;
>> +	unsigned int offset, low, high, mid, file_id;
>> +	unsigned long line_addr;
>> +
>> +	if (!lineinfo_num_entries)
>> +		return false;
>> +
>> +	/* Compute offset from _text */
>> +	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 largest entry <= offset */
>> +	low = 0;
>> +	high = lineinfo_num_entries;
>> +	while (low < high) {
>> +		mid = low + (high - low) / 2;
>> +		if (lineinfo_addrs[mid] <= offset)
>> +			low = mid + 1;
>> +		else
>> +			high = mid;
>> +	}
>> +
>> +	if (low == 0)
>> +		return false;
>> +	low--;
>> +
>> +	/*
>> +	 * Validate that the matched lineinfo entry belongs to the same
>> +	 * symbol.  Without this check, assembly routines or other
>> +	 * functions lacking DWARF data would inherit the file:line of
>> +	 * a preceding C function.
>> +	 */
>> +	line_addr = (unsigned long)_text + lineinfo_addrs[low];
>> +	if (line_addr < sym_start)
>> +		return false;
>
>This is suspicious. The binary search does "low = mid + 1".
>I would expect that lineinfo_addrs[low] would point to
>a higher address when the exact match is not found.
>
>Anyway, I think that we should accept only the exact match and do:
>
>	if (lineinfo_addrs[low] != offset)
>		return false;
>
>Or do I miss something? (Friday evening here ;-)

Right, when there's no exact match, low ends up pointing to the next higher
entry. The table is sparse, with one entry per source-line transition, not per
instruction address. The correct result for a given PC is the nearest
preceding entry, so the code uses low - 1 in that case. Same semantics as
kallsyms symbol lookup.

>> +	file_id = lineinfo_file_ids[low];
>> +	*line = lineinfo_lines[low];
>> +
>> +	if (file_id >= lineinfo_num_files)
>> +		return false;
>> +
>> +	*file = &lineinfo_filenames[lineinfo_file_offsets[file_id]];
>> +	return true;
>> +}
>> +#endif /* CONFIG_KALLSYMS_LINEINFO */
>> +
>>  /* 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)
>> @@ -497,6 +553,19 @@ static int __sprint_symbol(char *buffer, unsigned long address,
>>  		len += sprintf(buffer + len, "]");
>>  	}
>>
>> +#ifdef CONFIG_KALLSYMS_LINEINFO
>> +	if (!modname) {
>> +		const char *li_file;
>> +		unsigned int li_line;
>> +		unsigned long sym_start = address - offset;
>> +
>> +		if (kallsyms_lookup_lineinfo(address, sym_start,
>> +					     &li_file, &li_line))
>> +			len += snprintf(buffer + len, KSYM_SYMBOL_LEN - len,
>
>s/KSYM_SYMBOL_LEN/KSYM_LINEINFO_LEN/

KSYM_SYMBOL_LEN - len is the remaining capacity of the output buffer.  len
tracks total bytes written (symbol + offset + module + buildid), which can
easily exceed 128.

>> +					" (%s:%u)", li_file, li_line);
>> +	}
>> +#endif
>> +
>>  	return len;
>>  }
>
>
>I was rather curious how the code looked like and the mentioned things
>caught my eyes. And I focused on the kernel/kallsyms code.
>
>Unfortunately, I do not have time for a proper full review at the
>moment.
>
>The code seems to work. And it generates relative paths for me, for example:
>
>[  305.678609] sysrq: Show backtrace of all active CPUs
>[  305.680615] NMI backtrace for cpu 0
>[  305.680620] CPU: 0 UID: 0 PID: 1540 Comm: bash Kdump: loaded Not tainted 7.0.0-rc2-default+ #561 PREEMPT(full)  0d0ba470fd9bf64113a65472ab47c033a2658d88
>[  305.680626] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS rel-1.17.0-2-g4f253b9b-prebuilt.qemu.org 04/01/2014
>[  305.680628] Call Trace:
>[  305.680631]  <TASK>
>[  305.680640]  dump_stack_lvl+0x6c/0xa0 (lib/dump_stack.c:94)
>[  305.680680]  nmi_cpu_backtrace.cold+0x51/0x6a (lib/nmi_backtrace.c:113)
>[  305.680689]  ? __pfx_nmi_raise_cpu_backtrace+0x10/0x10
>[  305.680702]  nmi_trigger_cpumask_backtrace+0x113/0x130 (lib/nmi_backtrace.c:62)
>[  305.680720]  __handle_sysrq.cold+0x9b/0xde (drivers/tty/sysrq.c:611)
>[  305.680734]  write_sysrq_trigger+0x6a/0xb0 (drivers/tty/sysrq.c:1221)
>[  305.680750]  proc_reg_write+0x59/0xa0 (fs/proc/inode.c:330)
>[  305.680763]  vfs_write+0xd0/0x570 (fs/read_write.c:686)
>[  305.680771]  ? srso_alias_return_thunk+0x5/0xfbef5 (arch/x86/lib/retpoline.S:220)
>[  305.680776]  ? srso_alias_return_thunk+0x5/0xfbef5 (arch/x86/lib/retpoline.S:220)
>[  305.680779]  ? __lock_release.isra.0+0x1c9/0x300 (kernel/locking/lockdep.c:342)
>[  305.680796]  ? srso_alias_return_thunk+0x5/0xfbef5 (arch/x86/lib/retpoline.S:220)
>[  305.680813]  ksys_write+0x70/0xf0 (fs/read_write.c:738)
>[  305.680826]  do_syscall_64+0x11d/0x660 (arch/x86/entry/syscall_64.c:63)
>[  305.680832]  ? irqentry_exit+0x94/0x5f0 (./include/linux/irq-entry-common.h:298)
>[  305.680846]  entry_SYSCALL_64_after_hwframe+0x76/0x7e (arch/x86/entry/entry_64.S:121)

Thanks for the review and testing! Have a great weekend!

-- 
Thanks,
Sasha

^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 1/3] kallsyms: embed source file:line info in kernel stack traces
  2026-03-06  5:31         ` Randy Dunlap
@ 2026-03-06 17:53           ` Helge Deller
  0 siblings, 0 replies; 21+ messages in thread
From: Helge Deller @ 2026-03-06 17:53 UTC (permalink / raw)
  To: Randy Dunlap, Sasha Levin
  Cc: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley, Jonathan Corbet, Nathan Chancellor,
	Nicolas Schier, Petr Pavlu, Daniel Gomez, Greg KH, Petr Mladek,
	Steven Rostedt, Kees Cook, Peter Zijlstra, Thorsten Leemhuis,
	Vlastimil Babka, linux-kernel, linux-kbuild, linux-modules,
	linux-doc

On 3/6/26 06:31, Randy Dunlap wrote:
> On 3/5/26 2:26 PM, Helge Deller wrote:
>> On 3/5/26 03:18, Sasha Levin wrote:
>>> On Wed, Mar 04, 2026 at 09:17:37PM +0100, Helge Deller wrote:
>>>> On 3/3/26 19:21, Sasha Levin wrote:
>>>>> Add CONFIG_KALLSYMS_LINEINFO, which embeds a compact address-to-line
>>>>> lookup table in the kernel image so stack traces directly print source
>>>>> file and line number information:
>>>>>
>>>>>    root@localhost:~# echo c > /proc/sysrq-trigger
>>>>>    [   11.201987] sysrq: Trigger a crash
>>>>>    [   11.202831] Kernel panic - not syncing: sysrq triggered crash
>>>>>    [   11.206218] Call Trace:
>>>>>    [   11.206501]  <TASK>
>>>>>    [   11.206749]  dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:94)
>>>>>    [   11.207403]  vpanic+0x36e/0x620 (kernel/panic.c:650)
>>>>>    [   11.208565]  ? __lock_acquire+0x465/0x2240 (kernel/locking/lockdep.c:4674)
>>>>>    [   11.209324]  panic+0xc9/0xd0 (kernel/panic.c:787)
>>>>>    [   11.211873]  ? find_held_lock+0x2b/0x80 (kernel/locking/lockdep.c:5350)
>>>>>    [   11.212597]  ? lock_release+0xd3/0x300 (kernel/locking/lockdep.c:5535)
>>>>>    [   11.213312]  sysrq_handle_crash+0x1a/0x20 (drivers/tty/sysrq.c:154)
>>>>>    [   11.214005]  __handle_sysrq.cold+0x66/0x256 (drivers/tty/sysrq.c:611)
>>>>>    [   11.214712]  write_sysrq_trigger+0x65/0x80 (drivers/tty/sysrq.c:1221)
>>>>>    [   11.215424]  proc_reg_write+0x1bd/0x3c0 (fs/proc/inode.c:330)
>>>>>    [   11.216061]  vfs_write+0x1c6/0xff0 (fs/read_write.c:686)
>>>>>    [   11.218848]  ksys_write+0xfa/0x200 (fs/read_write.c:740)
>>>>>    [   11.222394]  do_syscall_64+0xf3/0x690 (arch/x86/entry/syscall_64.c:63)
>>>>>    [   11.223942]  entry_SYSCALL_64_after_hwframe+0x77/0x7f (arch/x86/entry/entry_64.S:121)
>>>>
>>>> As mentioned in the other series, I really like this patch series.
>>>>
>>>> I tested this series again on the parisc architecture, and the relative
>>>> directories are now stripped with this version of your patch.
>>>> IIRC, the previous patch did show the subdirectory names.
>>>> [  132.840382] Backtrace:
>>>> [  132.840382]  [<104254d8>] show_stack+0x50/0x64 (traps.c:212)
>>>> [  132.840382]  [<1041c0c8>] dump_stack_lvl+0x6c/0xa0 (dump_stack.c:122)
>>>> [  132.840382]  [<1041c118>] dump_stack+0x1c/0x2c (dump_stack.c:130)
>>>> [  132.840382]  [<10402218>] vpanic+0x154/0x344 (panic.c:550)
>>>> [  132.840382]  [<10402438>] panic+0x30/0x34 (panic.c:787)
>>>> [  132.840382]  [<10bebea8>] sysrq_handle_crash+0x30/0x34 (rcupdate.h:110)
>>>> [  132.840382]  [<10bec720>] __handle_sysrq+0xc0/0x1e4 (preempt.h:14)
>>>
>>> Ugh... Can you confirm that you've build this kernel with O=?
>>
>> Yes. Both -Os and -O2 do not show the relative path.
> 
> Helge,
> I'm fairly sure that Sasha meant with O=build_dir_name,
> not -O for optimization levels.

Ah, ok.
Anyway, I checked again.
I did *not* used a "O=..." parameter in either case.
I recompiled again with the RFC patch, and here the relative paths show up again (even without O=):
  
  root@debian:~# echo c > /proc/sysrq-trigger
[  121.172011] sysrq: Trigger a crash
[  121.173986] Kernel panic - not syncing: sysrq triggered crash
[  121.176141] CPU: 1 UID: 0 PID: 382 Comm: bash Not tainted 7.0.0-rc2-32bit+ #2971 VOLUNTARY
[  121.177041] Hardware name: 9000/778/B160L
[  121.178092] Backtrace:
[  121.178533]  [<104254d8>] show_stack+0x50/0x64 (kernel/traps.c:212)
[  121.181408]  [<1041c0c8>] dump_stack_lvl+0x6c/0xa0 (lib/dump_stack.c:122)
[  121.182145]  [<1041c118>] dump_stack+0x1c/0x2c (lib/dump_stack.c:130)
[  121.182779]  [<10402218>] vpanic+0x154/0x344 (kernel/panic.c:552)
[  121.182871]  [<10402438>] panic+0x30/0x34 (kernel/panic.c:787)
[  121.182871]  [<10beb5a0>] sysrq_handle_crash+0x30/0x34 (drivers/tty/sysrq.c:154)
[  121.182871]  [<10bebe18>] __handle_sysrq+0xc0/0x1e4 (arch/parisc/include/asm/current.h:13)
[  121.182871]  [<10bec7c0>] write_sysrq_trigger+0x8c/0xcc (drivers/tty/sysrq.c:1223)
[  121.182871]  [<106ba460>] proc_reg_write+0xd0/0x10c (fs/proc/inode.c:343)
[  121.182871]  [<1060faf0>] vfs_write+0xb8/0x46c (fs/read_write.c:691)
[  121.182871]  [<1061005c>] ksys_write+0x78/0x118 (fs/read_write.c:741)
[  121.182871]  [<10610114>] sys_write+0x18/0x28 (fs/read_write.c:748)
[  121.182871]  [<10421334>] syscall_exit+0x0/0x10 (kernel/entry.S:1722)

Helge
  
>>> The RFC had a dirty dirty hack around how we turn these absolute paths into
>>> relative ones, but I tried to re-do it so no one would yell at me :)
>>
>> Seems it is needed...

^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 1/3] kallsyms: embed source file:line info in kernel stack traces
  2026-03-06 17:14     ` Sasha Levin
@ 2026-03-10 15:20       ` Petr Mladek
  2026-03-11  0:58         ` Sasha Levin
  0 siblings, 1 reply; 21+ messages in thread
From: Petr Mladek @ 2026-03-10 15:20 UTC (permalink / raw)
  To: Sasha Levin
  Cc: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley, Jonathan Corbet, Nathan Chancellor,
	Nicolas Schier, Petr Pavlu, Daniel Gomez, Greg KH, Steven Rostedt,
	Kees Cook, Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka,
	linux-kernel, linux-kbuild, linux-modules, linux-doc

On Fri 2026-03-06 12:14:45, Sasha Levin wrote:
> On Fri, Mar 06, 2026 at 05:36:36PM +0100, Petr Mladek wrote:
> > On Tue 2026-03-03 13:21:01, Sasha Levin wrote:
> > > Add CONFIG_KALLSYMS_LINEINFO, which embeds a compact address-to-line
> > > lookup table in the kernel image so stack traces directly print source
> > > file and line number information:
> > > 
> > > --- a/include/linux/kallsyms.h
> > > +++ b/include/linux/kallsyms.h
> > > @@ -16,10 +16,19 @@
> > >  #include <asm/sections.h>
> > > 
> > >  #define KSYM_NAME_LEN 512
> > > +
> > > +#ifdef CONFIG_KALLSYMS_LINEINFO
> > > +/* Extra space for " (path/to/file.c:12345)" suffix */
> > > +#define KSYM_LINEINFO_LEN 128
> > > +#else
> > > +#define KSYM_LINEINFO_LEN 0
> > > +#endif
> > > +
> > >  #define KSYM_SYMBOL_LEN (sizeof("%s+%#lx/%#lx [%s %s]") + \
> > 
> > I guess that this is used also in ftrace where there formatting
> > is delayed. We might want:
> > 
> >  #define KSYM_SYMBOL_LEN (sizeof("%s+%#lx/%#lx [%s %s] (%s:%u)") + \
> 
> KSYM_LINEINFO_LEN already covers the full expansion of the path and line
> number, not just the literal format characters. ftrace stores raw addresses and
> formats via %pS at print time into a KSYM_SYMBOL_LEN-sized buffer, so there
> shouldn't be an issue here.

I was curious why the sizeof("%s+%#lx/%#lx [%s %s]") was there.
It did not make much sense to count some "random" part of the
format string.

I expected that it was related to the ftrace delayed formatting.
But they are written to the tracing buffer, see trace_vbprintk().

But I believe that it does not need to be counted. It seems to be some
cargo-cult programming. The size has been counted first by the commit
d069cf94ca296b7fb ("kallsyms for new modules") back in v2.6.12-rc2,
see
https://git.kernel.org/pub/scm/linux/kernel/git/history/history.git/commit/?id=d069cf94ca296b7fb4c7e362e8f27e2c8aca70f1
And it seems that it was not needed there.

That said, we could not simply remove it witout revisiting the rest of
the computation. Especilly, we need to make sure that it counts all
extra characters, like spaces, brackets, and the trailing '\0'.

Ideally, we should replace the unsafe sprintf() with snprintf() in
all users. (>> TODO ;-)

> > >  			(KSYM_NAME_LEN - 1) + \
> > >  			2*(BITS_PER_LONG*3/10) + (MODULE_NAME_LEN - 1) + \
> > > -			(BUILD_ID_SIZE_MAX * 2) + 1)
> > > +			(BUILD_ID_SIZE_MAX * 2) + 1 + \
> > > +			KSYM_LINEINFO_LEN)
> > > 
> > >  struct cred;
> > >  struct module;

Best Regards,
Petr

^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 1/3] kallsyms: embed source file:line info in kernel stack traces
  2026-03-10 15:20       ` Petr Mladek
@ 2026-03-11  0:58         ` Sasha Levin
  0 siblings, 0 replies; 21+ messages in thread
From: Sasha Levin @ 2026-03-11  0:58 UTC (permalink / raw)
  To: Petr Mladek
  Cc: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley, Jonathan Corbet, Nathan Chancellor,
	Nicolas Schier, Petr Pavlu, Daniel Gomez, Greg KH, Steven Rostedt,
	Kees Cook, Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka,
	linux-kernel, linux-kbuild, linux-modules, linux-doc

On Tue, Mar 10, 2026 at 04:20:32PM +0100, Petr Mladek wrote:
>On Fri 2026-03-06 12:14:45, Sasha Levin wrote:
>> On Fri, Mar 06, 2026 at 05:36:36PM +0100, Petr Mladek wrote:
>> > On Tue 2026-03-03 13:21:01, Sasha Levin wrote:
>> > > Add CONFIG_KALLSYMS_LINEINFO, which embeds a compact address-to-line
>> > > lookup table in the kernel image so stack traces directly print source
>> > > file and line number information:
>> > >
>> > > --- a/include/linux/kallsyms.h
>> > > +++ b/include/linux/kallsyms.h
>> > > @@ -16,10 +16,19 @@
>> > >  #include <asm/sections.h>
>> > >
>> > >  #define KSYM_NAME_LEN 512
>> > > +
>> > > +#ifdef CONFIG_KALLSYMS_LINEINFO
>> > > +/* Extra space for " (path/to/file.c:12345)" suffix */
>> > > +#define KSYM_LINEINFO_LEN 128
>> > > +#else
>> > > +#define KSYM_LINEINFO_LEN 0
>> > > +#endif
>> > > +
>> > >  #define KSYM_SYMBOL_LEN (sizeof("%s+%#lx/%#lx [%s %s]") + \
>> >
>> > I guess that this is used also in ftrace where there formatting
>> > is delayed. We might want:
>> >
>> >  #define KSYM_SYMBOL_LEN (sizeof("%s+%#lx/%#lx [%s %s] (%s:%u)") + \
>>
>> KSYM_LINEINFO_LEN already covers the full expansion of the path and line
>> number, not just the literal format characters. ftrace stores raw addresses and
>> formats via %pS at print time into a KSYM_SYMBOL_LEN-sized buffer, so there
>> shouldn't be an issue here.
>
>I was curious why the sizeof("%s+%#lx/%#lx [%s %s]") was there.
>It did not make much sense to count some "random" part of the
>format string.
>
>I expected that it was related to the ftrace delayed formatting.
>But they are written to the tracing buffer, see trace_vbprintk().
>
>But I believe that it does not need to be counted. It seems to be some
>cargo-cult programming. The size has been counted first by the commit
>d069cf94ca296b7fb ("kallsyms for new modules") back in v2.6.12-rc2,
>see
>https://git.kernel.org/pub/scm/linux/kernel/git/history/history.git/commit/?id=d069cf94ca296b7fb4c7e362e8f27e2c8aca70f1
>And it seems that it was not needed there.
>
>That said, we could not simply remove it witout revisiting the rest of
>the computation. Especilly, we need to make sure that it counts all
>extra characters, like spaces, brackets, and the trailing '\0'.
>
>Ideally, we should replace the unsafe sprintf() with snprintf() in
>all users. (>> TODO ;-)

Yeah, good catch. The sizeof() counts the format specifiers too  which never
end up in the output since their expansions are already covered by the other
terms.

I'd rather not poke that bear as part of this series, we can try it in a follow
up and see if anything explodes?

-- 
Thanks,
Sasha

^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 3/3] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
  2026-03-03 18:21 ` [PATCH 3/3] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction Sasha Levin
  2026-03-03 21:25   ` Geert Uytterhoeven
@ 2026-03-11  3:34   ` Vivian Wang
  2026-03-11  4:13     ` Vivian Wang
  2026-03-11 14:49     ` Sasha Levin
  1 sibling, 2 replies; 21+ messages in thread
From: Vivian Wang @ 2026-03-11  3:34 UTC (permalink / raw)
  To: Sasha Levin, Andrew Morton, Masahiro Yamada, Luis Chamberlain,
	Linus Torvalds, Richard Weinberger, Juergen Gross,
	Geert Uytterhoeven, James Bottomley
  Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
	Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
	Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, linux-kernel,
	linux-kbuild, linux-modules, linux-doc

Hi Sasha,

I've been trying this out and AFAICT this does work perfectly. Thank you
for this.

There are a few oddities I found:

Firstly I've been building with something like O=_riscv out of
convenience, and the file names have an extra ../ in the front. (This is
just me exiting out of init=/bin/sh.)

[    2.317268] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000
[    2.320283] CPU: 0 UID: 0 PID: 1 Comm: sh Not tainted 7.0.0-rc3-00004-g8ad18f1a1a2f #31 PREEMPTLAZY 
[    2.322048] Hardware name: riscv-virtio,qemu (DT)
[    2.323220] Call Trace:
[    2.324465] [<ffffffff800172a8>] dump_backtrace+0x1c/0x24 (../arch/riscv/kernel/stacktrace.c:150)
[    2.329061] [<ffffffff8000241e>] show_stack+0x2a/0x34 (../arch/riscv/kernel/stacktrace.c:156)
[    2.330334] [<ffffffff8000fe32>] dump_stack_lvl+0x4a/0x68 (../lib/dump_stack.c:122)
[    2.331462] [<ffffffff8000fe64>] dump_stack+0x14/0x1c (../lib/dump_stack.c:130)
[    2.332571] [<ffffffff80002a88>] vpanic+0x108/0x2bc (../kernel/panic.c:651)
[    2.333674] [<ffffffff80002c6e>] panic+0x32/0x34 (../kernel/panic.c:787)
[    2.334427] [<ffffffff8002e97a>] do_exit+0x7ee/0x7f4 (../kernel/exit.c:930)
[    2.335194] [<ffffffff8002eade>] do_group_exit+0x1a/0x88 (../kernel/exit.c:1099)
[    2.335945] [<ffffffff8002eb62>] __riscv_sys_exit_group+0x16/0x18 (../kernel/exit.c:1129)
[    2.336763] [<ffffffff80b3e868>] do_trap_ecall_u+0x260/0x45c (../arch/riscv/include/asm/syscall.h:112)
[    2.337765] [<ffffffff80b4c034>] handle_exception+0x168/0x174 (../arch/riscv/kernel/entry.S:233)

This is fine by me, but I've seen mentions of O= builds but I'm not sure
if it's expected.

Also, toggling CONFIG_KALLSYMS_LINEINFO seems to rebuild every single
file. I haven't debugged why, but is this expected?

I have a few ideas about the code as well. Since this patch 3 touches
most of the files involved, I'll just dump my thoughts on the whole
series here. I want to note that I haven't read the RFC thread too
carefully, but I don't think there were many comments on the implementation.

On 3/4/26 02:21, Sasha Levin wrote:
> Replace the flat uncompressed parallel arrays (lineinfo_addrs[],
> lineinfo_file_ids[], lineinfo_lines[]) with a block-indexed,
> delta-encoded, ULEB128 varint compressed format.
>
> The sorted address array has small deltas between consecutive entries
> (typically 1-50 bytes), file IDs have high locality (delta often 0,
> same file), and line numbers change slowly.  Delta-encoding followed
> by ULEB128 varint compression shrinks most values from 4 bytes to 1.
>
> Entries are grouped into blocks of 64.  A small uncompressed block
> index (first addr + byte offset per block) enables O(log(N/64)) binary
> search, followed by sequential decode of at most 64 varints within the
> matching block.  All decode state lives on the stack -- zero
> allocations, still safe for NMI/panic context.
>
> Measured on a defconfig+debug x86_64 build (3,017,154 entries, 4,822
> source files, 47,144 blocks):
>
>   Before (flat arrays):
>     lineinfo_addrs[]    12,068,616 bytes (u32 x 3.0M)
>     lineinfo_file_ids[]  6,034,308 bytes (u16 x 3.0M)
>     lineinfo_lines[]    12,068,616 bytes (u32 x 3.0M)
>     Total:              30,171,540 bytes (28.8 MiB, 10.0 bytes/entry)
>
>   After (block-indexed delta + ULEB128):
>     lineinfo_block_addrs[]    188,576 bytes (184 KiB)
>     lineinfo_block_offsets[]  188,576 bytes (184 KiB)
>     lineinfo_data[]        10,926,128 bytes (10.4 MiB)
>     Total:                 11,303,280 bytes (10.8 MiB, 3.7 bytes/entry)
>
>   Savings: 18.0 MiB (2.7x reduction)
>
> Booted in QEMU and verified with SysRq-l that annotations still work:
>
>   default_idle+0x9/0x10 (arch/x86/kernel/process.c:767)
>   default_idle_call+0x6c/0xb0 (kernel/sched/idle.c:122)
>   do_idle+0x335/0x490 (kernel/sched/idle.c:191)
>   cpu_startup_entry+0x4e/0x60 (kernel/sched/idle.c:429)
>   rest_init+0x1aa/0x1b0 (init/main.c:760)
>
> Suggested-by: Juergen Gross <jgross@suse.com>
> Assisted-by: Claude:claude-opus-4-6
> Signed-off-by: Sasha Levin <sashal@kernel.org>
> ---
>  .../admin-guide/kallsyms-lineinfo.rst         |   7 +-
>  include/linux/mod_lineinfo.h                  | 103 ++++++++--
>  init/Kconfig                                  |   8 +-
>  kernel/kallsyms.c                             |  91 +++++++--
>  kernel/kallsyms_internal.h                    |   7 +-
>  kernel/module/kallsyms.c                      | 107 +++++++---
>  scripts/gen_lineinfo.c                        | 192 ++++++++++++++----
>  scripts/kallsyms.c                            |   7 +-
>  scripts/link-vmlinux.sh                       |  16 +-
>  9 files changed, 423 insertions(+), 115 deletions(-)
>
> diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
> index 21450569d5324..fe92c5dde16b3 100644
> --- a/Documentation/admin-guide/kallsyms-lineinfo.rst
> +++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
> @@ -76,10 +76,11 @@ Memory Overhead
>  ===============
>  
>  The vmlinux lineinfo tables are stored in ``.rodata`` and typically add
> -approximately 44 MiB to the kernel image for a standard configuration
> -(~4.6 million DWARF line entries, ~10 bytes per entry after deduplication).
> +approximately 10-15 MiB to the kernel image for a standard configuration
> +(~4.6 million DWARF line entries, ~2-3 bytes per entry after delta
> +compression).
>  
> -Per-module lineinfo adds approximately 10 bytes per DWARF line entry to each
> +Per-module lineinfo adds approximately 2-3 bytes per DWARF line entry to each
>  ``.ko`` file.
>  

Maybe this could be given in terms of percentages? It wasn't obvious to
me what 10-15 MiB amounts to.

On riscv64, I'm seeing a 24.2 MiB to 30.2 MiB increase in
arch/riscv/boot/Image size on an approximately defconfig+mod2noconfig
build, which is about a 25% increase. I haven't checked yet, but if 25%
is similar to what other archs get, that's a more useful figure than
10-15 MiB, given that the size increase is correlated to the total
amount of code linked into the kernel/module.

>  Known Limitations
> diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h
> index d62e9608f0f82..ab758acfadceb 100644
> --- a/include/linux/mod_lineinfo.h
> +++ b/include/linux/mod_lineinfo.h
> @@ -8,13 +8,19 @@
>   *
>   * Section layout (all values in target-native endianness):
>   *
> - *   struct mod_lineinfo_header     (16 bytes)
> - *   u32 addrs[num_entries]         -- offsets from .text base, sorted
> - *   u16 file_ids[num_entries]      -- parallel to addrs
> - *   <2-byte pad if num_entries is odd>
> - *   u32 lines[num_entries]         -- parallel to addrs
> + *   struct mod_lineinfo_header     (24 bytes)
> + *   u32 block_addrs[num_blocks]    -- first addr per block, for binary search
> + *   u32 block_offsets[num_blocks]  -- byte offset into compressed data stream
> + *   u8  data[data_size]            -- ULEB128 delta-compressed entries
>   *   u32 file_offsets[num_files]    -- byte offset into filenames[]
>   *   char filenames[filenames_size] -- concatenated NUL-terminated strings
> + *
> + * Compressed stream format (per block of LINEINFO_BLOCK_ENTRIES entries):
> + *   Entry 0: file_id (ULEB128), line (ULEB128)
> + *            addr is in block_addrs[]
> + *   Entry 1..N: addr_delta (ULEB128),
> + *               file_id_delta (zigzag-encoded ULEB128),
> + *               line_delta (zigzag-encoded ULEB128)
>   */
>  #ifndef _LINUX_MOD_LINEINFO_H
>  #define _LINUX_MOD_LINEINFO_H
> @@ -25,44 +31,107 @@
>  #include <stdint.h>
>  typedef uint32_t u32;
>  typedef uint16_t u16;
> +typedef uint8_t  u8;
>  #endif
>  
> +#define LINEINFO_BLOCK_ENTRIES 64
> +
>  struct mod_lineinfo_header {
>  	u32 num_entries;
>  	u32 num_files;
>  	u32 filenames_size;	/* total bytes of concatenated filenames */
> +	u32 num_blocks;
> +	u32 data_size;		/* total bytes of compressed data stream */
>  	u32 reserved;		/* padding, must be 0 */
>  };
>  
>  /* Offset helpers: compute byte offset from start of section to each array */
>  
> -static inline u32 mod_lineinfo_addrs_off(void)
> +static inline u32 mod_lineinfo_block_addrs_off(void)
>  {
>  	return sizeof(struct mod_lineinfo_header);
>  }
>  
> -static inline u32 mod_lineinfo_file_ids_off(u32 num_entries)
> +static inline u32 mod_lineinfo_block_offsets_off(u32 num_blocks)
>  {
> -	return mod_lineinfo_addrs_off() + num_entries * sizeof(u32);
> +	return mod_lineinfo_block_addrs_off() + num_blocks * sizeof(u32);
>  }
>  
> -static inline u32 mod_lineinfo_lines_off(u32 num_entries)
> +static inline u32 mod_lineinfo_data_off(u32 num_blocks)
>  {
> -	/* u16 file_ids[] may need 2-byte padding to align lines[] to 4 bytes */
> -	u32 off = mod_lineinfo_file_ids_off(num_entries) +
> -		  num_entries * sizeof(u16);
> -	return (off + 3) & ~3u;
> +	return mod_lineinfo_block_offsets_off(num_blocks) +
> +	       num_blocks * sizeof(u32);
>  }
>  
> -static inline u32 mod_lineinfo_file_offsets_off(u32 num_entries)
> +static inline u32 mod_lineinfo_file_offsets_off(u32 num_blocks, u32 data_size)
>  {
> -	return mod_lineinfo_lines_off(num_entries) + num_entries * sizeof(u32);
> +	return mod_lineinfo_data_off(num_blocks) + data_size;
>  }
>  
> -static inline u32 mod_lineinfo_filenames_off(u32 num_entries, u32 num_files)
> +static inline u32 mod_lineinfo_filenames_off(u32 num_blocks, u32 data_size,
> +					     u32 num_files)
>  {
> -	return mod_lineinfo_file_offsets_off(num_entries) +
> +	return mod_lineinfo_file_offsets_off(num_blocks, data_size) +
>  	       num_files * sizeof(u32);
>  }
>  

I wonder if these headers could use a slightly simpler representation,
with each part represented with its offset from header start and total
size in bytes, a bit like flattened devicetrees. So like, blocks_offset,
blocks_size, files_offset, files_size...

This would make the assembly generation below more readable, and IMO
make understanding offset and array bound calculations way simpler, at
the cost of a few extra words in the header.

(Re: array bounds, I know there are easier ways to break the kernel
intentionally if you're writing kernel code. but these things that run
in the "ouch something bad happened" cases really should be a bit more
defensive against possibly bad data, esp. in dealing with loadable
modules. I haven't looked closely to the in-kernel lookup code, but I
don't see much sanity checks against lineinfo data? I *think* for
badly-sorted binary search just spits out a nonsensical offset, but I
really don't want to find out what happens to the whole
binary-then-linear search code with negative sizes or out-of-bounds
offsets or something like that.)

> +/* Zigzag encoding: map signed to unsigned so small magnitudes are small */
> +static inline u32 zigzag_encode(int32_t v)
> +{
> +	return ((u32)v << 1) ^ (u32)(v >> 31);
> +}
> +
> +static inline int32_t zigzag_decode(u32 v)
> +{
> +	return (int32_t)((v >> 1) ^ -(v & 1));
> +}
> +
> +/*
> + * Read a ULEB128 varint from a byte stream.
> + * Returns the decoded value and advances *pos past the encoded bytes.
> + * If *pos would exceed 'end', returns 0 and sets *pos = end (safe for
> + * NMI/panic context -- no crash, just a missed annotation).

What does that last bit mean...?

> + */
> +static inline u32 lineinfo_read_uleb128(const u8 *data, u32 *pos, u32 end)
> +{
> +	u32 result = 0;
> +	unsigned int shift = 0;
> +
> +	while (*pos < end) {
> +		u8 byte = data[*pos];
> +		(*pos)++;
> +		result |= (u32)(byte & 0x7f) << shift;
> +		if (!(byte & 0x80))
> +			return result;
> +		shift += 7;
> +		if (shift >= 32) {
> +			/* Malformed -- skip remaining continuation bytes */
> +			while (*pos < end && (data[*pos] & 0x80))
> +				(*pos)++;
> +			if (*pos < end)
> +				(*pos)++;
> +			return result;
> +		}
> +	}
> +	return result;
> +}
> +
> +/* Write a ULEB128 varint -- build tool only */
> +#ifndef __KERNEL__
> +static inline unsigned int lineinfo_write_uleb128(u8 *buf, u32 value)
> +{
> +	unsigned int len = 0;
> +
> +	do {
> +		u8 byte = value & 0x7f;
> +
> +		value >>= 7;
> +		if (value)
> +			byte |= 0x80;
> +		buf[len++] = byte;
> +	} while (value);
> +	return len;
> +}
> +#endif /* !__KERNEL__ */
> +
>  #endif /* _LINUX_MOD_LINEINFO_H */
> diff --git a/init/Kconfig b/init/Kconfig
> index bf53275bc405a..6e3795b3dbd62 100644
> --- a/init/Kconfig
> +++ b/init/Kconfig
> @@ -2065,8 +2065,9 @@ config KALLSYMS_LINEINFO
>  	    anon_vma_clone+0x2ed/0xcf0 (mm/rmap.c:412)
>  
>  	  This requires elfutils (libdw-dev/elfutils-devel) on the build host.
> -	  Adds approximately 44MB to a typical kernel image (10 bytes per
> -	  DWARF line-table entry, ~4.6M entries for a typical config).
> +	  Adds approximately 10-15MB to a typical kernel image (~2-3 bytes
> +	  per entry after delta compression, ~4.6M entries for a typical
> +	  config).
>  
>  	  If unsure, say N.
>  
> @@ -2079,7 +2080,8 @@ config KALLSYMS_LINEINFO_MODULES
>  	  so stack traces from module code include (file.c:123) annotations.
>  
>  	  Requires elfutils (libdw-dev/elfutils-devel) on the build host.
> -	  Increases .ko sizes by approximately 10 bytes per DWARF line entry.
> +	  Increases .ko sizes by approximately 2-3 bytes per DWARF line
> +	  entry after delta compression.
>  
>  	  If unsure, say N.
>  

(Same as above, maybe use percentages when talking about sizes?)

> diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
> index cea74992e5427..de4aa8fcfd69d 100644
> --- a/kernel/kallsyms.c
> +++ b/kernel/kallsyms.c
> @@ -468,14 +468,20 @@ static int append_buildid(char *buffer,   const char *modname,
>  #endif /* CONFIG_STACKTRACE_BUILD_ID */
>  
>  #ifdef CONFIG_KALLSYMS_LINEINFO
> +#include <linux/mod_lineinfo.h>
> +
>  bool kallsyms_lookup_lineinfo(unsigned long addr, unsigned long sym_start,
>  			      const char **file, unsigned int *line)
>  {

[...]

> diff --git a/kernel/kallsyms_internal.h b/kernel/kallsyms_internal.h
> index 868a1d5035212..691be44440395 100644
> --- a/kernel/kallsyms_internal.h
> +++ b/kernel/kallsyms_internal.h
> @@ -17,10 +17,11 @@ extern const u8 kallsyms_seqs_of_names[];
>  
>  #ifdef CONFIG_KALLSYMS_LINEINFO
>  extern const u32 lineinfo_num_entries;
> -extern const u32 lineinfo_addrs[];
> -extern const u16 lineinfo_file_ids[];
> -extern const u32 lineinfo_lines[];
>  extern const u32 lineinfo_num_files;
> +extern const u32 lineinfo_num_blocks;
> +extern const u32 lineinfo_block_addrs[];
> +extern const u32 lineinfo_block_offsets[];
> +extern const u8  lineinfo_data[];
>  extern const u32 lineinfo_file_offsets[];
>  extern const char lineinfo_filenames[];
>  #endif
> diff --git a/kernel/module/kallsyms.c b/kernel/module/kallsyms.c
> index 7af414bd65e79..0ead1bb69de4e 100644
> --- a/kernel/module/kallsyms.c
> +++ b/kernel/module/kallsyms.c
> @@ -512,15 +512,19 @@ bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
>  {

This and kallsyms_lookup_lineinfo() above look like almost exactly the
same code twice. Some refactoring would be nice, just so that others
don't have to read the same code twice, or worse, change the same code
twice later on.

[...]

> diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c
> index 609de59f47ffd..9507ed9bcbe55 100644
> --- a/scripts/gen_lineinfo.c
> +++ b/scripts/gen_lineinfo.c
> @@ -8,6 +8,9 @@
>   * file containing sorted lookup tables that the kernel uses to annotate
>   * stack traces with source file:line information.
>   *
> + * The output uses a block-indexed, delta-encoded, ULEB128-compressed format
> + * for ~3-4x size reduction compared to flat arrays.
> + *
>   * Requires libdw from elfutils.
>   */
>  
> @@ -53,6 +56,15 @@ static struct file_entry *files;
>  static unsigned int num_files;
>  static unsigned int files_capacity;
>  
> +/* Compressed output */
> +static unsigned char *compressed_data;
> +static unsigned int compressed_size;
> +static unsigned int compressed_capacity;
> +
> +static unsigned int *block_addrs;
> +static unsigned int *block_offsets;
> +static unsigned int num_blocks;
> +
>  #define FILE_HASH_BITS 13
>  #define FILE_HASH_SIZE (1 << FILE_HASH_BITS)
>  
> @@ -352,6 +364,93 @@ static void deduplicate(void)
>  	num_entries = j + 1;
>  }
>  
> +static void compressed_ensure(unsigned int need)
> +{
> +	if (compressed_size + need <= compressed_capacity)
> +		return;
> +	compressed_capacity = compressed_capacity ? compressed_capacity * 2 : 1024 * 1024;
> +	while (compressed_capacity < compressed_size + need)
> +		compressed_capacity *= 2;
> +	compressed_data = realloc(compressed_data, compressed_capacity);
> +	if (!compressed_data) {
> +		fprintf(stderr, "out of memory\n");
> +		exit(1);
> +	}
> +}
> +
> +static void compress_entries(void)
> +{
> +	unsigned int i, block;
> +
> +	if (num_entries == 0) {
> +		num_blocks = 0;
> +		return;
> +	}
> +
> +	num_blocks = (num_entries + LINEINFO_BLOCK_ENTRIES - 1) / LINEINFO_BLOCK_ENTRIES;
> +	block_addrs = calloc(num_blocks, sizeof(*block_addrs));
> +	block_offsets = calloc(num_blocks, sizeof(*block_offsets));
> +	if (!block_addrs || !block_offsets) {
> +		fprintf(stderr, "out of memory\n");
> +		exit(1);
> +	}
> +
> +	for (block = 0; block < num_blocks; block++) {
> +		unsigned int base = block * LINEINFO_BLOCK_ENTRIES;
> +		unsigned int count = num_entries - base;
> +		unsigned int prev_addr, prev_file_id, prev_line;
> +		unsigned char buf[10]; /* max 5 bytes per ULEB128 */
> +
> +		if (count > LINEINFO_BLOCK_ENTRIES)
> +			count = LINEINFO_BLOCK_ENTRIES;
> +
> +		block_addrs[block] = entries[base].offset;
> +		block_offsets[block] = compressed_size;
> +
> +		/* Entry 0: file_id (ULEB128), line (ULEB128) */
> +		compressed_ensure(20);
> +		compressed_size += lineinfo_write_uleb128(
> +			compressed_data + compressed_size,
> +			entries[base].file_id);
> +		compressed_size += lineinfo_write_uleb128(
> +			compressed_data + compressed_size,
> +			entries[base].line);
> +
> +		prev_addr = entries[base].offset;
> +		prev_file_id = entries[base].file_id;
> +		prev_line = entries[base].line;
> +
> +		/* Entries 1..N: deltas */
> +		for (i = 1; i < count; i++) {
> +			unsigned int idx = base + i;
> +			unsigned int addr_delta;
> +			int32_t file_delta, line_delta;
> +			unsigned int n;
> +
> +			addr_delta = entries[idx].offset - prev_addr;
> +			file_delta = (int32_t)entries[idx].file_id - (int32_t)prev_file_id;
> +			line_delta = (int32_t)entries[idx].line - (int32_t)prev_line;
> +
> +			compressed_ensure(15);
> +			n = lineinfo_write_uleb128(buf, addr_delta);
> +			memcpy(compressed_data + compressed_size, buf, n);
> +			compressed_size += n;
> +
> +			n = lineinfo_write_uleb128(buf, zigzag_encode(file_delta));
> +			memcpy(compressed_data + compressed_size, buf, n);
> +			compressed_size += n;
> +
> +			n = lineinfo_write_uleb128(buf, zigzag_encode(line_delta));
> +			memcpy(compressed_data + compressed_size, buf, n);
> +			compressed_size += n;
> +
> +			prev_addr = entries[idx].offset;
> +			prev_file_id = entries[idx].file_id;
> +			prev_line = entries[idx].line;
> +		}
> +	}
> +}
> +
>  static void compute_file_offsets(void)
>  {
>  	unsigned int offset = 0;
> @@ -395,28 +494,40 @@ static void output_assembly(void)
>  	printf("lineinfo_num_files:\n");
>  	printf("\t.long %u\n\n", num_files);
>  
> -	/* Sorted address offsets from _text */
> -	printf("\t.globl lineinfo_addrs\n");
> +	/* Number of blocks */
> +	printf("\t.globl lineinfo_num_blocks\n");
>  	printf("\t.balign 4\n");
> -	printf("lineinfo_addrs:\n");
> -	for (unsigned int i = 0; i < num_entries; i++)
> -		printf("\t.long 0x%x\n", entries[i].offset);
> -	printf("\n");
> +	printf("lineinfo_num_blocks:\n");
> +	printf("\t.long %u\n\n", num_blocks);
>  
> -	/* File IDs, parallel to addrs (u16 -- supports up to 65535 files) */
> -	printf("\t.globl lineinfo_file_ids\n");
> -	printf("\t.balign 2\n");
> -	printf("lineinfo_file_ids:\n");
> -	for (unsigned int i = 0; i < num_entries; i++)
> -		printf("\t.short %u\n", entries[i].file_id);
> +	/* Block first-addresses for binary search */
> +	printf("\t.globl lineinfo_block_addrs\n");
> +	printf("\t.balign 4\n");
> +	printf("lineinfo_block_addrs:\n");
> +	for (unsigned int i = 0; i < num_blocks; i++)
> +		printf("\t.long 0x%x\n", block_addrs[i]);
>  	printf("\n");
>  
> -	/* Line numbers, parallel to addrs */
> -	printf("\t.globl lineinfo_lines\n");
> +	/* Block byte offsets into compressed stream */
> +	printf("\t.globl lineinfo_block_offsets\n");
>  	printf("\t.balign 4\n");
> -	printf("lineinfo_lines:\n");
> -	for (unsigned int i = 0; i < num_entries; i++)
> -		printf("\t.long %u\n", entries[i].line);
> +	printf("lineinfo_block_offsets:\n");
> +	for (unsigned int i = 0; i < num_blocks; i++)
> +		printf("\t.long %u\n", block_offsets[i]);
> +	printf("\n");
> +
> +	/* Compressed data stream */
> +	printf("\t.globl lineinfo_data\n");
> +	printf("lineinfo_data:\n");
> +	for (unsigned int i = 0; i < compressed_size; i++) {
> +		if ((i % 16) == 0)
> +			printf("\t.byte ");
> +		else
> +			printf(",");
> +		printf("0x%02x", compressed_data[i]);
> +		if ((i % 16) == 15 || i == compressed_size - 1)
> +			printf("\n");
> +	}
>  	printf("\n");
>  

Note how compute_file_offsets() gives symbol names to the data it
generates. Meanwhile...

>  	/* File string offset table */
> @@ -450,33 +561,38 @@ static void output_module_assembly(void)
>  
>  	printf("\t.section .mod_lineinfo, \"a\"\n\n");
>  
> -	/* Header: num_entries, num_files, filenames_size, reserved */
> +	/* Header: num_entries, num_files, filenames_size, num_blocks, data_size, reserved */
>  	printf("\t.balign 4\n");
>  	printf("\t.long %u\n", num_entries);
>  	printf("\t.long %u\n", num_files);
>  	printf("\t.long %u\n", filenames_size);
> +	printf("\t.long %u\n", num_blocks);
> +	printf("\t.long %u\n", compressed_size);
>  	printf("\t.long 0\n\n");
>  
> -	/* addrs[] */
> -	for (unsigned int i = 0; i < num_entries; i++)
> -		printf("\t.long 0x%x\n", entries[i].offset);
> -	if (num_entries)
> +	/* block_addrs[] */
> +	for (unsigned int i = 0; i < num_blocks; i++)
> +		printf("\t.long 0x%x\n", block_addrs[i]);
> +	if (num_blocks)
>  		printf("\n");
>  
For the modules, it's comments in the C code that doesn't end up in the
assembly.

I'm thinking we could have something like:

	printf(".Lmod_lineinfo_block_addrs:\n")
	for (unsigned int i = 0; i < num_entries; i++)
		printf("\t.long 0x%x\n", ...);
	printf("\n")

(And similarly for the other blocks of data.)

This would make the assembly a tiny bit more readable, get rid of the
kinda ugly printf("\n") checks and prints, and would be useful for the
offset + size header format I mentioned earlier.

> -	/* file_ids[] */
> -	for (unsigned int i = 0; i < num_entries; i++)
> -		printf("\t.short %u\n", entries[i].file_id);
> -
> -	/* Padding to align lines[] to 4 bytes */
> -	if (num_entries & 1)
> -		printf("\t.short 0\n");
> -	if (num_entries)
> +	/* block_offsets[] */
> +	for (unsigned int i = 0; i < num_blocks; i++)
> +		printf("\t.long %u\n", block_offsets[i]);
> +	if (num_blocks)
>  		printf("\n");
>  
> -	/* lines[] */
> -	for (unsigned int i = 0; i < num_entries; i++)
> -		printf("\t.long %u\n", entries[i].line);
> -	if (num_entries)
> +	/* compressed data[] */
> +	for (unsigned int i = 0; i < compressed_size; i++) {
> +		if ((i % 16) == 0)
> +			printf("\t.byte ");
> +		else
> +			printf(",");
> +		printf("0x%02x", compressed_data[i]);
> +		if ((i % 16) == 15 || i == compressed_size - 1)
> +			printf("\n");
> +	}
> +	if (compressed_size)
>  		printf("\n");
>  

Also, maybe we can use .uleb128/.sleb128 here, and generate something like:

	printf("\t.sleb128 %#x - %#x\n", cur_addr, prev_addr); 

And have the assembler do the subtraction and encoding for us? If that
works it should significantly simplify the compression code above.

Speaking of... Why do we use uleb128(zigzag(num)) and not just sleb128(num)?

[...]

> @@ -558,10 +674,11 @@ int main(int argc, char *argv[])
>  			skipped_overflow);
>  
>  	deduplicate();
> +	compress_entries();
>  	compute_file_offsets();
>  
> -	fprintf(stderr, "lineinfo: %u entries, %u files\n",
> -		num_entries, num_files);
> +	fprintf(stderr, "lineinfo: %u entries, %u files, %u blocks, %u compressed bytes\n",
> +		num_entries, num_files, num_blocks, compressed_size);
>  
>  	if (module_mode)
>  		output_module_assembly();
> @@ -577,6 +694,9 @@ int main(int argc, char *argv[])
>  	for (unsigned int i = 0; i < num_files; i++)
>  		free(files[i].name);
>  	free(files);
> +	free(compressed_data);
> +	free(block_addrs);
> +	free(block_offsets);
>  
>  	return 0;
>  }
> diff --git a/scripts/kallsyms.c b/scripts/kallsyms.c
> index 42662c4fbc6c9..94fbdad3df7c6 100644
> --- a/scripts/kallsyms.c
> +++ b/scripts/kallsyms.c
> @@ -80,11 +80,12 @@ static bool is_ignored_symbol(const char *name, char type)
>  {
>  	/* Ignore lineinfo symbols for kallsyms pass stability */
>  	static const char * const lineinfo_syms[] = {
> -		"lineinfo_addrs",
> -		"lineinfo_file_ids",
> +		"lineinfo_block_addrs",
> +		"lineinfo_block_offsets",
> +		"lineinfo_data",
>  		"lineinfo_file_offsets",
>  		"lineinfo_filenames",
> -		"lineinfo_lines",
> +		"lineinfo_num_blocks",
>  		"lineinfo_num_entries",
>  		"lineinfo_num_files",
>  	};
> diff --git a/scripts/link-vmlinux.sh b/scripts/link-vmlinux.sh
> index 640209f2e9eb9..3c122cf9b95c5 100755
> --- a/scripts/link-vmlinux.sh
> +++ b/scripts/link-vmlinux.sh
> @@ -235,12 +235,16 @@ lineinfo_num_entries:
>  	.balign 4
>  lineinfo_num_files:
>  	.long 0
> -	.globl lineinfo_addrs
> -lineinfo_addrs:
> -	.globl lineinfo_file_ids
> -lineinfo_file_ids:
> -	.globl lineinfo_lines
> -lineinfo_lines:
> +	.globl lineinfo_num_blocks
> +	.balign 4
> +lineinfo_num_blocks:
> +	.long 0
> +	.globl lineinfo_block_addrs
> +lineinfo_block_addrs:
> +	.globl lineinfo_block_offsets
> +lineinfo_block_offsets:
> +	.globl lineinfo_data
> +lineinfo_data:
>  	.globl lineinfo_file_offsets
>  lineinfo_file_offsets:
>  	.globl lineinfo_filenames

The contents of this .tmp_lineinfo.S is fixed, so it feels like it could
just be a proper file, instead of something "generated" dynamically.

Thanks,
Vivian "dramforever" Wang


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 3/3] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
  2026-03-11  3:34   ` Vivian Wang
@ 2026-03-11  4:13     ` Vivian Wang
  2026-03-11 14:49     ` Sasha Levin
  1 sibling, 0 replies; 21+ messages in thread
From: Vivian Wang @ 2026-03-11  4:13 UTC (permalink / raw)
  To: Sasha Levin, Andrew Morton, Masahiro Yamada, Luis Chamberlain,
	Linus Torvalds, Richard Weinberger, Juergen Gross,
	Geert Uytterhoeven, James Bottomley
  Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
	Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
	Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, linux-kernel,
	linux-kbuild, linux-modules, linux-doc

On 3/11/26 11:34, Vivian Wang wrote:
> Hi Sasha,
>
> I've been trying this out and AFAICT this does work perfectly. Thank you
> for this.
>
> There are a few oddities I found:
>
> Firstly I've been building with something like O=_riscv out of
> convenience, and the file names have an extra ../ in the front. (This is
> just me exiting out of init=/bin/sh.)
>
> [    2.317268] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000
> [    2.320283] CPU: 0 UID: 0 PID: 1 Comm: sh Not tainted 7.0.0-rc3-00004-g8ad18f1a1a2f #31 PREEMPTLAZY 
> [    2.322048] Hardware name: riscv-virtio,qemu (DT)
> [    2.323220] Call Trace:
> [    2.324465] [<ffffffff800172a8>] dump_backtrace+0x1c/0x24 (../arch/riscv/kernel/stacktrace.c:150)
> [    2.329061] [<ffffffff8000241e>] show_stack+0x2a/0x34 (../arch/riscv/kernel/stacktrace.c:156)
> [    2.330334] [<ffffffff8000fe32>] dump_stack_lvl+0x4a/0x68 (../lib/dump_stack.c:122)
> [    2.331462] [<ffffffff8000fe64>] dump_stack+0x14/0x1c (../lib/dump_stack.c:130)
> [    2.332571] [<ffffffff80002a88>] vpanic+0x108/0x2bc (../kernel/panic.c:651)
> [    2.333674] [<ffffffff80002c6e>] panic+0x32/0x34 (../kernel/panic.c:787)
> [    2.334427] [<ffffffff8002e97a>] do_exit+0x7ee/0x7f4 (../kernel/exit.c:930)
> [    2.335194] [<ffffffff8002eade>] do_group_exit+0x1a/0x88 (../kernel/exit.c:1099)
> [    2.335945] [<ffffffff8002eb62>] __riscv_sys_exit_group+0x16/0x18 (../kernel/exit.c:1129)
> [    2.336763] [<ffffffff80b3e868>] do_trap_ecall_u+0x260/0x45c (../arch/riscv/include/asm/syscall.h:112)
> [    2.337765] [<ffffffff80b4c034>] handle_exception+0x168/0x174 (../arch/riscv/kernel/entry.S:233)
>
> This is fine by me, but I've seen mentions of O= builds but I'm not sure
> if it's expected.
>
> Also, toggling CONFIG_KALLSYMS_LINEINFO seems to rebuild every single
> file. I haven't debugged why, but is this expected?
>
> I have a few ideas about the code as well. Since this patch 3 touches
> most of the files involved, I'll just dump my thoughts on the whole
> series here. I want to note that I haven't read the RFC thread too
> carefully, but I don't think there were many comments on the implementation.

Uh, oops, I just realized I was replying on v1 not v2.

Most of my idea should still apply. One notable idea was the module
lineinfo header offset/size one. I see that one of the things addressed
in v2 was the alignment after compressed data. If we'd been using
offset/size in the header it would be just, code-wise, one extra .balign
4 and done. No need to fiddle with alignment when decoding in
mod_lineinfo_file_offsets_off().

Sorry for the inconvenience.


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 3/3] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
  2026-03-11  3:34   ` Vivian Wang
  2026-03-11  4:13     ` Vivian Wang
@ 2026-03-11 14:49     ` Sasha Levin
  2026-03-12  2:03       ` Vivian Wang
  1 sibling, 1 reply; 21+ messages in thread
From: Sasha Levin @ 2026-03-11 14:49 UTC (permalink / raw)
  To: Vivian Wang
  Cc: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley, Jonathan Corbet, Nathan Chancellor,
	Nicolas Schier, Petr Pavlu, Daniel Gomez, Greg KH, Petr Mladek,
	Steven Rostedt, Kees Cook, Peter Zijlstra, Thorsten Leemhuis,
	Vlastimil Babka, linux-kernel, linux-kbuild, linux-modules,
	linux-doc

Thanks for the review!

On Wed, Mar 11, 2026 at 11:34:24AM +0800, Vivian Wang wrote:
>Hi Sasha,
>
>I've been trying this out and AFAICT this does work perfectly. Thank you
>for this.
>
>There are a few oddities I found:
>
>Firstly I've been building with something like O=_riscv out of
>convenience, and the file names have an extra ../ in the front. (This is
>just me exiting out of init=/bin/sh.)
>
>[    2.317268] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000
>[    2.320283] CPU: 0 UID: 0 PID: 1 Comm: sh Not tainted 7.0.0-rc3-00004-g8ad18f1a1a2f #31 PREEMPTLAZY
>[    2.322048] Hardware name: riscv-virtio,qemu (DT)
>[    2.323220] Call Trace:
>[    2.324465] [<ffffffff800172a8>] dump_backtrace+0x1c/0x24 (../arch/riscv/kernel/stacktrace.c:150)
>[    2.329061] [<ffffffff8000241e>] show_stack+0x2a/0x34 (../arch/riscv/kernel/stacktrace.c:156)
>[    2.330334] [<ffffffff8000fe32>] dump_stack_lvl+0x4a/0x68 (../lib/dump_stack.c:122)
>[    2.331462] [<ffffffff8000fe64>] dump_stack+0x14/0x1c (../lib/dump_stack.c:130)
>[    2.332571] [<ffffffff80002a88>] vpanic+0x108/0x2bc (../kernel/panic.c:651)
>[    2.333674] [<ffffffff80002c6e>] panic+0x32/0x34 (../kernel/panic.c:787)
>[    2.334427] [<ffffffff8002e97a>] do_exit+0x7ee/0x7f4 (../kernel/exit.c:930)
>[    2.335194] [<ffffffff8002eade>] do_group_exit+0x1a/0x88 (../kernel/exit.c:1099)
>[    2.335945] [<ffffffff8002eb62>] __riscv_sys_exit_group+0x16/0x18 (../kernel/exit.c:1129)
>[    2.336763] [<ffffffff80b3e868>] do_trap_ecall_u+0x260/0x45c (../arch/riscv/include/asm/syscall.h:112)
>[    2.337765] [<ffffffff80b4c034>] handle_exception+0x168/0x174 (../arch/riscv/kernel/entry.S:233)
>
>This is fine by me, but I've seen mentions of O= builds but I'm not sure
>if it's expected.

Could you try v2 and see if it makes it prettier? I tried to tackle this :)

>Also, toggling CONFIG_KALLSYMS_LINEINFO seems to rebuild every single
>file. I haven't debugged why, but is this expected?

I think that this is because we increase KSYM_SYMBOL_LEN when lineinfo is
enabled. I suppose we can just increase the size irregardless of whether
lineinfo is enabled and ignore the waste?

Or, folks really won't be toggling this option too often for the rebuilds to
matter too much, so we can just enjoy the savings?

>I have a few ideas about the code as well. Since this patch 3 touches
>most of the files involved, I'll just dump my thoughts on the whole
>series here. I want to note that I haven't read the RFC thread too
>carefully, but I don't think there were many comments on the implementation.
>
>On 3/4/26 02:21, Sasha Levin wrote:
>> Replace the flat uncompressed parallel arrays (lineinfo_addrs[],
>> lineinfo_file_ids[], lineinfo_lines[]) with a block-indexed,
>> delta-encoded, ULEB128 varint compressed format.
>>
>> The sorted address array has small deltas between consecutive entries
>> (typically 1-50 bytes), file IDs have high locality (delta often 0,
>> same file), and line numbers change slowly.  Delta-encoding followed
>> by ULEB128 varint compression shrinks most values from 4 bytes to 1.
>>
>> Entries are grouped into blocks of 64.  A small uncompressed block
>> index (first addr + byte offset per block) enables O(log(N/64)) binary
>> search, followed by sequential decode of at most 64 varints within the
>> matching block.  All decode state lives on the stack -- zero
>> allocations, still safe for NMI/panic context.
>>
>> Measured on a defconfig+debug x86_64 build (3,017,154 entries, 4,822
>> source files, 47,144 blocks):
>>
>>   Before (flat arrays):
>>     lineinfo_addrs[]    12,068,616 bytes (u32 x 3.0M)
>>     lineinfo_file_ids[]  6,034,308 bytes (u16 x 3.0M)
>>     lineinfo_lines[]    12,068,616 bytes (u32 x 3.0M)
>>     Total:              30,171,540 bytes (28.8 MiB, 10.0 bytes/entry)
>>
>>   After (block-indexed delta + ULEB128):
>>     lineinfo_block_addrs[]    188,576 bytes (184 KiB)
>>     lineinfo_block_offsets[]  188,576 bytes (184 KiB)
>>     lineinfo_data[]        10,926,128 bytes (10.4 MiB)
>>     Total:                 11,303,280 bytes (10.8 MiB, 3.7 bytes/entry)
>>
>>   Savings: 18.0 MiB (2.7x reduction)
>>
>> Booted in QEMU and verified with SysRq-l that annotations still work:
>>
>>   default_idle+0x9/0x10 (arch/x86/kernel/process.c:767)
>>   default_idle_call+0x6c/0xb0 (kernel/sched/idle.c:122)
>>   do_idle+0x335/0x490 (kernel/sched/idle.c:191)
>>   cpu_startup_entry+0x4e/0x60 (kernel/sched/idle.c:429)
>>   rest_init+0x1aa/0x1b0 (init/main.c:760)
>>
>> Suggested-by: Juergen Gross <jgross@suse.com>
>> Assisted-by: Claude:claude-opus-4-6
>> Signed-off-by: Sasha Levin <sashal@kernel.org>
>> ---
>>  .../admin-guide/kallsyms-lineinfo.rst         |   7 +-
>>  include/linux/mod_lineinfo.h                  | 103 ++++++++--
>>  init/Kconfig                                  |   8 +-
>>  kernel/kallsyms.c                             |  91 +++++++--
>>  kernel/kallsyms_internal.h                    |   7 +-
>>  kernel/module/kallsyms.c                      | 107 +++++++---
>>  scripts/gen_lineinfo.c                        | 192 ++++++++++++++----
>>  scripts/kallsyms.c                            |   7 +-
>>  scripts/link-vmlinux.sh                       |  16 +-
>>  9 files changed, 423 insertions(+), 115 deletions(-)
>>
>> diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
>> index 21450569d5324..fe92c5dde16b3 100644
>> --- a/Documentation/admin-guide/kallsyms-lineinfo.rst
>> +++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
>> @@ -76,10 +76,11 @@ Memory Overhead
>>  ===============
>>
>>  The vmlinux lineinfo tables are stored in ``.rodata`` and typically add
>> -approximately 44 MiB to the kernel image for a standard configuration
>> -(~4.6 million DWARF line entries, ~10 bytes per entry after deduplication).
>> +approximately 10-15 MiB to the kernel image for a standard configuration
>> +(~4.6 million DWARF line entries, ~2-3 bytes per entry after delta
>> +compression).
>>
>> -Per-module lineinfo adds approximately 10 bytes per DWARF line entry to each
>> +Per-module lineinfo adds approximately 2-3 bytes per DWARF line entry to each
>>  ``.ko`` file.
>>
>
>Maybe this could be given in terms of percentages? It wasn't obvious to
>me what 10-15 MiB amounts to.
>
>On riscv64, I'm seeing a 24.2 MiB to 30.2 MiB increase in
>arch/riscv/boot/Image size on an approximately defconfig+mod2noconfig
>build, which is about a 25% increase. I haven't checked yet, but if 25%
>is similar to what other archs get, that's a more useful figure than
>10-15 MiB, given that the size increase is correlated to the total
>amount of code linked into the kernel/module.

I ended up giving an example instead of percentages because it seemed to vary
wildly between configs and archs. For example, defconfig on x86 yields a 15%
increase compared to the 25% you see with your config on riscv, compared to a
39% increase with defconfig on riscv.

>>  Known Limitations
>> diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h
>> index d62e9608f0f82..ab758acfadceb 100644
>> --- a/include/linux/mod_lineinfo.h
>> +++ b/include/linux/mod_lineinfo.h
>> @@ -8,13 +8,19 @@
>>   *
>>   * Section layout (all values in target-native endianness):
>>   *
>> - *   struct mod_lineinfo_header     (16 bytes)
>> - *   u32 addrs[num_entries]         -- offsets from .text base, sorted
>> - *   u16 file_ids[num_entries]      -- parallel to addrs
>> - *   <2-byte pad if num_entries is odd>
>> - *   u32 lines[num_entries]         -- parallel to addrs
>> + *   struct mod_lineinfo_header     (24 bytes)
>> + *   u32 block_addrs[num_blocks]    -- first addr per block, for binary search
>> + *   u32 block_offsets[num_blocks]  -- byte offset into compressed data stream
>> + *   u8  data[data_size]            -- ULEB128 delta-compressed entries
>>   *   u32 file_offsets[num_files]    -- byte offset into filenames[]
>>   *   char filenames[filenames_size] -- concatenated NUL-terminated strings
>> + *
>> + * Compressed stream format (per block of LINEINFO_BLOCK_ENTRIES entries):
>> + *   Entry 0: file_id (ULEB128), line (ULEB128)
>> + *            addr is in block_addrs[]
>> + *   Entry 1..N: addr_delta (ULEB128),
>> + *               file_id_delta (zigzag-encoded ULEB128),
>> + *               line_delta (zigzag-encoded ULEB128)
>>   */
>>  #ifndef _LINUX_MOD_LINEINFO_H
>>  #define _LINUX_MOD_LINEINFO_H
>> @@ -25,44 +31,107 @@
>>  #include <stdint.h>
>>  typedef uint32_t u32;
>>  typedef uint16_t u16;
>> +typedef uint8_t  u8;
>>  #endif
>>
>> +#define LINEINFO_BLOCK_ENTRIES 64
>> +
>>  struct mod_lineinfo_header {
>>  	u32 num_entries;
>>  	u32 num_files;
>>  	u32 filenames_size;	/* total bytes of concatenated filenames */
>> +	u32 num_blocks;
>> +	u32 data_size;		/* total bytes of compressed data stream */
>>  	u32 reserved;		/* padding, must be 0 */
>>  };
>>
>>  /* Offset helpers: compute byte offset from start of section to each array */
>>
>> -static inline u32 mod_lineinfo_addrs_off(void)
>> +static inline u32 mod_lineinfo_block_addrs_off(void)
>>  {
>>  	return sizeof(struct mod_lineinfo_header);
>>  }
>>
>> -static inline u32 mod_lineinfo_file_ids_off(u32 num_entries)
>> +static inline u32 mod_lineinfo_block_offsets_off(u32 num_blocks)
>>  {
>> -	return mod_lineinfo_addrs_off() + num_entries * sizeof(u32);
>> +	return mod_lineinfo_block_addrs_off() + num_blocks * sizeof(u32);
>>  }
>>
>> -static inline u32 mod_lineinfo_lines_off(u32 num_entries)
>> +static inline u32 mod_lineinfo_data_off(u32 num_blocks)
>>  {
>> -	/* u16 file_ids[] may need 2-byte padding to align lines[] to 4 bytes */
>> -	u32 off = mod_lineinfo_file_ids_off(num_entries) +
>> -		  num_entries * sizeof(u16);
>> -	return (off + 3) & ~3u;
>> +	return mod_lineinfo_block_offsets_off(num_blocks) +
>> +	       num_blocks * sizeof(u32);
>>  }
>>
>> -static inline u32 mod_lineinfo_file_offsets_off(u32 num_entries)
>> +static inline u32 mod_lineinfo_file_offsets_off(u32 num_blocks, u32 data_size)
>>  {
>> -	return mod_lineinfo_lines_off(num_entries) + num_entries * sizeof(u32);
>> +	return mod_lineinfo_data_off(num_blocks) + data_size;
>>  }
>>
>> -static inline u32 mod_lineinfo_filenames_off(u32 num_entries, u32 num_files)
>> +static inline u32 mod_lineinfo_filenames_off(u32 num_blocks, u32 data_size,
>> +					     u32 num_files)
>>  {
>> -	return mod_lineinfo_file_offsets_off(num_entries) +
>> +	return mod_lineinfo_file_offsets_off(num_blocks, data_size) +
>>  	       num_files * sizeof(u32);
>>  }
>>
>
>I wonder if these headers could use a slightly simpler representation,
>with each part represented with its offset from header start and total
>size in bytes, a bit like flattened devicetrees. So like, blocks_offset,
>blocks_size, files_offset, files_size...
>
>This would make the assembly generation below more readable, and IMO
>make understanding offset and array bound calculations way simpler, at
>the cost of a few extra words in the header.

Makes sense

>(Re: array bounds, I know there are easier ways to break the kernel
>intentionally if you're writing kernel code. but these things that run
>in the "ouch something bad happened" cases really should be a bit more
>defensive against possibly bad data, esp. in dealing with loadable
>modules. I haven't looked closely to the in-kernel lookup code, but I
>don't see much sanity checks against lineinfo data? I *think* for
>badly-sorted binary search just spits out a nonsensical offset, but I
>really don't want to find out what happens to the whole
>binary-then-linear search code with negative sizes or out-of-bounds
>offsets or something like that.)
>
>> +/* Zigzag encoding: map signed to unsigned so small magnitudes are small */
>> +static inline u32 zigzag_encode(int32_t v)
>> +{
>> +	return ((u32)v << 1) ^ (u32)(v >> 31);
>> +}
>> +
>> +static inline int32_t zigzag_decode(u32 v)
>> +{
>> +	return (int32_t)((v >> 1) ^ -(v & 1));
>> +}
>> +
>> +/*
>> + * Read a ULEB128 varint from a byte stream.
>> + * Returns the decoded value and advances *pos past the encoded bytes.
>> + * If *pos would exceed 'end', returns 0 and sets *pos = end (safe for
>> + * NMI/panic context -- no crash, just a missed annotation).
>
>What does that last bit mean...?

This goes back to your previous point about correctness and checks in the
lineinfo code :)

It just means that this function never faults or allocates. On bad input it
returns 0, so the worst case is a missing annotation, not a crash.

>> + */
>> +static inline u32 lineinfo_read_uleb128(const u8 *data, u32 *pos, u32 end)
>> +{
>> +	u32 result = 0;
>> +	unsigned int shift = 0;
>> +
>> +	while (*pos < end) {
>> +		u8 byte = data[*pos];
>> +		(*pos)++;
>> +		result |= (u32)(byte & 0x7f) << shift;
>> +		if (!(byte & 0x80))
>> +			return result;
>> +		shift += 7;
>> +		if (shift >= 32) {
>> +			/* Malformed -- skip remaining continuation bytes */
>> +			while (*pos < end && (data[*pos] & 0x80))
>> +				(*pos)++;
>> +			if (*pos < end)
>> +				(*pos)++;
>> +			return result;
>> +		}
>> +	}
>> +	return result;
>> +}
>> +
>> +/* Write a ULEB128 varint -- build tool only */
>> +#ifndef __KERNEL__
>> +static inline unsigned int lineinfo_write_uleb128(u8 *buf, u32 value)
>> +{
>> +	unsigned int len = 0;
>> +
>> +	do {
>> +		u8 byte = value & 0x7f;
>> +
>> +		value >>= 7;
>> +		if (value)
>> +			byte |= 0x80;
>> +		buf[len++] = byte;
>> +	} while (value);
>> +	return len;
>> +}
>> +#endif /* !__KERNEL__ */
>> +
>>  #endif /* _LINUX_MOD_LINEINFO_H */
>> diff --git a/init/Kconfig b/init/Kconfig
>> index bf53275bc405a..6e3795b3dbd62 100644
>> --- a/init/Kconfig
>> +++ b/init/Kconfig
>> @@ -2065,8 +2065,9 @@ config KALLSYMS_LINEINFO
>>  	    anon_vma_clone+0x2ed/0xcf0 (mm/rmap.c:412)
>>
>>  	  This requires elfutils (libdw-dev/elfutils-devel) on the build host.
>> -	  Adds approximately 44MB to a typical kernel image (10 bytes per
>> -	  DWARF line-table entry, ~4.6M entries for a typical config).
>> +	  Adds approximately 10-15MB to a typical kernel image (~2-3 bytes
>> +	  per entry after delta compression, ~4.6M entries for a typical
>> +	  config).
>>
>>  	  If unsure, say N.
>>
>> @@ -2079,7 +2080,8 @@ config KALLSYMS_LINEINFO_MODULES
>>  	  so stack traces from module code include (file.c:123) annotations.
>>
>>  	  Requires elfutils (libdw-dev/elfutils-devel) on the build host.
>> -	  Increases .ko sizes by approximately 10 bytes per DWARF line entry.
>> +	  Increases .ko sizes by approximately 2-3 bytes per DWARF line
>> +	  entry after delta compression.
>>
>>  	  If unsure, say N.
>>
>
>(Same as above, maybe use percentages when talking about sizes?)
>
>> diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
>> index cea74992e5427..de4aa8fcfd69d 100644
>> --- a/kernel/kallsyms.c
>> +++ b/kernel/kallsyms.c
>> @@ -468,14 +468,20 @@ static int append_buildid(char *buffer,   const char *modname,
>>  #endif /* CONFIG_STACKTRACE_BUILD_ID */
>>
>>  #ifdef CONFIG_KALLSYMS_LINEINFO
>> +#include <linux/mod_lineinfo.h>
>> +
>>  bool kallsyms_lookup_lineinfo(unsigned long addr, unsigned long sym_start,
>>  			      const char **file, unsigned int *line)
>>  {
>
>[...]
>
>> diff --git a/kernel/kallsyms_internal.h b/kernel/kallsyms_internal.h
>> index 868a1d5035212..691be44440395 100644
>> --- a/kernel/kallsyms_internal.h
>> +++ b/kernel/kallsyms_internal.h
>> @@ -17,10 +17,11 @@ extern const u8 kallsyms_seqs_of_names[];
>>
>>  #ifdef CONFIG_KALLSYMS_LINEINFO
>>  extern const u32 lineinfo_num_entries;
>> -extern const u32 lineinfo_addrs[];
>> -extern const u16 lineinfo_file_ids[];
>> -extern const u32 lineinfo_lines[];
>>  extern const u32 lineinfo_num_files;
>> +extern const u32 lineinfo_num_blocks;
>> +extern const u32 lineinfo_block_addrs[];
>> +extern const u32 lineinfo_block_offsets[];
>> +extern const u8  lineinfo_data[];
>>  extern const u32 lineinfo_file_offsets[];
>>  extern const char lineinfo_filenames[];
>>  #endif
>> diff --git a/kernel/module/kallsyms.c b/kernel/module/kallsyms.c
>> index 7af414bd65e79..0ead1bb69de4e 100644
>> --- a/kernel/module/kallsyms.c
>> +++ b/kernel/module/kallsyms.c
>> @@ -512,15 +512,19 @@ bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
>>  {
>
>This and kallsyms_lookup_lineinfo() above look like almost exactly the
>same code twice. Some refactoring would be nice, just so that others
>don't have to read the same code twice, or worse, change the same code
>twice later on.

Makes sense

>[...]
>
>> diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c
>> index 609de59f47ffd..9507ed9bcbe55 100644
>> --- a/scripts/gen_lineinfo.c
>> +++ b/scripts/gen_lineinfo.c
>> @@ -8,6 +8,9 @@
>>   * file containing sorted lookup tables that the kernel uses to annotate
>>   * stack traces with source file:line information.
>>   *
>> + * The output uses a block-indexed, delta-encoded, ULEB128-compressed format
>> + * for ~3-4x size reduction compared to flat arrays.
>> + *
>>   * Requires libdw from elfutils.
>>   */
>>
>> @@ -53,6 +56,15 @@ static struct file_entry *files;
>>  static unsigned int num_files;
>>  static unsigned int files_capacity;
>>
>> +/* Compressed output */
>> +static unsigned char *compressed_data;
>> +static unsigned int compressed_size;
>> +static unsigned int compressed_capacity;
>> +
>> +static unsigned int *block_addrs;
>> +static unsigned int *block_offsets;
>> +static unsigned int num_blocks;
>> +
>>  #define FILE_HASH_BITS 13
>>  #define FILE_HASH_SIZE (1 << FILE_HASH_BITS)
>>
>> @@ -352,6 +364,93 @@ static void deduplicate(void)
>>  	num_entries = j + 1;
>>  }
>>
>> +static void compressed_ensure(unsigned int need)
>> +{
>> +	if (compressed_size + need <= compressed_capacity)
>> +		return;
>> +	compressed_capacity = compressed_capacity ? compressed_capacity * 2 : 1024 * 1024;
>> +	while (compressed_capacity < compressed_size + need)
>> +		compressed_capacity *= 2;
>> +	compressed_data = realloc(compressed_data, compressed_capacity);
>> +	if (!compressed_data) {
>> +		fprintf(stderr, "out of memory\n");
>> +		exit(1);
>> +	}
>> +}
>> +
>> +static void compress_entries(void)
>> +{
>> +	unsigned int i, block;
>> +
>> +	if (num_entries == 0) {
>> +		num_blocks = 0;
>> +		return;
>> +	}
>> +
>> +	num_blocks = (num_entries + LINEINFO_BLOCK_ENTRIES - 1) / LINEINFO_BLOCK_ENTRIES;
>> +	block_addrs = calloc(num_blocks, sizeof(*block_addrs));
>> +	block_offsets = calloc(num_blocks, sizeof(*block_offsets));
>> +	if (!block_addrs || !block_offsets) {
>> +		fprintf(stderr, "out of memory\n");
>> +		exit(1);
>> +	}
>> +
>> +	for (block = 0; block < num_blocks; block++) {
>> +		unsigned int base = block * LINEINFO_BLOCK_ENTRIES;
>> +		unsigned int count = num_entries - base;
>> +		unsigned int prev_addr, prev_file_id, prev_line;
>> +		unsigned char buf[10]; /* max 5 bytes per ULEB128 */
>> +
>> +		if (count > LINEINFO_BLOCK_ENTRIES)
>> +			count = LINEINFO_BLOCK_ENTRIES;
>> +
>> +		block_addrs[block] = entries[base].offset;
>> +		block_offsets[block] = compressed_size;
>> +
>> +		/* Entry 0: file_id (ULEB128), line (ULEB128) */
>> +		compressed_ensure(20);
>> +		compressed_size += lineinfo_write_uleb128(
>> +			compressed_data + compressed_size,
>> +			entries[base].file_id);
>> +		compressed_size += lineinfo_write_uleb128(
>> +			compressed_data + compressed_size,
>> +			entries[base].line);
>> +
>> +		prev_addr = entries[base].offset;
>> +		prev_file_id = entries[base].file_id;
>> +		prev_line = entries[base].line;
>> +
>> +		/* Entries 1..N: deltas */
>> +		for (i = 1; i < count; i++) {
>> +			unsigned int idx = base + i;
>> +			unsigned int addr_delta;
>> +			int32_t file_delta, line_delta;
>> +			unsigned int n;
>> +
>> +			addr_delta = entries[idx].offset - prev_addr;
>> +			file_delta = (int32_t)entries[idx].file_id - (int32_t)prev_file_id;
>> +			line_delta = (int32_t)entries[idx].line - (int32_t)prev_line;
>> +
>> +			compressed_ensure(15);
>> +			n = lineinfo_write_uleb128(buf, addr_delta);
>> +			memcpy(compressed_data + compressed_size, buf, n);
>> +			compressed_size += n;
>> +
>> +			n = lineinfo_write_uleb128(buf, zigzag_encode(file_delta));
>> +			memcpy(compressed_data + compressed_size, buf, n);
>> +			compressed_size += n;
>> +
>> +			n = lineinfo_write_uleb128(buf, zigzag_encode(line_delta));
>> +			memcpy(compressed_data + compressed_size, buf, n);
>> +			compressed_size += n;
>> +
>> +			prev_addr = entries[idx].offset;
>> +			prev_file_id = entries[idx].file_id;
>> +			prev_line = entries[idx].line;
>> +		}
>> +	}
>> +}
>> +
>>  static void compute_file_offsets(void)
>>  {
>>  	unsigned int offset = 0;
>> @@ -395,28 +494,40 @@ static void output_assembly(void)
>>  	printf("lineinfo_num_files:\n");
>>  	printf("\t.long %u\n\n", num_files);
>>
>> -	/* Sorted address offsets from _text */
>> -	printf("\t.globl lineinfo_addrs\n");
>> +	/* Number of blocks */
>> +	printf("\t.globl lineinfo_num_blocks\n");
>>  	printf("\t.balign 4\n");
>> -	printf("lineinfo_addrs:\n");
>> -	for (unsigned int i = 0; i < num_entries; i++)
>> -		printf("\t.long 0x%x\n", entries[i].offset);
>> -	printf("\n");
>> +	printf("lineinfo_num_blocks:\n");
>> +	printf("\t.long %u\n\n", num_blocks);
>>
>> -	/* File IDs, parallel to addrs (u16 -- supports up to 65535 files) */
>> -	printf("\t.globl lineinfo_file_ids\n");
>> -	printf("\t.balign 2\n");
>> -	printf("lineinfo_file_ids:\n");
>> -	for (unsigned int i = 0; i < num_entries; i++)
>> -		printf("\t.short %u\n", entries[i].file_id);
>> +	/* Block first-addresses for binary search */
>> +	printf("\t.globl lineinfo_block_addrs\n");
>> +	printf("\t.balign 4\n");
>> +	printf("lineinfo_block_addrs:\n");
>> +	for (unsigned int i = 0; i < num_blocks; i++)
>> +		printf("\t.long 0x%x\n", block_addrs[i]);
>>  	printf("\n");
>>
>> -	/* Line numbers, parallel to addrs */
>> -	printf("\t.globl lineinfo_lines\n");
>> +	/* Block byte offsets into compressed stream */
>> +	printf("\t.globl lineinfo_block_offsets\n");
>>  	printf("\t.balign 4\n");
>> -	printf("lineinfo_lines:\n");
>> -	for (unsigned int i = 0; i < num_entries; i++)
>> -		printf("\t.long %u\n", entries[i].line);
>> +	printf("lineinfo_block_offsets:\n");
>> +	for (unsigned int i = 0; i < num_blocks; i++)
>> +		printf("\t.long %u\n", block_offsets[i]);
>> +	printf("\n");
>> +
>> +	/* Compressed data stream */
>> +	printf("\t.globl lineinfo_data\n");
>> +	printf("lineinfo_data:\n");
>> +	for (unsigned int i = 0; i < compressed_size; i++) {
>> +		if ((i % 16) == 0)
>> +			printf("\t.byte ");
>> +		else
>> +			printf(",");
>> +		printf("0x%02x", compressed_data[i]);
>> +		if ((i % 16) == 15 || i == compressed_size - 1)
>> +			printf("\n");
>> +	}
>>  	printf("\n");
>>
>
>Note how compute_file_offsets() gives symbol names to the data it
>generates. Meanwhile...
>
>>  	/* File string offset table */
>> @@ -450,33 +561,38 @@ static void output_module_assembly(void)
>>
>>  	printf("\t.section .mod_lineinfo, \"a\"\n\n");
>>
>> -	/* Header: num_entries, num_files, filenames_size, reserved */
>> +	/* Header: num_entries, num_files, filenames_size, num_blocks, data_size, reserved */
>>  	printf("\t.balign 4\n");
>>  	printf("\t.long %u\n", num_entries);
>>  	printf("\t.long %u\n", num_files);
>>  	printf("\t.long %u\n", filenames_size);
>> +	printf("\t.long %u\n", num_blocks);
>> +	printf("\t.long %u\n", compressed_size);
>>  	printf("\t.long 0\n\n");
>>
>> -	/* addrs[] */
>> -	for (unsigned int i = 0; i < num_entries; i++)
>> -		printf("\t.long 0x%x\n", entries[i].offset);
>> -	if (num_entries)
>> +	/* block_addrs[] */
>> +	for (unsigned int i = 0; i < num_blocks; i++)
>> +		printf("\t.long 0x%x\n", block_addrs[i]);
>> +	if (num_blocks)
>>  		printf("\n");
>>
>For the modules, it's comments in the C code that doesn't end up in the
>assembly.
>
>I'm thinking we could have something like:
>
>	printf(".Lmod_lineinfo_block_addrs:\n")
>	for (unsigned int i = 0; i < num_entries; i++)
>		printf("\t.long 0x%x\n", ...);
>	printf("\n")
>
>(And similarly for the other blocks of data.)
>
>This would make the assembly a tiny bit more readable, get rid of the
>kinda ugly printf("\n") checks and prints, and would be useful for the
>offset + size header format I mentioned earlier.

Makes sense

>> -	/* file_ids[] */
>> -	for (unsigned int i = 0; i < num_entries; i++)
>> -		printf("\t.short %u\n", entries[i].file_id);
>> -
>> -	/* Padding to align lines[] to 4 bytes */
>> -	if (num_entries & 1)
>> -		printf("\t.short 0\n");
>> -	if (num_entries)
>> +	/* block_offsets[] */
>> +	for (unsigned int i = 0; i < num_blocks; i++)
>> +		printf("\t.long %u\n", block_offsets[i]);
>> +	if (num_blocks)
>>  		printf("\n");
>>
>> -	/* lines[] */
>> -	for (unsigned int i = 0; i < num_entries; i++)
>> -		printf("\t.long %u\n", entries[i].line);
>> -	if (num_entries)
>> +	/* compressed data[] */
>> +	for (unsigned int i = 0; i < compressed_size; i++) {
>> +		if ((i % 16) == 0)
>> +			printf("\t.byte ");
>> +		else
>> +			printf(",");
>> +		printf("0x%02x", compressed_data[i]);
>> +		if ((i % 16) == 15 || i == compressed_size - 1)
>> +			printf("\n");
>> +	}
>> +	if (compressed_size)
>>  		printf("\n");
>>
>
>Also, maybe we can use .uleb128/.sleb128 here, and generate something like:
>
>	printf("\t.sleb128 %#x - %#x\n", cur_addr, prev_addr);
>
>And have the assembler do the subtraction and encoding for us? If that
>works it should significantly simplify the compression code above.

Makes sense

>Speaking of... Why do we use uleb128(zigzag(num)) and not just sleb128(num)?

Because I originally wrote uleb128 to tackle the unsigned fields, and zigzag
was a quick way to handle signed deltas.

But as you point out, if we use .ubleb128/.sleb128 I think we can just drop
zigzag.

>[...]
>
>> @@ -558,10 +674,11 @@ int main(int argc, char *argv[])
>>  			skipped_overflow);
>>
>>  	deduplicate();
>> +	compress_entries();
>>  	compute_file_offsets();
>>
>> -	fprintf(stderr, "lineinfo: %u entries, %u files\n",
>> -		num_entries, num_files);
>> +	fprintf(stderr, "lineinfo: %u entries, %u files, %u blocks, %u compressed bytes\n",
>> +		num_entries, num_files, num_blocks, compressed_size);
>>
>>  	if (module_mode)
>>  		output_module_assembly();
>> @@ -577,6 +694,9 @@ int main(int argc, char *argv[])
>>  	for (unsigned int i = 0; i < num_files; i++)
>>  		free(files[i].name);
>>  	free(files);
>> +	free(compressed_data);
>> +	free(block_addrs);
>> +	free(block_offsets);
>>
>>  	return 0;
>>  }
>> diff --git a/scripts/kallsyms.c b/scripts/kallsyms.c
>> index 42662c4fbc6c9..94fbdad3df7c6 100644
>> --- a/scripts/kallsyms.c
>> +++ b/scripts/kallsyms.c
>> @@ -80,11 +80,12 @@ static bool is_ignored_symbol(const char *name, char type)
>>  {
>>  	/* Ignore lineinfo symbols for kallsyms pass stability */
>>  	static const char * const lineinfo_syms[] = {
>> -		"lineinfo_addrs",
>> -		"lineinfo_file_ids",
>> +		"lineinfo_block_addrs",
>> +		"lineinfo_block_offsets",
>> +		"lineinfo_data",
>>  		"lineinfo_file_offsets",
>>  		"lineinfo_filenames",
>> -		"lineinfo_lines",
>> +		"lineinfo_num_blocks",
>>  		"lineinfo_num_entries",
>>  		"lineinfo_num_files",
>>  	};
>> diff --git a/scripts/link-vmlinux.sh b/scripts/link-vmlinux.sh
>> index 640209f2e9eb9..3c122cf9b95c5 100755
>> --- a/scripts/link-vmlinux.sh
>> +++ b/scripts/link-vmlinux.sh
>> @@ -235,12 +235,16 @@ lineinfo_num_entries:
>>  	.balign 4
>>  lineinfo_num_files:
>>  	.long 0
>> -	.globl lineinfo_addrs
>> -lineinfo_addrs:
>> -	.globl lineinfo_file_ids
>> -lineinfo_file_ids:
>> -	.globl lineinfo_lines
>> -lineinfo_lines:
>> +	.globl lineinfo_num_blocks
>> +	.balign 4
>> +lineinfo_num_blocks:
>> +	.long 0
>> +	.globl lineinfo_block_addrs
>> +lineinfo_block_addrs:
>> +	.globl lineinfo_block_offsets
>> +lineinfo_block_offsets:
>> +	.globl lineinfo_data
>> +lineinfo_data:
>>  	.globl lineinfo_file_offsets
>>  lineinfo_file_offsets:
>>  	.globl lineinfo_filenames
>
>The contents of this .tmp_lineinfo.S is fixed, so it feels like it could
>just be a proper file, instead of something "generated" dynamically.

Makes sense

-- 
Thanks,
Sasha

^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 3/3] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
  2026-03-11 14:49     ` Sasha Levin
@ 2026-03-12  2:03       ` Vivian Wang
  2026-03-12  2:18         ` Sasha Levin
  0 siblings, 1 reply; 21+ messages in thread
From: Vivian Wang @ 2026-03-12  2:03 UTC (permalink / raw)
  To: Sasha Levin
  Cc: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley, Jonathan Corbet, Nathan Chancellor,
	Nicolas Schier, Petr Pavlu, Daniel Gomez, Greg KH, Petr Mladek,
	Steven Rostedt, Kees Cook, Peter Zijlstra, Thorsten Leemhuis,
	Vlastimil Babka, linux-kernel, linux-kbuild, linux-modules,
	linux-doc

Hi Sasha,

On 3/11/26 22:49, Sasha Levin wrote:
> Thanks for the review!
>
> On Wed, Mar 11, 2026 at 11:34:24AM +0800, Vivian Wang wrote:
>> Hi Sasha,
>>
>> I've been trying this out and AFAICT this does work perfectly. Thank you
>> for this.
>>
>> There are a few oddities I found:
>>
>> Firstly I've been building with something like O=_riscv out of
>> convenience, and the file names have an extra ../ in the front. (This is
>> just me exiting out of init=/bin/sh.) 
>> [    2.317268] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000
>> [    2.320283] CPU: 0 UID: 0 PID: 1 Comm: sh Not tainted 7.0.0-rc3-00004-g8ad18f1a1a2f #31 PREEMPTLAZY
>> [    2.322048] Hardware name: riscv-virtio,qemu (DT)
>> [    2.323220] Call Trace:
>> [    2.324465] [<ffffffff800172a8>] dump_backtrace+0x1c/0x24 (../arch/riscv/kernel/stacktrace.c:150)
>> [    2.329061] [<ffffffff8000241e>] show_stack+0x2a/0x34 (../arch/riscv/kernel/stacktrace.c:156)
>> [    2.330334] [<ffffffff8000fe32>] dump_stack_lvl+0x4a/0x68 (../lib/dump_stack.c:122)
>> [    2.331462] [<ffffffff8000fe64>] dump_stack+0x14/0x1c (../lib/dump_stack.c:130)
>> [    2.332571] [<ffffffff80002a88>] vpanic+0x108/0x2bc (../kernel/panic.c:651)
>> [    2.333674] [<ffffffff80002c6e>] panic+0x32/0x34 (../kernel/panic.c:787)
>> [    2.334427] [<ffffffff8002e97a>] do_exit+0x7ee/0x7f4 (../kernel/exit.c:930)
>> [    2.335194] [<ffffffff8002eade>] do_group_exit+0x1a/0x88 (../kernel/exit.c:1099)
>> [    2.335945] [<ffffffff8002eb62>] __riscv_sys_exit_group+0x16/0x18 (../kernel/exit.c:1129)
>> [    2.336763] [<ffffffff80b3e868>] do_trap_ecall_u+0x260/0x45c (../arch/riscv/include/asm/syscall.h:112)
>> [    2.337765] [<ffffffff80b4c034>] handle_exception+0x168/0x174 (../arch/riscv/kernel/entry.S:233) 
>> This is fine by me, but I've seen mentions of O= builds but I'm not sure
>> if it's expected.
>
> Could you try v2 and see if it makes it prettier? I tried to tackle
> this :)

Thanks, I'll try it out and see.

>
>> Also, toggling CONFIG_KALLSYMS_LINEINFO seems to rebuild every single
>> file. I haven't debugged why, but is this expected?
>
> I think that this is because we increase KSYM_SYMBOL_LEN when lineinfo is
> enabled. I suppose we can just increase the size irregardless of whether
> lineinfo is enabled and ignore the waste?
>
> Or, folks really won't be toggling this option too often for the
> rebuilds to
> matter too much, so we can just enjoy the savings? 

Yeah I understand now. The size affects some fundamental headers.

I just thought it was odd. The current situation is fine by me - if I'm
building a kernel and toggling configs, it means I have the vmlinux file
and can use scripts/decode_stacktrace.sh :)

>> I have a few ideas about the code as well. Since this patch 3 touches
>> most of the files involved, I'll just dump my thoughts on the whole
>> series here. I want to note that I haven't read the RFC thread too
>> carefully, but I don't think there were many comments on the
>> implementation.
>>
>> On 3/4/26 02:21, Sasha Levin wrote:
>>> Replace the flat uncompressed parallel arrays (lineinfo_addrs[],
>>> lineinfo_file_ids[], lineinfo_lines[]) with a block-indexed,
>>> delta-encoded, ULEB128 varint compressed format.
>>>
>>> The sorted address array has small deltas between consecutive entries
>>> (typically 1-50 bytes), file IDs have high locality (delta often 0,
>>> same file), and line numbers change slowly.  Delta-encoding followed
>>> by ULEB128 varint compression shrinks most values from 4 bytes to 1.
>>>
>>> Entries are grouped into blocks of 64.  A small uncompressed block
>>> index (first addr + byte offset per block) enables O(log(N/64)) binary
>>> search, followed by sequential decode of at most 64 varints within the
>>> matching block.  All decode state lives on the stack -- zero
>>> allocations, still safe for NMI/panic context.
>>>
>>> Measured on a defconfig+debug x86_64 build (3,017,154 entries, 4,822
>>> source files, 47,144 blocks):
>>>
>>>   Before (flat arrays):
>>>     lineinfo_addrs[]    12,068,616 bytes (u32 x 3.0M)
>>>     lineinfo_file_ids[]  6,034,308 bytes (u16 x 3.0M)
>>>     lineinfo_lines[]    12,068,616 bytes (u32 x 3.0M)
>>>     Total:              30,171,540 bytes (28.8 MiB, 10.0 bytes/entry)
>>>
>>>   After (block-indexed delta + ULEB128):
>>>     lineinfo_block_addrs[]    188,576 bytes (184 KiB)
>>>     lineinfo_block_offsets[]  188,576 bytes (184 KiB)
>>>     lineinfo_data[]        10,926,128 bytes (10.4 MiB)
>>>     Total:                 11,303,280 bytes (10.8 MiB, 3.7 bytes/entry)
>>>
>>>   Savings: 18.0 MiB (2.7x reduction)
>>>
>>> Booted in QEMU and verified with SysRq-l that annotations still work:
>>>
>>>   default_idle+0x9/0x10 (arch/x86/kernel/process.c:767)
>>>   default_idle_call+0x6c/0xb0 (kernel/sched/idle.c:122)
>>>   do_idle+0x335/0x490 (kernel/sched/idle.c:191)
>>>   cpu_startup_entry+0x4e/0x60 (kernel/sched/idle.c:429)
>>>   rest_init+0x1aa/0x1b0 (init/main.c:760)
>>>
>>> Suggested-by: Juergen Gross <jgross@suse.com>
>>> Assisted-by: Claude:claude-opus-4-6
>>> Signed-off-by: Sasha Levin <sashal@kernel.org>
>>> ---
>>>  .../admin-guide/kallsyms-lineinfo.rst         |   7 +-
>>>  include/linux/mod_lineinfo.h                  | 103 ++++++++--
>>>  init/Kconfig                                  |   8 +-
>>>  kernel/kallsyms.c                             |  91 +++++++--
>>>  kernel/kallsyms_internal.h                    |   7 +-
>>>  kernel/module/kallsyms.c                      | 107 +++++++---
>>>  scripts/gen_lineinfo.c                        | 192 ++++++++++++++----
>>>  scripts/kallsyms.c                            |   7 +-
>>>  scripts/link-vmlinux.sh                       |  16 +-
>>>  9 files changed, 423 insertions(+), 115 deletions(-)
>>>
>>> diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
>>> index 21450569d5324..fe92c5dde16b3 100644
>>> --- a/Documentation/admin-guide/kallsyms-lineinfo.rst
>>> +++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
>>> @@ -76,10 +76,11 @@ Memory Overhead
>>>  ===============
>>>
>>>  The vmlinux lineinfo tables are stored in ``.rodata`` and typically add
>>> -approximately 44 MiB to the kernel image for a standard configuration
>>> -(~4.6 million DWARF line entries, ~10 bytes per entry after deduplication).
>>> +approximately 10-15 MiB to the kernel image for a standard configuration
>>> +(~4.6 million DWARF line entries, ~2-3 bytes per entry after delta
>>> +compression).
>>>
>>> -Per-module lineinfo adds approximately 10 bytes per DWARF line entry to each
>>> +Per-module lineinfo adds approximately 2-3 bytes per DWARF line entry to each
>>>  ``.ko`` file. 
>>
>> Maybe this could be given in terms of percentages? It wasn't obvious to
>> me what 10-15 MiB amounts to.
>>
>> On riscv64, I'm seeing a 24.2 MiB to 30.2 MiB increase in
>> arch/riscv/boot/Image size on an approximately defconfig+mod2noconfig
>> build, which is about a 25% increase. I haven't checked yet, but if 25%
>> is similar to what other archs get, that's a more useful figure than
>> 10-15 MiB, given that the size increase is correlated to the total
>> amount of code linked into the kernel/module.
>
> I ended up giving an example instead of percentages because it seemed
> to vary
> wildly between configs and archs. For example, defconfig on x86 yields
> a 15%
> increase compared to the 25% you see with your config on riscv,
> compared to a
> 39% increase with defconfig on riscv. 

That's fair. I guess it also depends on arch code density and compiler
codegen.

[...]

>>> +/*
>>> + * Read a ULEB128 varint from a byte stream.
>>> + * Returns the decoded value and advances *pos past the encoded bytes.
>>> + * If *pos would exceed 'end', returns 0 and sets *pos = end (safe for
>>> + * NMI/panic context -- no crash, just a missed annotation). 
>>
>> What does that last bit mean...?
>
> This goes back to your previous point about correctness and checks in the
> lineinfo code :)
>
> It just means that this function never faults or allocates. On bad
> input it
> returns 0, so the worst case is a missing annotation, not a crash. 

Ah, right, it didn't occur to me it was "annotation" as in the lineinfo
annotation in the stack trace. I thought it was something like noinstr
or lockdep stuff at first. This checks out.

Vivian "dramforever" Wang


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH 3/3] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
  2026-03-12  2:03       ` Vivian Wang
@ 2026-03-12  2:18         ` Sasha Levin
  0 siblings, 0 replies; 21+ messages in thread
From: Sasha Levin @ 2026-03-12  2:18 UTC (permalink / raw)
  To: Vivian Wang
  Cc: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley, Jonathan Corbet, Nathan Chancellor,
	Nicolas Schier, Petr Pavlu, Daniel Gomez, Greg KH, Petr Mladek,
	Steven Rostedt, Kees Cook, Peter Zijlstra, Thorsten Leemhuis,
	Vlastimil Babka, linux-kernel, linux-kbuild, linux-modules,
	linux-doc

On Thu, Mar 12, 2026 at 10:03:55AM +0800, Vivian Wang wrote:
>Hi Sasha,
>
>On 3/11/26 22:49, Sasha Levin wrote:
>> Thanks for the review!
>>
>> On Wed, Mar 11, 2026 at 11:34:24AM +0800, Vivian Wang wrote:
>>> Hi Sasha,
>>>
>>> I've been trying this out and AFAICT this does work perfectly. Thank you
>>> for this.
>>>
>>> There are a few oddities I found:
>>>
>>> Firstly I've been building with something like O=_riscv out of
>>> convenience, and the file names have an extra ../ in the front. (This is
>>> just me exiting out of init=/bin/sh.) 
>>> [    2.317268] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000
>>> [    2.320283] CPU: 0 UID: 0 PID: 1 Comm: sh Not tainted 7.0.0-rc3-00004-g8ad18f1a1a2f #31 PREEMPTLAZY
>>> [    2.322048] Hardware name: riscv-virtio,qemu (DT)
>>> [    2.323220] Call Trace:
>>> [    2.324465] [<ffffffff800172a8>] dump_backtrace+0x1c/0x24 (../arch/riscv/kernel/stacktrace.c:150)
>>> [    2.329061] [<ffffffff8000241e>] show_stack+0x2a/0x34 (../arch/riscv/kernel/stacktrace.c:156)
>>> [    2.330334] [<ffffffff8000fe32>] dump_stack_lvl+0x4a/0x68 (../lib/dump_stack.c:122)
>>> [    2.331462] [<ffffffff8000fe64>] dump_stack+0x14/0x1c (../lib/dump_stack.c:130)
>>> [    2.332571] [<ffffffff80002a88>] vpanic+0x108/0x2bc (../kernel/panic.c:651)
>>> [    2.333674] [<ffffffff80002c6e>] panic+0x32/0x34 (../kernel/panic.c:787)
>>> [    2.334427] [<ffffffff8002e97a>] do_exit+0x7ee/0x7f4 (../kernel/exit.c:930)
>>> [    2.335194] [<ffffffff8002eade>] do_group_exit+0x1a/0x88 (../kernel/exit.c:1099)
>>> [    2.335945] [<ffffffff8002eb62>] __riscv_sys_exit_group+0x16/0x18 (../kernel/exit.c:1129)
>>> [    2.336763] [<ffffffff80b3e868>] do_trap_ecall_u+0x260/0x45c (../arch/riscv/include/asm/syscall.h:112)
>>> [    2.337765] [<ffffffff80b4c034>] handle_exception+0x168/0x174 (../arch/riscv/kernel/entry.S:233)
>>> This is fine by me, but I've seen mentions of O= builds but I'm not sure
>>> if it's expected.
>>
>> Could you try v2 and see if it makes it prettier? I tried to tackle
>> this :)
>
>Thanks, I'll try it out and see.
>
>>
>>> Also, toggling CONFIG_KALLSYMS_LINEINFO seems to rebuild every single
>>> file. I haven't debugged why, but is this expected?
>>
>> I think that this is because we increase KSYM_SYMBOL_LEN when lineinfo is
>> enabled. I suppose we can just increase the size irregardless of whether
>> lineinfo is enabled and ignore the waste?
>>
>> Or, folks really won't be toggling this option too often for the
>> rebuilds to
>> matter too much, so we can just enjoy the savings? 
>
>Yeah I understand now. The size affects some fundamental headers.
>
>I just thought it was odd. The current situation is fine by me - if I'm
>building a kernel and toggling configs, it means I have the vmlinux file
>and can use scripts/decode_stacktrace.sh :)
>
>>> I have a few ideas about the code as well. Since this patch 3 touches
>>> most of the files involved, I'll just dump my thoughts on the whole
>>> series here. I want to note that I haven't read the RFC thread too
>>> carefully, but I don't think there were many comments on the
>>> implementation.
>>>
>>> On 3/4/26 02:21, Sasha Levin wrote:
>>>> Replace the flat uncompressed parallel arrays (lineinfo_addrs[],
>>>> lineinfo_file_ids[], lineinfo_lines[]) with a block-indexed,
>>>> delta-encoded, ULEB128 varint compressed format.
>>>>
>>>> The sorted address array has small deltas between consecutive entries
>>>> (typically 1-50 bytes), file IDs have high locality (delta often 0,
>>>> same file), and line numbers change slowly.  Delta-encoding followed
>>>> by ULEB128 varint compression shrinks most values from 4 bytes to 1.
>>>>
>>>> Entries are grouped into blocks of 64.  A small uncompressed block
>>>> index (first addr + byte offset per block) enables O(log(N/64)) binary
>>>> search, followed by sequential decode of at most 64 varints within the
>>>> matching block.  All decode state lives on the stack -- zero
>>>> allocations, still safe for NMI/panic context.
>>>>
>>>> Measured on a defconfig+debug x86_64 build (3,017,154 entries, 4,822
>>>> source files, 47,144 blocks):
>>>>
>>>>   Before (flat arrays):
>>>>     lineinfo_addrs[]    12,068,616 bytes (u32 x 3.0M)
>>>>     lineinfo_file_ids[]  6,034,308 bytes (u16 x 3.0M)
>>>>     lineinfo_lines[]    12,068,616 bytes (u32 x 3.0M)
>>>>     Total:              30,171,540 bytes (28.8 MiB, 10.0 bytes/entry)
>>>>
>>>>   After (block-indexed delta + ULEB128):
>>>>     lineinfo_block_addrs[]    188,576 bytes (184 KiB)
>>>>     lineinfo_block_offsets[]  188,576 bytes (184 KiB)
>>>>     lineinfo_data[]        10,926,128 bytes (10.4 MiB)
>>>>     Total:                 11,303,280 bytes (10.8 MiB, 3.7 bytes/entry)
>>>>
>>>>   Savings: 18.0 MiB (2.7x reduction)
>>>>
>>>> Booted in QEMU and verified with SysRq-l that annotations still work:
>>>>
>>>>   default_idle+0x9/0x10 (arch/x86/kernel/process.c:767)
>>>>   default_idle_call+0x6c/0xb0 (kernel/sched/idle.c:122)
>>>>   do_idle+0x335/0x490 (kernel/sched/idle.c:191)
>>>>   cpu_startup_entry+0x4e/0x60 (kernel/sched/idle.c:429)
>>>>   rest_init+0x1aa/0x1b0 (init/main.c:760)
>>>>
>>>> Suggested-by: Juergen Gross <jgross@suse.com>
>>>> Assisted-by: Claude:claude-opus-4-6
>>>> Signed-off-by: Sasha Levin <sashal@kernel.org>
>>>> ---
>>>>  .../admin-guide/kallsyms-lineinfo.rst         |   7 +-
>>>>  include/linux/mod_lineinfo.h                  | 103 ++++++++--
>>>>  init/Kconfig                                  |   8 +-
>>>>  kernel/kallsyms.c                             |  91 +++++++--
>>>>  kernel/kallsyms_internal.h                    |   7 +-
>>>>  kernel/module/kallsyms.c                      | 107 +++++++---
>>>>  scripts/gen_lineinfo.c                        | 192 ++++++++++++++----
>>>>  scripts/kallsyms.c                            |   7 +-
>>>>  scripts/link-vmlinux.sh                       |  16 +-
>>>>  9 files changed, 423 insertions(+), 115 deletions(-)
>>>>
>>>> diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
>>>> index 21450569d5324..fe92c5dde16b3 100644
>>>> --- a/Documentation/admin-guide/kallsyms-lineinfo.rst
>>>> +++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
>>>> @@ -76,10 +76,11 @@ Memory Overhead
>>>>  ===============
>>>>
>>>>  The vmlinux lineinfo tables are stored in ``.rodata`` and typically add
>>>> -approximately 44 MiB to the kernel image for a standard configuration
>>>> -(~4.6 million DWARF line entries, ~10 bytes per entry after deduplication).
>>>> +approximately 10-15 MiB to the kernel image for a standard configuration
>>>> +(~4.6 million DWARF line entries, ~2-3 bytes per entry after delta
>>>> +compression).
>>>>
>>>> -Per-module lineinfo adds approximately 10 bytes per DWARF line entry to each
>>>> +Per-module lineinfo adds approximately 2-3 bytes per DWARF line entry to each
>>>>  ``.ko`` file.
>>>
>>> Maybe this could be given in terms of percentages? It wasn't obvious to
>>> me what 10-15 MiB amounts to.
>>>
>>> On riscv64, I'm seeing a 24.2 MiB to 30.2 MiB increase in
>>> arch/riscv/boot/Image size on an approximately defconfig+mod2noconfig
>>> build, which is about a 25% increase. I haven't checked yet, but if 25%
>>> is similar to what other archs get, that's a more useful figure than
>>> 10-15 MiB, given that the size increase is correlated to the total
>>> amount of code linked into the kernel/module.
>>
>> I ended up giving an example instead of percentages because it seemed
>> to vary
>> wildly between configs and archs. For example, defconfig on x86 yields
>> a 15%
>> increase compared to the 25% you see with your config on riscv,
>> compared to a
>> 39% increase with defconfig on riscv. 
>
>That's fair. I guess it also depends on arch code density and compiler
>codegen.
>
>[...]
>
>>>> +/*
>>>> + * Read a ULEB128 varint from a byte stream.
>>>> + * Returns the decoded value and advances *pos past the encoded bytes.
>>>> + * If *pos would exceed 'end', returns 0 and sets *pos = end (safe for
>>>> + * NMI/panic context -- no crash, just a missed annotation).
>>>
>>> What does that last bit mean...?
>>
>> This goes back to your previous point about correctness and checks in the
>> lineinfo code :)
>>
>> It just means that this function never faults or allocates. On bad
>> input it
>> returns 0, so the worst case is a missing annotation, not a crash. 
>
>Ah, right, it didn't occur to me it was "annotation" as in the lineinfo
>annotation in the stack trace. I thought it was something like noinstr
>or lockdep stuff at first. This checks out.

Thanks again for the review! I'll send a v3 :)

-- 
Thanks,
Sasha

^ permalink raw reply	[flat|nested] 21+ messages in thread

end of thread, other threads:[~2026-03-12  2:18 UTC | newest]

Thread overview: 21+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-03 18:21 [PATCH 0/3] kallsyms: embed source file:line info in kernel stack traces Sasha Levin
2026-03-03 18:21 ` [PATCH 1/3] " Sasha Levin
2026-03-04 20:17   ` Helge Deller
2026-03-05  2:18     ` Sasha Levin
2026-03-05 22:26       ` Helge Deller
2026-03-06  5:31         ` Randy Dunlap
2026-03-06 17:53           ` Helge Deller
2026-03-06  5:28   ` Randy Dunlap
2026-03-06 16:36   ` Petr Mladek
2026-03-06 17:14     ` Sasha Levin
2026-03-10 15:20       ` Petr Mladek
2026-03-11  0:58         ` Sasha Levin
2026-03-03 18:21 ` [PATCH 2/3] kallsyms: extend lineinfo to loadable modules Sasha Levin
2026-03-03 18:21 ` [PATCH 3/3] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction Sasha Levin
2026-03-03 21:25   ` Geert Uytterhoeven
2026-03-04  1:11     ` Sasha Levin
2026-03-11  3:34   ` Vivian Wang
2026-03-11  4:13     ` Vivian Wang
2026-03-11 14:49     ` Sasha Levin
2026-03-12  2:03       ` Vivian Wang
2026-03-12  2:18         ` Sasha Levin

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox