public inbox for linux-kernel@vger.kernel.org
 help / color / mirror / Atom feed
From: Sasha Levin <sashal@kernel.org>
To: Andrew Morton <akpm@linux-foundation.org>,
	Masahiro Yamada <masahiroy@kernel.org>,
	Luis Chamberlain <mcgrof@kernel.org>,
	Linus Torvalds <torvalds@linux-foundation.org>,
	Richard Weinberger <richard@nod.at>,
	Juergen Gross <jgross@suse.com>,
	Geert Uytterhoeven <geert@linux-m68k.org>,
	James Bottomley <James.Bottomley@HansenPartnership.com>
Cc: Jonathan Corbet <corbet@lwn.net>,
	Nathan Chancellor <nathan@kernel.org>,
	Nicolas Schier <nsc@kernel.org>, Petr Pavlu <petr.pavlu@suse.com>,
	Daniel Gomez <da.gomez@kernel.org>,
	Greg KH <gregkh@linuxfoundation.org>,
	Petr Mladek <pmladek@suse.com>,
	Steven Rostedt <rostedt@goodmis.org>, Kees Cook <kees@kernel.org>,
	Peter Zijlstra <peterz@infradead.org>,
	Thorsten Leemhuis <linux@leemhuis.info>,
	Vlastimil Babka <vbabka@kernel.org>, Helge Deller <deller@gmx.de>,
	Randy Dunlap <rdunlap@infradead.org>,
	Laurent Pinchart <laurent.pinchart@ideasonboard.com>,
	Vivian Wang <wangruikang@iscas.ac.cn>,
	Zhen Lei <thunder.leizhen@huawei.com>,
	Sami Tolvanen <samitolvanen@google.com>,
	linux-kernel@vger.kernel.org, linux-kbuild@vger.kernel.org,
	linux-modules@vger.kernel.org, linux-doc@vger.kernel.org,
	Sasha Levin <sashal@kernel.org>
Subject: [PATCH v5 2/4] kallsyms: extend lineinfo to loadable modules
Date: Mon,  4 May 2026 11:33:58 -0400	[thread overview]
Message-ID: <20260504153401.2416391-3-sashal@kernel.org> (raw)
In-Reply-To: <20260504153401.2416391-1-sashal@kernel.org>

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 per-section .mod_lineinfo and .init.mod_lineinfo
sections containing compact binary tables of section-relative offsets,
file IDs, line numbers, and filenames, and embeds them back into the
.ko via a partial link (ld -r).

At runtime, module_lookup_lineinfo() walks the section descriptors in
each blob, finds the one whose runtime range contains the queried
address, and binary-searches that section's table.  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
(or its init memory) is unloaded.

The gen_lineinfo tool gains --module mode which:
 - Walks an allowlist of text-like sections (.text, .exit.text,
   .init.text), gating each on its presence in the .ko.
 - Uses an ELF relocation against each covered section's symbol as the
   runtime "anchor", resolved by the module loader's standard
   apply_relocations() pass — no implicit base derivation from
   mod->mem[].base, no special-cased loader logic.
 - Disambiguates DWARF addresses across sections that all share
   sh_addr == 0 in ET_REL files via per-section synthetic biases
   applied inside apply_debug_line_relocations() (handles both abs32
   and abs64 width relocs).
 - Handles libdw's ET_REL path-doubling quirk in make_relative().
 - Declares empty section stanzas in its output assembly so the
   resulting lineinfo.o has LOCAL SECTION symbols rather than GLOBAL
   UND ones; otherwise ld -r would not bind the relocation to the
   .ko's existing section symbol of the same name and depmod would
   warn.

The build pipeline runs gen-mod-lineinfo.sh after the existing modfinal
step:

  gen_lineinfo --module ${KO} > ${KO}.lineinfo.S
  ${CC} -c -o ${KO}.lineinfo.o ${KO}.lineinfo.S
  ${LD} -r ${KO}.lineinfo.o ${KO} -o ${KO}.tmp && mv ${KO}.tmp ${KO}

Order matters: lineinfo.o must come first so its zero-byte text
contributions stay at offset 0 of the merged sections.

The init blob lives in MOD_INIT_RODATA and is revoked via WRITE_ONCE
in do_init_module() before do_free_init() releases the memory; the
module_init_lineinfo_data() reader uses READ_ONCE so concurrent
lookups either see the old pointer (still valid until do_free_init's
synchronize_rcu) or NULL.

The struct module fields are guarded by
#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES and accessed through inline
reader accessors so callers don't duplicate the guard.

Per-module overhead is approximately 14 bytes per DWARF line entry
plus a small fixed cost per covered section descriptor.  The next
patch in this series delta-compresses the per-section streams to ~3-4
bytes per entry.

Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
 .../admin-guide/kallsyms-lineinfo.rst         |  41 +-
 MAINTAINERS                                   | 108 ++-
 include/linux/mod_lineinfo.h                  | 104 +++
 include/linux/module.h                        |  39 +
 init/Kconfig                                  |  19 +-
 kernel/kallsyms.c                             |  18 +-
 kernel/module/kallsyms.c                      | 181 ++++
 kernel/module/main.c                          |  26 +
 scripts/Makefile.modfinal                     |   6 +
 scripts/gen-mod-lineinfo.sh                   |  50 +
 scripts/gen_lineinfo.c                        | 854 ++++++++++++++++--
 11 files changed, 1320 insertions(+), 126 deletions(-)
 create mode 100644 include/linux/mod_lineinfo.h
 create mode 100644 scripts/gen-mod-lineinfo.sh

diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
index c8ec124394354..dd264830c8d5b 100644
--- a/Documentation/admin-guide/kallsyms-lineinfo.rst
+++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
@@ -51,22 +51,47 @@ 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 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 2-3 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 a683a7023f6e9..f264e763d1041 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7873,7 +7873,7 @@ F:	drivers/gpu/drm/sun4i/sun8i*
 
 DRM DRIVER FOR APPLE TOUCH BARS
 M:	Aun-Ali Zaidi <admin@kodeit.net>
-M:	Aditya Garg <gargaditya08@live.com>
+M:	Aditya Garg <gargaditya08@proton.me>
 L:	dri-devel@lists.freedesktop.org
 S:	Maintained
 T:	git https://gitlab.freedesktop.org/drm/misc/kernel.git
@@ -13797,6 +13797,9 @@ KALLSYMS LINEINFO
 M:	Sasha Levin <sashal@kernel.org>
 S:	Maintained
 F:	Documentation/admin-guide/kallsyms-lineinfo.rst
+F:	include/linux/mod_lineinfo.h
+F:	lib/tests/lineinfo_kunit.c
+F:	scripts/gen-mod-lineinfo.sh
 F:	scripts/gen_lineinfo.c
 
 KASAN
@@ -13866,7 +13869,6 @@ M:	Pratyush Yadav <pratyush@kernel.org>
 R:	Dave Young <ruirui.yang@linux.dev>
 L:	kexec@lists.infradead.org
 S:	Maintained
-W:	http://lse.sourceforge.net/kdump/
 F:	Documentation/admin-guide/kdump/
 F:	fs/proc/vmcore.c
 F:	include/linux/crash_core.h
@@ -15258,7 +15260,7 @@ M:	Andrea Cervesato <andrea.cervesato@suse.com>
 M:	Cyril Hrubis <chrubis@suse.cz>
 M:	Jan Stancek <jstancek@redhat.com>
 M:	Petr Vorel <pvorel@suse.cz>
-M:	Li Wang <liwang@redhat.com>
+M:	Li Wang <li.wang@linux.dev>
 M:	Yang Xu <xuyang2018.jy@fujitsu.com>
 M:	Xiao Yang <yangx.jy@fujitsu.com>
 L:	ltp@lists.linux.it (subscribers-only)
@@ -15405,7 +15407,7 @@ F:	include/net/netns/mctp.h
 F:	net/mctp/
 
 MAPLE TREE
-M:	Liam R. Howlett <Liam.Howlett@oracle.com>
+M:	Liam R. Howlett <liam@infradead.org>
 R:	Alice Ryhl <aliceryhl@google.com>
 R:	Andrew Ballance <andrewjballance@gmail.com>
 L:	maple-tree@lists.infradead.org
@@ -16765,7 +16767,7 @@ MEMORY MANAGEMENT - CORE
 M:	Andrew Morton <akpm@linux-foundation.org>
 M:	David Hildenbrand <david@kernel.org>
 R:	Lorenzo Stoakes <ljs@kernel.org>
-R:	Liam R. Howlett <Liam.Howlett@oracle.com>
+R:	Liam R. Howlett <liam@infradead.org>
 R:	Vlastimil Babka <vbabka@kernel.org>
 R:	Mike Rapoport <rppt@kernel.org>
 R:	Suren Baghdasaryan <surenb@google.com>
@@ -16811,7 +16813,7 @@ F:	mm/sparse.c
 F:	mm/util.c
 F:	mm/vmpressure.c
 F:	mm/vmstat.c
-N:	include/linux/page[-_]*
+N:	include\/linux\/page[-_][a-zA-Z]*
 
 MEMORY MANAGEMENT - EXECMEM
 M:	Andrew Morton <akpm@linux-foundation.org>
@@ -16901,7 +16903,7 @@ MEMORY MANAGEMENT - MISC
 M:	Andrew Morton <akpm@linux-foundation.org>
 M:	David Hildenbrand <david@kernel.org>
 R:	Lorenzo Stoakes <ljs@kernel.org>
-R:	Liam R. Howlett <Liam.Howlett@oracle.com>
+R:	Liam R. Howlett <liam@infradead.org>
 R:	Vlastimil Babka <vbabka@kernel.org>
 R:	Mike Rapoport <rppt@kernel.org>
 R:	Suren Baghdasaryan <surenb@google.com>
@@ -16968,6 +16970,7 @@ S:	Maintained
 F:	include/linux/compaction.h
 F:	include/linux/gfp.h
 F:	include/linux/page-isolation.h
+F:	include/linux/pageblock-flags.h
 F:	mm/compaction.c
 F:	mm/debug_page_alloc.c
 F:	mm/debug_page_ref.c
@@ -16989,7 +16992,7 @@ M:	Andrew Morton <akpm@linux-foundation.org>
 M:	Johannes Weiner <hannes@cmpxchg.org>
 R:	David Hildenbrand <david@kernel.org>
 R:	Michal Hocko <mhocko@kernel.org>
-R:	Qi Zheng <zhengqi.arch@bytedance.com>
+R:	Qi Zheng <qi.zheng@linux.dev>
 R:	Shakeel Butt <shakeel.butt@linux.dev>
 R:	Lorenzo Stoakes <ljs@kernel.org>
 L:	linux-mm@kvack.org
@@ -17002,7 +17005,7 @@ M:	Andrew Morton <akpm@linux-foundation.org>
 M:	David Hildenbrand <david@kernel.org>
 M:	Lorenzo Stoakes <ljs@kernel.org>
 R:	Rik van Riel <riel@surriel.com>
-R:	Liam R. Howlett <Liam.Howlett@oracle.com>
+R:	Liam R. Howlett <liam@infradead.org>
 R:	Vlastimil Babka <vbabka@kernel.org>
 R:	Harry Yoo <harry@kernel.org>
 R:	Jann Horn <jannh@google.com>
@@ -17049,7 +17052,7 @@ M:	David Hildenbrand <david@kernel.org>
 M:	Lorenzo Stoakes <ljs@kernel.org>
 R:	Zi Yan <ziy@nvidia.com>
 R:	Baolin Wang <baolin.wang@linux.alibaba.com>
-R:	Liam R. Howlett <Liam.Howlett@oracle.com>
+R:	Liam R. Howlett <liam@infradead.org>
 R:	Nico Pache <npache@redhat.com>
 R:	Ryan Roberts <ryan.roberts@arm.com>
 R:	Dev Jain <dev.jain@arm.com>
@@ -17087,7 +17090,7 @@ F:	tools/testing/selftests/mm/uffd-*.[ch]
 MEMORY MANAGEMENT - RUST
 M:	Alice Ryhl <aliceryhl@google.com>
 R:	Lorenzo Stoakes <ljs@kernel.org>
-R:	Liam R. Howlett <Liam.Howlett@oracle.com>
+R:	Liam R. Howlett <liam@infradead.org>
 L:	linux-mm@kvack.org
 L:	rust-for-linux@vger.kernel.org
 S:	Maintained
@@ -17101,7 +17104,7 @@ F:	rust/kernel/page.rs
 
 MEMORY MAPPING
 M:	Andrew Morton <akpm@linux-foundation.org>
-M:	Liam R. Howlett <Liam.Howlett@oracle.com>
+M:	Liam R. Howlett <liam@infradead.org>
 M:	Lorenzo Stoakes <ljs@kernel.org>
 R:	Vlastimil Babka <vbabka@kernel.org>
 R:	Jann Horn <jannh@google.com>
@@ -17133,7 +17136,7 @@ F:	tools/testing/vma/
 MEMORY MAPPING - LOCKING
 M:	Andrew Morton <akpm@linux-foundation.org>
 M:	Suren Baghdasaryan <surenb@google.com>
-M:	Liam R. Howlett <Liam.Howlett@oracle.com>
+M:	Liam R. Howlett <liam@infradead.org>
 M:	Lorenzo Stoakes <ljs@kernel.org>
 R:	Vlastimil Babka <vbabka@kernel.org>
 R:	Shakeel Butt <shakeel.butt@linux.dev>
@@ -17148,7 +17151,7 @@ F:	mm/mmap_lock.c
 
 MEMORY MAPPING - MADVISE (MEMORY ADVICE)
 M:	Andrew Morton <akpm@linux-foundation.org>
-M:	Liam R. Howlett <Liam.Howlett@oracle.com>
+M:	Liam R. Howlett <liam@infradead.org>
 M:	Lorenzo Stoakes <ljs@kernel.org>
 M:	David Hildenbrand <david@kernel.org>
 R:	Vlastimil Babka <vbabka@kernel.org>
@@ -18678,19 +18681,59 @@ F:	net/xfrm/
 F:	tools/testing/selftests/net/ipsec.c
 
 NETWORKING [IPv4/IPv6]
-M:	"David S. Miller" <davem@davemloft.net>
 M:	David Ahern <dsahern@kernel.org>
+M:	Ido Schimmel <idosch@nvidia.com>
 L:	netdev@vger.kernel.org
 S:	Maintained
-T:	git git://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git
-F:	arch/x86/net/*
-F:	include/linux/ip.h
-F:	include/linux/ipv6*
+F:	Documentation/netlink/specs/rt-addr.yaml
+F:	Documentation/netlink/specs/rt-neigh.yaml
+F:	Documentation/netlink/specs/rt-route.yaml
+F:	Documentation/netlink/specs/rt-rule.yaml
+F:	include/linux/inetdevice.h
+F:	include/linux/mroute*
+F:	include/net/addrconf.h
+F:	include/net/arp.h
 F:	include/net/fib*
+F:	include/net/if_inet6.h
+F:	include/net/inetpeer.h
 F:	include/net/ip*
+F:	include/net/lwtunnel.h
+F:	include/net/ndisc.h
+F:	include/net/netns/nexthop.h
+F:	include/net/nexthop.h
 F:	include/net/route.h
-F:	net/ipv4/
-F:	net/ipv6/
+F:	include/uapi/linux/fib_rules.h
+F:	include/uapi/linux/in_route.h
+F:	include/uapi/linux/mroute*
+F:	include/uapi/linux/nexthop.h
+F:	net/core/fib*
+F:	net/core/lwtunnel.c
+F:	net/ipv4/arp.c
+F:	net/ipv4/devinet.c
+F:	net/ipv4/fib*
+F:	net/ipv4/icmp.c
+F:	net/ipv4/igmp.c
+F:	net/ipv4/inet_fragment.c
+F:	net/ipv4/inetpeer.c
+F:	net/ipv4/ip*
+F:	net/ipv4/metrics.c
+F:	net/ipv4/netlink.c
+F:	net/ipv4/nexthop.c
+F:	net/ipv4/route.c
+F:	net/ipv6/addr*
+F:	net/ipv6/anycast.c
+F:	net/ipv6/exthdrs.c
+F:	net/ipv6/exthdrs_core.c
+F:	net/ipv6/fib*
+F:	net/ipv6/icmp.c
+F:	net/ipv6/ip*
+F:	net/ipv6/mcast*
+F:	net/ipv6/ndisc.c
+F:	net/ipv6/output_core.c
+F:	net/ipv6/reassembly.c
+F:	net/ipv6/route.c
+F:	tools/testing/selftests/net/fib*
+F:	tools/testing/selftests/net/forwarding/
 
 NETWORKING [LABELED] (NetLabel, Labeled IPsec, SECMARK)
 M:	Paul Moore <paul@paul-moore.com>
@@ -18825,18 +18868,11 @@ F:	Documentation/networking/net_failover.rst
 F:	drivers/net/net_failover.c
 F:	include/net/net_failover.h
 
-NEXTHOP
-M:	David Ahern <dsahern@kernel.org>
-L:	netdev@vger.kernel.org
-S:	Maintained
-F:	include/net/netns/nexthop.h
-F:	include/net/nexthop.h
-F:	include/uapi/linux/nexthop.h
-F:	net/ipv4/nexthop.c
-
 NFC SUBSYSTEM
-L:	netdev@vger.kernel.org
-S:	Orphan
+M:	David Heidelberg <david+nfc@ixit.cz>
+L:	oe-linux-nfc@lists.linux.dev
+S:	Maintained
+T:	git https://codeberg.org/linux-nfc/linux.git
 F:	Documentation/devicetree/bindings/net/nfc/
 F:	drivers/nfc/
 F:	include/net/nfc/
@@ -20780,6 +20816,7 @@ M:	Dominik Brodowski <linux@dominikbrodowski.net>
 S:	Odd Fixes
 T:	git git://git.kernel.org/pub/scm/linux/kernel/git/brodo/linux.git
 F:	Documentation/pcmcia/
+F:	drivers/net/ethernet/8390/pcnet_cs.c
 F:	drivers/pcmcia/
 F:	include/pcmcia/
 F:	tools/pcmcia/
@@ -23375,7 +23412,7 @@ RUST [ALLOC]
 M:	Danilo Krummrich <dakr@kernel.org>
 R:	Lorenzo Stoakes <ljs@kernel.org>
 R:	Vlastimil Babka <vbabka@kernel.org>
-R:	Liam R. Howlett <Liam.Howlett@oracle.com>
+R:	Liam R. Howlett <liam@infradead.org>
 R:	Uladzislau Rezki <urezki@gmail.com>
 L:	rust-for-linux@vger.kernel.org
 S:	Maintained
@@ -23527,7 +23564,7 @@ F:	drivers/s390/net/
 
 S390 PCI SUBSYSTEM
 M:	Niklas Schnelle <schnelle@linux.ibm.com>
-M:	Gerald Schaefer <gerald.schaefer@linux.ibm.com>
+M:	Gerd Bayer <gbayer@linux.ibm.com>
 L:	linux-s390@vger.kernel.org
 S:	Supported
 F:	Documentation/arch/s390/pci.rst
@@ -24320,7 +24357,7 @@ F:	include/media/i2c/rj54n1cb0c.h
 SHRINKER
 M:	Andrew Morton <akpm@linux-foundation.org>
 M:	Dave Chinner <david@fromorbit.com>
-R:	Qi Zheng <zhengqi.arch@bytedance.com>
+R:	Qi Zheng <qi.zheng@linux.dev>
 R:	Roman Gushchin <roman.gushchin@linux.dev>
 R:	Muchun Song <muchun.song@linux.dev>
 L:	linux-mm@kvack.org
@@ -24770,6 +24807,7 @@ SOFTWARE RAID (Multiple Disks) SUPPORT
 M:	Song Liu <song@kernel.org>
 M:	Yu Kuai <yukuai@fnnas.com>
 R:	Li Nan <linan122@huawei.com>
+R:	Xiao Ni <xiao@kernel.org>
 L:	linux-raid@vger.kernel.org
 S:	Supported
 Q:	https://patchwork.kernel.org/project/linux-raid/list/
diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h
new file mode 100644
index 0000000000000..9cda3263a0784
--- /dev/null
+++ b/include/linux/mod_lineinfo.h
@@ -0,0 +1,104 @@
+/* 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 and
+ * .init.mod_lineinfo sections embedded in loadable kernel modules.  It
+ * is dual-use: included from both the kernel and the userspace
+ * gen_lineinfo tool.
+ *
+ * Top-level layout (all values in target-native endianness):
+ *
+ *   struct mod_lineinfo_root
+ *   struct mod_lineinfo_section sections[hdr.num_sections]
+ *   ... per-section sub-tables, each pointed at by sections[i].table_offset
+ *
+ * Each mod_lineinfo_section descriptor identifies one ELF text section
+ * covered by the lineinfo blob.  Its .anchor field is an ELF relocation
+ * resolved at module-load time to the runtime base of the named section,
+ * eliminating the need to derive the base from mod->mem[].base segments.
+ * If the relocation fails to resolve (e.g. unknown reloc type), .anchor
+ * stays zero and lookups silently degrade to "no annotation".
+ *
+ * Each per-section sub-table is laid out as a stand-alone
+ * mod_lineinfo_header followed by parallel arrays:
+ *
+ *   struct mod_lineinfo_header     (16 bytes)
+ *   u32 addrs[num_entries]         -- offsets from this section's 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;
+typedef uint64_t u64;
+#endif
+
+/*
+ * Per-section descriptor.  One entry per ELF text section covered by the
+ * blob (.text, .exit.text, .init.text, ...).
+ */
+struct mod_lineinfo_section {
+	u64 anchor;		/* RELOC: runtime base of covered section, or 0 */
+	u32 size;		/* covered section size in bytes */
+	u32 table_offset;	/* byte offset from blob start to this section's
+				 * mod_lineinfo_header */
+};
+
+/*
+ * Top-level header.  Sits at offset 0 of every .mod_lineinfo /
+ * .init.mod_lineinfo section.  The compiler inserts 4 bytes of trailing
+ * padding so the u64 anchor in sections[0] starts 8-byte aligned.
+ */
+struct mod_lineinfo_root {
+	u32 num_sections;
+	struct mod_lineinfo_section sections[];
+};
+
+struct mod_lineinfo_header {
+	u32 num_entries;
+	u32 num_files;
+	u32 filenames_size;	/* total bytes of concatenated filenames */
+};
+
+/* Offset helpers: compute byte offset from the per-section header 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 7566815fabbe8..2bc0263b086d2 100644
--- a/include/linux/module.h
+++ b/include/linux/module.h
@@ -507,6 +507,12 @@ 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;
+	void *init_lineinfo_data;	/* .init.mod_lineinfo, NULL after init runs */
+	unsigned int init_lineinfo_data_size;
+#endif
 #ifdef CONFIG_JUMP_LABEL
 	struct jump_entry *jump_entries;
 	unsigned int num_jump_entries;
@@ -1020,6 +1026,39 @@ static inline unsigned long find_kallsyms_symbol_value(struct module *mod,
 
 #endif  /* CONFIG_MODULES && CONFIG_KALLSYMS */
 
+bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
+			    const char **file, unsigned int *line);
+
+/*
+ * Reader accessors so callers don't need to duplicate the
+ * CONFIG_KALLSYMS_LINEINFO_MODULES guard around mod->lineinfo_data /
+ * mod->init_lineinfo_data field access.  Setters/clearers in the loader
+ * use the field directly under a matching #ifdef.
+ */
+static inline void *module_lineinfo_data(const struct module *mod,
+					 unsigned int *size)
+{
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+	*size = mod->lineinfo_data_size;
+	return mod->lineinfo_data;
+#else
+	*size = 0;
+	return NULL;
+#endif
+}
+
+static inline void *module_init_lineinfo_data(const struct module *mod,
+					      unsigned int *size)
+{
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+	*size = READ_ONCE(mod->init_lineinfo_data_size);
+	return READ_ONCE(mod->init_lineinfo_data);
+#else
+	*size = 0;
+	return NULL;
+#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 99e78c253056a..3e3acfc37be7e 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2085,8 +2085,23 @@ 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.
+
+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 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 1e3f527b13988..d95387f51b4c0 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -551,12 +551,24 @@ static int __sprint_symbol(char *buffer, unsigned long address,
 	 * replaced with bar()"); appending lineinfo there would produce a
 	 * confusing "foo (file:line)()".
 	 */
-	if (add_lineinfo && IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) && !modname) {
+	if (add_lineinfo && IS_ENABLED(CONFIG_KALLSYMS_LINEINFO)) {
 		const char *li_file;
 		unsigned int li_line;
+		bool found = false;
+
+		if (!modname) {
+			found = kallsyms_lookup_lineinfo(address,
+							 &li_file, &li_line);
+		} else if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES)) {
+			struct module *mod = __module_address(address);
+
+			if (mod)
+				found = module_lookup_lineinfo(mod, address,
+							       &li_file,
+							       &li_line);
+		}
 
-		if (kallsyms_lookup_lineinfo(address,
-					     &li_file, &li_line))
+		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..819d6594c2937 100644
--- a/kernel/module/kallsyms.c
+++ b/kernel/module/kallsyms.c
@@ -494,3 +494,184 @@ int module_kallsyms_on_each_symbol(const char *modname,
 	mutex_unlock(&module_mutex);
 	return ret;
 }
+
+#include <linux/mod_lineinfo.h>
+
+/*
+ * Search one per-section sub-table for @section_offset using flat parallel
+ * arrays.  @hdr is the per-section header at byte offset @hdr_offset within
+ * @blob.  Returns true on hit and populates @file / @line.
+ */
+static bool module_lookup_lineinfo_section(const void *blob, u32 blob_size,
+					   u32 hdr_offset,
+					   unsigned int section_offset,
+					   const char **file,
+					   unsigned int *line)
+{
+	const struct mod_lineinfo_header *hdr;
+	const u8 *base;
+	const u32 *addrs, *lines, *file_offsets;
+	const u16 *file_ids;
+	const char *filenames;
+	u32 num_entries, num_files, filenames_size;
+	unsigned int low, high, mid;
+	u16 file_id;
+
+	if (hdr_offset > blob_size ||
+	    blob_size - hdr_offset < sizeof(*hdr))
+		return false;
+
+	base = (const u8 *)blob + hdr_offset;
+	hdr = (const struct mod_lineinfo_header *)base;
+	num_entries = hdr->num_entries;
+	num_files = hdr->num_files;
+	filenames_size = hdr->filenames_size;
+
+	if (num_entries == 0)
+		return false;
+
+	/*
+	 * Validate counts before multiplying — sizing arithmetic could
+	 * otherwise overflow on 32-bit with a malformed blob.  Each entry
+	 * contributes one u32 (addrs), one u16 (file_ids), and one u32
+	 * (lines); each file contributes one u32 (file_offsets).
+	 */
+	{
+		u32 avail = blob_size - hdr_offset;
+		u32 needed = mod_lineinfo_filenames_off(num_entries, num_files);
+
+		if (num_entries > U32_MAX / sizeof(u32))
+			return false;
+		if (num_files > U32_MAX / sizeof(u32))
+			return false;
+		if (needed > avail || filenames_size > avail - needed)
+			return false;
+	}
+
+	/*
+	 * Filenames are read as NUL-terminated C strings.  Require the blob
+	 * to end in NUL so a malformed file_offsets entry can never lead the
+	 * later "%s" consumer past the end of the section.
+	 */
+	if (filenames_size == 0 ||
+	    base[mod_lineinfo_filenames_off(num_entries, num_files) +
+		 filenames_size - 1] != 0)
+		return false;
+
+	addrs = (const u32 *)(base + mod_lineinfo_addrs_off());
+	file_ids = (const u16 *)(base + mod_lineinfo_file_ids_off(num_entries));
+	lines = (const u32 *)(base + mod_lineinfo_lines_off(num_entries));
+	file_offsets = (const u32 *)(base + mod_lineinfo_file_offsets_off(num_entries));
+	filenames = (const char *)(base + mod_lineinfo_filenames_off(num_entries, num_files));
+
+	/* Binary search for largest entry <= section_offset. */
+	low = 0;
+	high = num_entries;
+	while (low < high) {
+		mid = low + (high - low) / 2;
+		if (addrs[mid] <= section_offset)
+			low = mid + 1;
+		else
+			high = mid;
+	}
+
+	if (low == 0)
+		return false;
+	low--;
+
+	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;
+}
+
+/*
+ * Walk a single .mod_lineinfo / .init.mod_lineinfo blob, find the section
+ * descriptor whose [anchor, anchor+size) range contains @addr, then search
+ * that section's sub-table.
+ */
+static bool module_lookup_lineinfo_blob(const void *blob, u32 blob_size,
+					unsigned long addr,
+					const char **file, unsigned int *line)
+{
+	const struct mod_lineinfo_root *root;
+	u32 i, sections_end;
+
+	if (!blob || blob_size < sizeof(*root))
+		return false;
+
+	root = blob;
+	if (root->num_sections == 0)
+		return false;
+
+	if (root->num_sections > U32_MAX / sizeof(struct mod_lineinfo_section))
+		return false;
+	sections_end = sizeof(*root) +
+		       root->num_sections * sizeof(struct mod_lineinfo_section);
+	if (sections_end > blob_size)
+		return false;
+
+	for (i = 0; i < root->num_sections; i++) {
+		const struct mod_lineinfo_section *s = &root->sections[i];
+		unsigned long base = (unsigned long)s->anchor;
+		unsigned long offset;
+
+		if (!base)
+			continue;	/* relocation didn't resolve */
+		if (addr < base)
+			continue;
+		offset = addr - base;
+		if (offset >= s->size)
+			continue;
+		if (offset > U32_MAX)
+			continue;
+
+		return module_lookup_lineinfo_section(blob, blob_size,
+						      s->table_offset,
+						      (unsigned int)offset,
+						      file, line);
+	}
+
+	return false;
+}
+
+/*
+ * Look up source file:line for an address within a loaded module.
+ *
+ * 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,
+			    const char **file, unsigned int *line)
+{
+	const void *blob;
+	unsigned int size;
+
+	if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES))
+		return false;
+
+	blob = module_lineinfo_data(mod, &size);
+	if (blob && module_lookup_lineinfo_blob(blob, size, addr, file, line))
+		return true;
+
+	/*
+	 * The init blob lives in MOD_INIT_RODATA and is revoked by
+	 * do_init_module() before do_free_init() releases the memory.  The
+	 * READ_ONCE inside module_init_lineinfo_data() pairs with the
+	 * WRITE_ONCE in do_init_module so we never see a partial
+	 * pointer/size pair, and an RCU grace period in do_free_init()
+	 * guarantees the memory still exists for the duration of any lookup
+	 * that captured the pointer before the revocation.
+	 */
+	blob = module_init_lineinfo_data(mod, &size);
+	if (blob && module_lookup_lineinfo_blob(blob, size, addr, file, line))
+		return true;
+
+	return false;
+}
diff --git a/kernel/module/main.c b/kernel/module/main.c
index 46dd8d25a6058..46bb2bf799d1e 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -2712,6 +2712,19 @@ 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
+	/*
+	 * Use section_objs() (not any_section_objs) — both blobs carry an
+	 * ELF anchor relocation that the module loader resolves via its
+	 * standard apply_relocations() pass, which only walks SHF_ALLOC
+	 * sections.  Picking up a non-ALLOC section here would also leave
+	 * the pointer dangling into the temporary load image once freed.
+	 */
+	mod->lineinfo_data = section_objs(info, ".mod_lineinfo", 1,
+					  &mod->lineinfo_data_size);
+	mod->init_lineinfo_data = section_objs(info, ".init.mod_lineinfo", 1,
+					       &mod->init_lineinfo_data_size);
+#endif
 #ifdef CONFIG_JUMP_LABEL
 	mod->jump_entries = section_objs(info, "__jump_table",
 					sizeof(*mod->jump_entries),
@@ -3165,6 +3178,19 @@ static noinline int do_init_module(struct module *mod)
 	/* .BTF is not SHF_ALLOC and will get removed, so sanitize pointers */
 	mod->btf_data = NULL;
 	mod->btf_base_data = NULL;
+#endif
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+	/*
+	 * .init.mod_lineinfo lives in MOD_INIT_RODATA which do_free_init() is
+	 * about to release.  Clear the pointer so concurrent stack-trace
+	 * lookups stop dereferencing it; do_free_init()'s synchronize_rcu()
+	 * then waits out any reader that already captured the old pointer.
+	 * WRITE_ONCE pairs with the READ_ONCE inside module_init_lineinfo_data()
+	 * so the compiler can't tear or reorder the revocation across the
+	 * llist_add() that follows.
+	 */
+	WRITE_ONCE(mod->init_lineinfo_data, NULL);
+	WRITE_ONCE(mod->init_lineinfo_data_size, 0);
 #endif
 	/*
 	 * We want to free module_init, but be aware that kallsyms may be
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 100644
index 0000000000000..832d290f3bf4c
--- /dev/null
+++ b/scripts/gen-mod-lineinfo.sh
@@ -0,0 +1,50 @@
+#!/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 that contains
+# an ELF relocation against the module's .text section symbol, and partial-
+# links the result back into the .ko via "ld -r" so the relocation rides
+# along to the module loader.  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.tmp"
+}
+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"; 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"
+
+# Partial-link lineinfo.o INTO the .ko.  Order matters: lineinfo.o must come
+# FIRST so its empty .text contributes 0 bytes at offset 0 of the merged
+# .text, which keeps the .quad .text relocation (against lineinfo.o's local
+# .text symbol, which after merge points at offset 0 of merged .text)
+# resolving to the start of the module's .text.  Reversing inputs here
+# silently breaks lookup correctness.
+${LD} -r "${KO}.lineinfo.o" "${KO}" -o "${KO}.lineinfo.tmp"
+mv "${KO}.lineinfo.tmp" "${KO}"
+
+exit 0
diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c
index 699e760178f09..e1e08469b4f2f 100644
--- a/scripts/gen_lineinfo.c
+++ b/scripts/gen_lineinfo.c
@@ -24,16 +24,66 @@
 #include <gelf.h>
 #include <limits.h>
 
+#include "../include/linux/mod_lineinfo.h"
+
+static int module_mode;
+
 static unsigned int skipped_overflow;
 
+/* Target ELF traits, captured once in main() and reused at emit time. */
+static bool target_64bit;
+static bool target_le;
+
 /*
- * vmlinux mode: end of the invariant .text region.  Zero means "no cap"
- * (graceful fallback when _etext is absent on some build).
+ * Vmlinux mode only: address range of the *invariant* .text region.
+ * See find_text_end_addr() for why we cap on _etext.  text_end_addr == 0
+ * means "no cap available; capture everything above text_addr" (v3
+ * behavior, used as graceful fallback if _etext is absent).
  */
 static unsigned long long text_end_addr;
 
+/*
+ * In module mode we cover several text-like sections, split across two
+ * output blobs by lifecycle:
+ *
+ *   .mod_lineinfo      -- persistent code (.text, .exit.text); MOD_RODATA
+ *   .init.mod_lineinfo -- init code (.init.text); freed with init memory
+ *
+ * In ET_REL .ko files .text/.init.text/.exit.text all have sh_addr == 0,
+ * so DWARF line addresses (which become sh_addr + addend after relocation)
+ * collide across sections.  We disambiguate by giving each *present*
+ * covered section a unique synthetic "bias" — a u32 base address — and
+ * adding that bias to relocated values inside apply_debug_line_relocations.
+ * libdw then yields biased addresses that classify_address() can map back
+ * to a single section unambiguously.  The bias is internal to gen_lineinfo
+ * and never leaks into the emitted blob.
+ */
+enum mod_lineinfo_blob {
+	BLOB_PERSISTENT,
+	BLOB_INIT,
+	NUM_BLOBS,
+};
+
+struct covered_section {
+	const char *name;	/* ELF section name (e.g. ".text") */
+	enum mod_lineinfo_blob blob;
+	unsigned long long bias;/* synthetic base address (set in resolve_*) */
+	unsigned long long size;
+	bool present;		/* found in this .ko */
+	unsigned int sec_index;	/* ELF section header index, for reloc matching */
+	unsigned int n_entries;	/* DWARF line entries collected for this section */
+};
+
+static struct covered_section all_sections[] = {
+	{ .name = ".text",      .blob = BLOB_PERSISTENT },
+	{ .name = ".exit.text", .blob = BLOB_PERSISTENT },
+	{ .name = ".init.text", .blob = BLOB_INIT },
+};
+#define ALL_SECTIONS	(sizeof(all_sections) / sizeof(all_sections[0]))
+
 struct line_entry {
-	unsigned int offset;	/* offset from _text */
+	unsigned int offset;	/* offset from covered section's start */
+	unsigned int section_id;/* index into covered_sections[] (module mode only) */
 	unsigned int file_id;
 	unsigned int line;
 };
@@ -52,7 +102,12 @@ static struct file_entry *files;
 static unsigned int num_files;
 static unsigned int files_capacity;
 
-#define FILE_HASH_BITS 13
+/*
+ * Hash size must comfortably exceed the 65535-file cap below so the open
+ * addressing in find_or_add_file() always has a free slot to land on.
+ * 17 bits = 131072 entries gives ~50% max load factor.
+ */
+#define FILE_HASH_BITS 17
 #define FILE_HASH_SIZE (1 << FILE_HASH_BITS)
 
 struct file_hash_entry {
@@ -71,8 +126,8 @@ static unsigned int hash_str(const char *s)
 	return h & (FILE_HASH_SIZE - 1);
 }
 
-static void add_entry(unsigned int offset, unsigned int file_id,
-		      unsigned int line)
+static void add_entry(unsigned int offset, unsigned int section_id,
+		      unsigned int file_id, unsigned int line)
 {
 	if (num_entries >= entries_capacity) {
 		entries_capacity = entries_capacity ? entries_capacity * 2 : 65536;
@@ -83,6 +138,7 @@ static void add_entry(unsigned int offset, unsigned int file_id,
 		}
 	}
 	entries[num_entries].offset = offset;
+	entries[num_entries].section_id = section_id;
 	entries[num_entries].file_id = file_id;
 	entries[num_entries].line = line;
 	num_entries++;
@@ -155,27 +211,25 @@ 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] == '/') {
-			const char *rel = path + len + 1;
-
-			/*
-			 * If comp_dir pointed to a subdirectory
-			 * (e.g. arch/parisc/kernel) rather than
-			 * the tree root, stripping it leaves a
-			 * bare filename.  Fall through to the
-			 * kernel_dirs scan so we recover the full
-			 * relative path instead.
-			 */
-			if (strchr(rel, '/'))
-				return rel;
+	if (path[0] == '/') {
+		/* Try comp_dir prefix from DWARF */
+		if (comp_dir) {
+			size_t len = strlen(comp_dir);
+
+			if (!strncmp(path, comp_dir, len) && path[len] == '/') {
+				const char *rel = path + len + 1;
+
+				/*
+				 * If comp_dir pointed to a subdirectory
+				 * (e.g. arch/parisc/kernel) rather than
+				 * the tree root, stripping it leaves a
+				 * bare filename.  Fall through to the
+				 * kernel_dirs scan so we recover the full
+				 * relative path instead.
+				 */
+				if (strchr(rel, '/'))
+					return rel;
+			}
 		}
 
 		/*
@@ -201,9 +255,42 @@ static const char *make_relative(const char *path, const char *comp_dir)
 		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);
+		size_t mid = len / 2;
+
+		if (len > 1 && path[mid] == '/' &&
+		    !memcmp(path, path + mid + 1, mid))
+			return path + mid + 1;
+	}
+
+	/*
+	 * Bare filename with no directory component — try to recover the
+	 * relative path using comp_dir.  Some toolchains/elfutils combos
+	 * produce bare filenames where comp_dir holds the source directory.
+	 * Construct the absolute path and run the kernel_dirs scan.
+	 */
+	if (!strchr(path, '/') && comp_dir && comp_dir[0] == '/') {
+		static char buf[PATH_MAX];
+
+		snprintf(buf, sizeof(buf), "%s/%s", comp_dir, path);
+		for (p = buf + 1; *p; p++) {
+			if (*(p - 1) == '/') {
+				for (unsigned int i = 0; i < sizeof(kernel_dirs) /
+				     sizeof(kernel_dirs[0]); i++) {
+					if (!strncmp(p, kernel_dirs[i],
+						     strlen(kernel_dirs[i])))
+						return p;
+				}
+			}
+		}
+	}
+
+	return path;
 }
 
 static int compare_entries(const void *a, const void *b)
@@ -211,6 +298,9 @@ static int compare_entries(const void *a, const void *b)
 	const struct line_entry *ea = a;
 	const struct line_entry *eb = b;
 
+	/* Group by section first so each per-section table is contiguous. */
+	if (ea->section_id != eb->section_id)
+		return ea->section_id < eb->section_id ? -1 : 1;
 	if (ea->offset != eb->offset)
 		return ea->offset < eb->offset ? -1 : 1;
 	if (ea->file_id != eb->file_id)
@@ -222,7 +312,8 @@ static int compare_entries(const void *a, const void *b)
 
 /*
  * Look up a vmlinux symbol by exact name and return its st_value, or
- * @fallback if absent.  Aborts when @required and the symbol is missing.
+ * @fallback if the symbol is absent (lets callers gracefully skip
+ * optional bounds like _etext).
  */
 static unsigned long long find_vmlinux_sym(Elf *elf, const char *name,
 					   unsigned long long fallback,
@@ -270,22 +361,325 @@ static unsigned long long find_text_addr(Elf *elf)
 }
 
 /*
- * vmlinux is linked in multiple passes: gen_lineinfo runs against
- * .tmp_vmlinux1 (which carries an empty lineinfo stub), then real tables
- * are linked in for the final image.  Sections placed AFTER .rodata
- * (.init.text, .exit.text, ...) shift forward as .rodata grows to hold
- * the real lineinfo blob, so DWARF addresses we'd capture for them in
- * pass 1 would be stale in the final kernel.  Cap captured addresses at
- * _etext, the symbol that marks the end of .text — placed before .rodata
- * in every architecture's vmlinux.lds.S, so its addresses are invariant
- * across the relink.  Returns 0 if _etext is absent (no cap; v3 behavior).
+ * Vmlinux is linked in multiple passes: gen_lineinfo runs against
+ * .tmp_vmlinux1 (which carries the empty lineinfo stub), and the resulting
+ * tables are then linked into the final vmlinux.  Sections placed AFTER
+ * .rodata (.init.text, .exit.text, ...) shift forward as the real lineinfo
+ * tables replace the empty stub, so DWARF addresses we'd capture for them
+ * here are stale by the time the kernel runs.
+ *
+ * Cap the captured range at _etext, the symbol that marks the end of the
+ * .text section.  .text is placed BEFORE .rodata in every architecture's
+ * vmlinux.lds.S, so its addresses are invariant across the relink.
+ * Returns 0 on architectures or builds that don't expose _etext, in which
+ * case the cap is disabled (preserving the v3 behavior — addresses past
+ * .text remain captured but may be off in stack traces).
  */
 static unsigned long long find_text_end_addr(Elf *elf)
 {
 	return find_vmlinux_sym(elf, "_etext", 0, false);
 }
 
-static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
+/*
+ * Populate @sections[].present/sec_index/size/bias.  Sections that don't
+ * exist stay marked absent.  Biases are assigned in array order: each
+ * present section gets a base equal to the running total of preceding
+ * present sections' sizes, rounded up to 16 to keep ranges sparse.  This
+ * guarantees [bias, bias+size) ranges are pairwise disjoint and fit in
+ * u32 as long as the sum of all covered text sizes is below 4 GiB.
+ */
+static void resolve_covered_sections(Elf *elf,
+				     struct covered_section *sections,
+				     unsigned int num_sections)
+{
+	Elf_Scn *scn = NULL;
+	GElf_Shdr shdr;
+	size_t shstrndx;
+	unsigned long long cursor = 0;
+
+	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)
+			continue;
+		for (unsigned int i = 0; i < num_sections; i++) {
+			if (sections[i].present)
+				continue;
+			if (strcmp(name, sections[i].name))
+				continue;
+			if (shdr.sh_size > UINT_MAX) {
+				fprintf(stderr,
+					"lineinfo: section %s exceeds 4 GiB (size=%llu); skipping\n",
+					name,
+					(unsigned long long)shdr.sh_size);
+				break;
+			}
+			sections[i].sec_index = elf_ndxscn(scn);
+			sections[i].size = shdr.sh_size;
+			sections[i].present = true;
+			break;
+		}
+	}
+
+	/* Pack present sections into non-overlapping bias ranges. */
+	for (unsigned int i = 0; i < num_sections; i++) {
+		if (!sections[i].present)
+			continue;
+		sections[i].bias = cursor;
+		cursor += sections[i].size;
+		cursor = (cursor + 15) & ~15ULL;	/* pad for separation */
+	}
+}
+
+/* Look up a covered_section by ELF section header index. */
+static struct covered_section *section_by_index(struct covered_section *sections,
+						unsigned int num_sections,
+						unsigned int sec_index)
+{
+	for (unsigned int i = 0; i < num_sections; i++) {
+		if (sections[i].present && sections[i].sec_index == sec_index)
+			return &sections[i];
+	}
+	return NULL;
+}
+
+/*
+ * Apply .rela.debug_line relocations to a mutable copy of .debug_line data.
+ *
+ * elfutils libdw (through at least 0.194) does NOT apply relocations for
+ * ET_REL files when using dwarf_begin_elf().  The internal libdwfl layer
+ * does this via __libdwfl_relocate(), but that API is not public.
+ *
+ * For DWARF5, the .debug_line file name table uses DW_FORM_line_strp
+ * references into .debug_line_str.  Without relocation, all these offsets
+ * resolve to 0 (or garbage), causing dwarf_linesrc()/dwarf_filesrc() to
+ * return wrong filenames (typically the comp_dir for every file).
+ *
+ * This function applies the relocations manually so that the patched
+ * .debug_line data can be fed to dwarf_begin_elf() and produce correct
+ * results.
+ *
+ * See elfutils bug https://sourceware.org/bugzilla/show_bug.cgi?id=31447
+ * A fix (dwelf_elf_apply_relocs) was proposed but not yet merged as of
+ * elfutils 0.194: https://sourceware.org/pipermail/elfutils-devel/2024q3/007388.html
+ */
+/*
+ * Determine the relocation type for a 32-bit absolute reference
+ * on the given architecture.  Returns 0 if unknown.
+ */
+static unsigned int r_type_abs32(unsigned int e_machine)
+{
+	switch (e_machine) {
+	case EM_X86_64:		return R_X86_64_32;
+	case EM_386:		return R_386_32;
+	case EM_AARCH64:	return R_AARCH64_ABS32;
+	case EM_ARM:		return R_ARM_ABS32;
+	case EM_RISCV:		return R_RISCV_32;
+	case EM_S390:		return R_390_32;
+	case EM_MIPS:		return R_MIPS_32;
+	case EM_PPC64:		return R_PPC64_ADDR32;
+	case EM_PPC:		return R_PPC_ADDR32;
+	case EM_LOONGARCH:	return R_LARCH_32;
+	case EM_PARISC:		return R_PARISC_DIR32;
+	default:		return 0;
+	}
+}
+
+/*
+ * Determine the relocation type for a 64-bit absolute reference
+ * on the given architecture.  Returns 0 on 32-bit-only architectures
+ * (where DW_LNE_set_address fits in 32 bits and r_type_abs32 covers it).
+ */
+static unsigned int r_type_abs64(unsigned int e_machine)
+{
+	switch (e_machine) {
+	case EM_X86_64:		return R_X86_64_64;
+	case EM_AARCH64:	return R_AARCH64_ABS64;
+	case EM_RISCV:		return R_RISCV_64;
+	case EM_S390:		return R_390_64;
+	case EM_MIPS:		return R_MIPS_64;
+	case EM_PPC64:		return R_PPC64_ADDR64;
+	case EM_LOONGARCH:	return R_LARCH_64;
+	case EM_PARISC:		return R_PARISC_DIR64;
+	default:		return 0;
+	}
+}
+
+/*
+ * Write a 4- or 8-byte unsigned integer in target byte order.
+ * Cross-builds (e.g. x86_64 host -> s390 module) need the patched
+ * .debug_line bytes laid out per the .ko's e_ident[EI_DATA], not the host's.
+ */
+static void elf_write_uint(unsigned char *dst, uint64_t value, size_t size,
+			   bool little_endian)
+{
+	if (little_endian) {
+		for (size_t i = 0; i < size; i++)
+			dst[i] = (value >> (i * 8)) & 0xff;
+	} else {
+		for (size_t i = 0; i < size; i++)
+			dst[i] = (value >> ((size - 1 - i) * 8)) & 0xff;
+	}
+}
+
+static void apply_debug_line_relocations(Elf *elf)
+{
+	Elf_Scn *scn = NULL;
+	Elf_Scn *debug_line_scn = NULL;
+	Elf_Scn *rela_debug_line_scn = NULL;
+	Elf_Scn *symtab_scn = NULL;
+	GElf_Shdr shdr;
+	GElf_Ehdr ehdr;
+	unsigned int abs32_type, abs64_type;
+	bool target_le;
+	size_t shstrndx;
+	Elf_Data *dl_data, *rela_data, *sym_data;
+	GElf_Shdr rela_shdr, sym_shdr;
+	size_t nrels, i;
+
+	if (gelf_getehdr(elf, &ehdr) == NULL)
+		return;
+
+	abs32_type = r_type_abs32(ehdr.e_machine);
+	abs64_type = r_type_abs64(ehdr.e_machine);
+	if (!abs32_type && !abs64_type)
+		return;
+	target_le = (ehdr.e_ident[EI_DATA] == ELFDATA2LSB);
+
+	if (elf_getshdrstrndx(elf, &shstrndx) != 0)
+		return;
+
+	/* Find the relevant sections */
+	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)
+			continue;
+
+		if (!strcmp(name, ".debug_line"))
+			debug_line_scn = scn;
+		else if (!strcmp(name, ".rela.debug_line"))
+			rela_debug_line_scn = scn;
+		else if (shdr.sh_type == SHT_SYMTAB)
+			symtab_scn = scn;
+	}
+
+	if (!debug_line_scn || !rela_debug_line_scn || !symtab_scn)
+		return;
+
+	dl_data = elf_getdata(debug_line_scn, NULL);
+	rela_data = elf_getdata(rela_debug_line_scn, NULL);
+	sym_data = elf_getdata(symtab_scn, NULL);
+	if (!dl_data || !rela_data || !sym_data)
+		return;
+
+	if (!gelf_getshdr(rela_debug_line_scn, &rela_shdr))
+		return;
+	if (!gelf_getshdr(symtab_scn, &sym_shdr))
+		return;
+
+	nrels = rela_shdr.sh_size / rela_shdr.sh_entsize;
+
+	for (i = 0; i < nrels; i++) {
+		GElf_Rela rela;
+		GElf_Sym sym;
+		unsigned int r_type;
+		size_t r_sym;
+		bool is_abs64;
+
+		if (!gelf_getrela(rela_data, i, &rela))
+			continue;
+
+		r_type = GELF_R_TYPE(rela.r_info);
+		r_sym = GELF_R_SYM(rela.r_info);
+
+		/*
+		 * Two reloc widths matter for .debug_line:
+		 *   abs32 - DW_FORM_line_strp file-table refs into .debug_line_str
+		 *   abs64 - DW_LNE_set_address arguments (sequence start PCs)
+		 * Without both, libdw sees zeros and reports wrong filenames or
+		 * collapses every sequence to address 0 (collision after dedup).
+		 */
+		if (abs32_type && r_type == abs32_type) {
+			is_abs64 = false;
+		} else if (abs64_type && r_type == abs64_type) {
+			is_abs64 = true;
+		} else {
+			continue;
+		}
+
+		if (!gelf_getsym(sym_data, r_sym, &sym))
+			continue;
+
+		size_t width = is_abs64 ? 8 : 4;
+		uint64_t value = (uint64_t)(sym.st_value + rela.r_addend);
+
+		/*
+		 * If the relocation targets one of our covered text sections,
+		 * fold in that section's synthetic bias so the patched DWARF
+		 * address lands in a unique numeric range.  String-ref relocs
+		 * (DW_FORM_line_strp into .debug_line_str) target a different
+		 * section, so the symbol-based check correctly excludes them
+		 * from biasing — for both abs64 (64-bit ELF) and abs32 (32-bit
+		 * ELF, where DW_LNE_set_address is also 4 bytes wide).
+		 */
+		if (module_mode) {
+			struct covered_section *cs;
+
+			cs = section_by_index(all_sections, ALL_SECTIONS,
+					      sym.st_shndx);
+			if (cs)
+				value += cs->bias;
+		}
+
+		if (!is_abs64)
+			value &= 0xffffffffULL;
+
+		if (rela.r_offset + width <= dl_data->d_size)
+			elf_write_uint((unsigned char *)dl_data->d_buf +
+					       rela.r_offset,
+				       value, width, target_le);
+	}
+}
+
+/*
+ * Decide which covered_section a (biased) DWARF address belongs to.
+ * apply_debug_line_relocations() has already added the section's bias to
+ * each line-program PC, so [bias, bias+size) ranges are pairwise disjoint
+ * and a simple linear scan picks the right bucket.  Returns the index
+ * within @sections, or @num_sections if @addr falls outside every
+ * present range (caller skips the entry).
+ */
+static unsigned int classify_address(struct covered_section *sections,
+				     unsigned int num_sections,
+				     unsigned long long addr,
+				     unsigned long long *out_offset)
+{
+	for (unsigned int i = 0; i < num_sections; i++) {
+		if (!sections[i].present)
+			continue;
+		if (addr < sections[i].bias)
+			continue;
+		if (addr >= sections[i].bias + sections[i].size)
+			continue;
+		*out_offset = addr - sections[i].bias;
+		return i;
+	}
+	return num_sections;
+}
+
+static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr,
+			  struct covered_section *sections,
+			  unsigned int num_sections)
 {
 	Dwarf_Off off = 0, next_off;
 	size_t hdr_size;
@@ -312,7 +706,8 @@ static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
 			Dwarf_Addr addr;
 			const char *src;
 			const char *rel;
-			unsigned int file_id, loffset;
+			unsigned int file_id, loffset, sec_id;
+			unsigned long long sec_off;
 			int lineno;
 
 			if (!line)
@@ -329,56 +724,87 @@ static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
 			if (!src)
 				continue;
 
-			if (addr < text_addr)
-				continue;
-			/*
-			 * Skip addresses past _etext.  Sections after .rodata
-			 * shift when the real lineinfo replaces the empty stub
-			 * during the multi-pass vmlinux link, so any address
-			 * we'd capture there would be stale by the time the
-			 * final kernel runs.
-			 */
-			if (text_end_addr && addr >= text_end_addr)
-				continue;
-
-			{
-				unsigned long long raw_offset = addr - text_addr;
+			if (module_mode) {
+				/*
+				 * In ET_REL .ko files .text/.init.text/.exit.text
+				 * all share sh_addr == 0; classify_address picks
+				 * the right bucket from the explicit ranges we
+				 * captured.
+				 */
+				sec_id = classify_address(sections, num_sections,
+							  addr, &sec_off);
+				if (sec_id == num_sections)
+					continue;
+				if (sec_off > UINT_MAX) {
+					skipped_overflow++;
+					continue;
+				}
+				loffset = (unsigned int)sec_off;
+				sections[sec_id].n_entries++;
+			} else {
+				unsigned long long raw_offset;
 
+				if (addr < text_addr)
+					continue;
+				/*
+				 * Skip addresses past _etext.  Sections after
+				 * .rodata shift when the real lineinfo replaces
+				 * the empty stub during the multi-pass vmlinux
+				 * link, so any address we'd capture there would
+				 * be stale by the time the final kernel runs.
+				 */
+				if (text_end_addr && addr >= text_end_addr)
+					continue;
+				raw_offset = addr - text_addr;
 				if (raw_offset > UINT_MAX) {
 					skipped_overflow++;
 					continue;
 				}
 				loffset = (unsigned int)raw_offset;
+				sec_id = 0;
 			}
 
 			rel = make_relative(src, comp_dir);
 			file_id = find_or_add_file(rel);
 
-			add_entry(loffset, file_id, (unsigned int)lineno);
+			add_entry(loffset, sec_id, file_id, (unsigned int)lineno);
 		}
 next:
 		off = next_off;
 	}
 }
 
-static void deduplicate(void)
+static void deduplicate(struct covered_section *sections,
+			unsigned int num_sections)
 {
 	unsigned int i, j;
 
 	if (num_entries < 2)
 		return;
 
-	/* Sort by offset, then file_id, then line for stability */
+	/*
+	 * Sort by section_id, then offset, then file_id, line.  This groups
+	 * each section's entries contiguously so the per-section emit can
+	 * iterate a simple range, and ensures the binary search invariant
+	 * (offsets ascending) holds within each section.
+	 */
 	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
+	 * Remove duplicates.  Reset on a section_id boundary: the same offset
+	 * can legitimately appear in two different sections (they all start
+	 * at sh_addr 0 in ET_REL), and the "same as previous kept entry"
+	 * collapse is only meaningful inside one section's binary-search
+	 * domain.
 	 */
 	j = 0;
 	for (i = 1; i < num_entries; i++) {
+		if (entries[i].section_id != entries[j].section_id) {
+			j++;
+			if (j != i)
+				entries[j] = entries[i];
+			continue;
+		}
 		if (entries[i].offset == entries[j].offset)
 			continue;
 		if (entries[i].file_id == entries[j].file_id &&
@@ -389,6 +815,14 @@ static void deduplicate(void)
 			entries[j] = entries[i];
 	}
 	num_entries = j + 1;
+
+	/* Recompute per-section n_entries from the deduped array. */
+	if (sections) {
+		for (unsigned int k = 0; k < num_sections; k++)
+			sections[k].n_entries = 0;
+		for (i = 0; i < num_entries; i++)
+			sections[entries[i].section_id].n_entries++;
+	}
 }
 
 static void compute_file_offsets(void)
@@ -486,6 +920,199 @@ static void output_assembly(void)
 	printf("\n");
 }
 
+/*
+ * Emit one per-section table in the simple flat-array layout:
+ *
+ *   mod_lineinfo_header
+ *   addrs[count]    (u32, sorted)
+ *   file_ids[count] (u16) + 2-byte pad if count is odd
+ *   lines[count]    (u32)
+ *   file_offsets[]  (u32)
+ *   filenames[]
+ *
+ * @suffix uniquifies labels so multiple tables can coexist in one blob.
+ * Caller has sorted entries[] so this section's entries occupy [first,
+ * first + count).
+ */
+static void emit_section_table(unsigned int first, unsigned int count,
+			       const char *suffix)
+{
+	printf(".Lhdr%s:\n", suffix);
+	printf("\t.balign 4\n");
+	printf("\t.long %u\t\t/* num_entries */\n", count);
+	printf("\t.long %u\t\t/* num_files */\n", num_files);
+	printf("\t.long .Lfilenames_end%s - .Lfilenames%s\n\n", suffix, suffix);
+
+	/* addrs[] */
+	for (unsigned int i = 0; i < count; i++)
+		printf("\t.long 0x%x\n", entries[first + i].offset);
+
+	/* file_ids[] */
+	for (unsigned int i = 0; i < count; i++)
+		printf("\t.short %u\n", entries[first + i].file_id);
+	if (count & 1)
+		printf("\t.short 0\t\t/* pad to align lines[] */\n");
+
+	/* lines[] */
+	for (unsigned int i = 0; i < count; i++)
+		printf("\t.long %u\n", entries[first + i].line);
+
+	/* file_offsets[] */
+	printf("\t.balign 4\n");
+	for (unsigned int i = 0; i < num_files; i++)
+		printf("\t.long %u\n", files[i].str_offset);
+
+	/* filenames[] */
+	printf(".Lfilenames%s:\n", suffix);
+	for (unsigned int i = 0; i < num_files; i++)
+		print_escaped_asciz(files[i].name);
+	printf(".Lfilenames_end%s:\n", suffix);
+}
+
+/*
+ * Emit one mod_lineinfo_section descriptor.  The "anchor" field is a
+ * relocation against the named ELF section symbol; the module loader
+ * resolves it on load to the runtime base of that section.
+ *
+ * On 64-bit ELF: 8-byte slot via .quad <name> (R_*_64 reloc).
+ * On 32-bit ELF: 4-byte reloc via .long <name>, plus 4 bytes of zero
+ * padding.  The two halves are ordered to match target endianness so a
+ * naive u64 read on the kernel side recovers the relocated value.
+ */
+static void emit_section_descriptor(const char *section_name,
+				    unsigned long long size,
+				    const char *table_label,
+				    const char *root_label)
+{
+	if (target_64bit) {
+		printf("\t.quad %s\t/* sections[].anchor (RELOC) */\n",
+		       section_name);
+	} else if (target_le) {
+		printf("\t.long %s\t/* sections[].anchor low (RELOC) */\n",
+		       section_name);
+		printf("\t.long 0\t\t/* sections[].anchor high pad */\n");
+	} else {
+		printf("\t.long 0\t\t/* sections[].anchor high pad */\n");
+		printf("\t.long %s\t/* sections[].anchor low (RELOC) */\n",
+		       section_name);
+	}
+	printf("\t.long %llu\t/* sections[].size */\n", size);
+	printf("\t.long %s - %s\t/* sections[].table_offset */\n",
+	       table_label, root_label);
+}
+
+/*
+ * Emit one .mod_lineinfo / .init.mod_lineinfo blob.  Walks all_sections[]
+ * picking only entries that (a) belong to the requested blob and (b)
+ * actually produced at least one DWARF line entry — sections present in
+ * the .ko but without DWARF (e.g. compiler-generated stub thunks) are
+ * silently skipped.  The caller-supplied entries[] is already sorted by
+ * section_id, so each section's entries are contiguous; we walk the
+ * master array in order to compute per-section starting indices.
+ */
+static void emit_blob(const char *output_section,
+		      const char *blob_tag,
+		      enum mod_lineinfo_blob blob)
+{
+	unsigned int active = 0;
+	unsigned int section_starts[ALL_SECTIONS];
+	unsigned int cursor = 0;
+
+	for (unsigned int i = 0; i < ALL_SECTIONS; i++) {
+		section_starts[i] = cursor;
+		cursor += all_sections[i].n_entries;
+		if (all_sections[i].blob == blob && all_sections[i].n_entries)
+			active++;
+	}
+
+	if (!active)
+		return;
+
+	printf("\t.section %s, \"a\"\n\n", output_section);
+
+	printf("\t.balign 8\n");
+	printf(".Lroot_%s:\n", blob_tag);
+	printf("\t.long %u\t\t/* num_sections */\n", active);
+	/* Pad to align the u64 anchor in sections[0] to 8 bytes. */
+	printf("\t.balign 8\n");
+
+	{
+		unsigned int slot = 0;
+		for (unsigned int i = 0; i < ALL_SECTIONS; i++) {
+			char table_label[64];
+			char root_label[64];
+
+			if (all_sections[i].blob != blob)
+				continue;
+			if (!all_sections[i].n_entries)
+				continue;
+			snprintf(table_label, sizeof(table_label),
+				 ".Lhdr_%s_%u", blob_tag, slot);
+			snprintf(root_label, sizeof(root_label),
+				 ".Lroot_%s", blob_tag);
+			emit_section_descriptor(all_sections[i].name,
+						all_sections[i].size,
+						table_label, root_label);
+			slot++;
+		}
+	}
+	printf("\n");
+
+	{
+		unsigned int slot = 0;
+
+		for (unsigned int i = 0; i < ALL_SECTIONS; i++) {
+			char suffix[64];
+
+			if (all_sections[i].blob != blob)
+				continue;
+			if (!all_sections[i].n_entries)
+				continue;
+			snprintf(suffix, sizeof(suffix), "_%s_%u",
+				 blob_tag, slot);
+			emit_section_table(section_starts[i],
+					   all_sections[i].n_entries,
+					   suffix);
+			slot++;
+		}
+	}
+	printf("\n");
+}
+
+/*
+ * Declare each text-like section we plan to reference as an empty
+ * SHF_EXECINSTR section in this object.  Without these stanzas the
+ * assembler treats `.quad .exit.text` as an undefined external symbol;
+ * after ld -r the resulting GLOBAL UND `.exit.text` doesn't bind to the
+ * .ko's LOCAL SECTION symbol of the same name, leaving depmod with an
+ * unresolved-symbol warning and the loader unable to relocate the anchor.
+ *
+ * Declaring the section here gives lineinfo.o its own local SECTION
+ * symbol; ld -r merges sections by name so the local symbol simply
+ * relocates to offset 0 of the merged section (lineinfo.o is linked
+ * FIRST so its zero-byte contribution stays at the start).
+ */
+static void declare_empty_text_sections(void)
+{
+	for (unsigned int i = 0; i < ALL_SECTIONS; i++) {
+		if (!all_sections[i].present)
+			continue;
+		printf("\t.section %s, \"ax\"\n", all_sections[i].name);
+	}
+	printf("\n");
+}
+
+static void output_module_assembly(void)
+{
+	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");
+
+	declare_empty_text_sections();
+}
+
 int main(int argc, char *argv[])
 {
 	int fd;
@@ -493,12 +1120,23 @@ 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;
 	}
 
-	fd = open(argv[1], O_RDONLY);
+	/*
+	 * For module mode, open O_RDWR so we can apply debug section
+	 * relocations to the in-memory ELF data.  The modifications
+	 * are NOT written back to disk (no elf_update() call).
+	 */
+	fd = open(argv[1], module_mode ? O_RDWR : O_RDONLY);
 	if (fd < 0) {
 		fprintf(stderr, "Cannot open %s: %s\n", argv[1],
 			strerror(errno));
@@ -506,7 +1144,7 @@ int main(int argc, char *argv[])
 	}
 
 	elf_version(EV_CURRENT);
-	elf = elf_begin(fd, ELF_C_READ, NULL);
+	elf = elf_begin(fd, module_mode ? ELF_C_RDWR : ELF_C_READ, NULL);
 	if (!elf) {
 		fprintf(stderr, "elf_begin failed: %s\n",
 			elf_errmsg(elf_errno()));
@@ -514,8 +1152,34 @@ int main(int argc, char *argv[])
 		return 1;
 	}
 
-	text_addr = find_text_addr(elf);
-	text_end_addr = find_text_end_addr(elf);
+	{
+		GElf_Ehdr ehdr;
+
+		if (gelf_getehdr(elf, &ehdr) == NULL) {
+			fprintf(stderr, "gelf_getehdr failed\n");
+			elf_end(elf);
+			close(fd);
+			return 1;
+		}
+		target_64bit = (ehdr.e_ident[EI_CLASS] == ELFCLASS64);
+		target_le = (ehdr.e_ident[EI_DATA] == ELFDATA2LSB);
+	}
+
+	if (module_mode) {
+		/*
+		 * .ko files are ET_REL after ld -r.  Resolve covered text
+		 * sections FIRST so apply_debug_line_relocations() can use
+		 * the assigned biases when patching line-program addresses;
+		 * libdw does NOT apply relocations for ET_REL files, so we
+		 * also handle DW_FORM_line_strp refs into .debug_line_str.
+		 */
+		resolve_covered_sections(elf, all_sections, ALL_SECTIONS);
+		apply_debug_line_relocations(elf);
+		text_addr = 0;	/* unused in module mode */
+	} else {
+		text_addr = find_text_addr(elf);
+		text_end_addr = find_text_end_addr(elf);
+	}
 
 	dwarf = dwarf_begin_elf(elf, DWARF_C_READ, NULL);
 	if (!dwarf) {
@@ -528,20 +1192,55 @@ int main(int argc, char *argv[])
 		return 1;
 	}
 
-	process_dwarf(dwarf, text_addr);
+	if (module_mode) {
+		unsigned int persistent_total, init_total;
+
+		output_module_assembly();	/* file header only */
 
-	if (skipped_overflow)
+		/*
+		 * Single DWARF pass classifies every line entry into its
+		 * covering section (or skips it).  Each entry is tagged with
+		 * the master-array section_id so per-blob emit can filter.
+		 */
+		process_dwarf(dwarf, 0, all_sections, ALL_SECTIONS);
+		deduplicate(all_sections, ALL_SECTIONS);
+		compute_file_offsets();
+
+		emit_blob(".mod_lineinfo", "p", BLOB_PERSISTENT);
+		emit_blob(".init.mod_lineinfo", "i", BLOB_INIT);
+
+		persistent_total = 0;
+		init_total = 0;
+		for (unsigned int i = 0; i < ALL_SECTIONS; i++) {
+			if (all_sections[i].blob == BLOB_PERSISTENT)
+				persistent_total += all_sections[i].n_entries;
+			else if (all_sections[i].blob == BLOB_INIT)
+				init_total += all_sections[i].n_entries;
+		}
 		fprintf(stderr,
-			"lineinfo: warning: %u entries skipped (offset > 4 GiB from _text)\n",
-			skipped_overflow);
+			"lineinfo: persistent %u entries, init %u entries, %u files\n",
+			persistent_total, init_total, num_files);
+
+		if (skipped_overflow)
+			fprintf(stderr,
+				"lineinfo: warning: %u entries skipped (offset > 4 GiB)\n",
+				skipped_overflow);
+	} else {
+		process_dwarf(dwarf, text_addr, NULL, 0);
 
-	deduplicate();
-	compute_file_offsets();
+		if (skipped_overflow)
+			fprintf(stderr,
+				"lineinfo: warning: %u entries skipped (offset > 4 GiB from _text)\n",
+				skipped_overflow);
 
-	fprintf(stderr, "lineinfo: %u entries, %u files\n",
-		num_entries, num_files);
+		deduplicate(NULL, 0);
+		compute_file_offsets();
 
-	output_assembly();
+		fprintf(stderr, "lineinfo: %u entries, %u files\n",
+			num_entries, num_files);
+
+		output_assembly();
+	}
 
 	dwarf_end(dwarf);
 	elf_end(elf);
@@ -552,6 +1251,5 @@ int main(int argc, char *argv[])
 	for (unsigned int i = 0; i < num_files; i++)
 		free(files[i].name);
 	free(files);
-
 	return 0;
 }
-- 
2.53.0


  parent reply	other threads:[~2026-05-04 15:34 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-04 15:33 [PATCH v5 0/4] kallsyms: embed source file:line info in kernel stack traces Sasha Levin
2026-05-04 15:33 ` [PATCH v5 1/4] " Sasha Levin
2026-05-04 15:33 ` Sasha Levin [this message]
2026-05-04 15:33 ` [PATCH v5 3/4] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction Sasha Levin
2026-05-04 15:34 ` [PATCH v5 4/4] kallsyms: add KUnit tests for lineinfo feature Sasha Levin

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260504153401.2416391-3-sashal@kernel.org \
    --to=sashal@kernel.org \
    --cc=James.Bottomley@HansenPartnership.com \
    --cc=akpm@linux-foundation.org \
    --cc=corbet@lwn.net \
    --cc=da.gomez@kernel.org \
    --cc=deller@gmx.de \
    --cc=geert@linux-m68k.org \
    --cc=gregkh@linuxfoundation.org \
    --cc=jgross@suse.com \
    --cc=kees@kernel.org \
    --cc=laurent.pinchart@ideasonboard.com \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kbuild@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-modules@vger.kernel.org \
    --cc=linux@leemhuis.info \
    --cc=masahiroy@kernel.org \
    --cc=mcgrof@kernel.org \
    --cc=nathan@kernel.org \
    --cc=nsc@kernel.org \
    --cc=peterz@infradead.org \
    --cc=petr.pavlu@suse.com \
    --cc=pmladek@suse.com \
    --cc=rdunlap@infradead.org \
    --cc=richard@nod.at \
    --cc=rostedt@goodmis.org \
    --cc=samitolvanen@google.com \
    --cc=thunder.leizhen@huawei.com \
    --cc=torvalds@linux-foundation.org \
    --cc=vbabka@kernel.org \
    --cc=wangruikang@iscas.ac.cn \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox