[PATCH v5 2/4] kallsyms: extend lineinfo to loadable modules

Sasha Levin
2026-05-04T15:34:55Z

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