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

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

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

The gen_lineinfo tool gains --module mode which:
 - Uses .text section address as base (ET_REL files have no _text symbol)
 - Filters entries to .text-only (excludes .init.text/.exit.text)
 - Handles libdw's ET_REL path-doubling quirk in make_relative()
 - Outputs a flat binary-format section instead of named global symbols

Per-module overhead is approximately 10 bytes per DWARF line entry.

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

diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst 
b/Documentation/admin-guide/kallsyms-lineinfo.rst
index c8ec124394354..5cae995eb118e 100644
--- a/Documentation/admin-guide/kallsyms-lineinfo.rst
+++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
@@ -51,22 +51,46 @@ With ``CONFIG_KALLSYMS_LINEINFO``::
 Note that assembly routines (such as ``entry_SYSCALL_64_after_hwframe``) are
 not annotated because they lack DWARF debug information.
 
+Module Support
+==============
+
+``CONFIG_KALLSYMS_LINEINFO_MODULES`` extends the feature to loadable kernel
+modules.  When enabled, each ``.ko`` is post-processed at build time to embed
+a ``.mod_lineinfo`` section containing the same kind of address-to-source
+mapping.
+
+Enable in addition to the base options::
+
+    CONFIG_MODULES=y
+    CONFIG_KALLSYMS_LINEINFO_MODULES=y
+
+Stack traces from module code will then include annotations::
+
+    my_driver_func+0x30/0x100 [my_driver] (drivers/foo/bar.c:123)
+
+The ``.mod_lineinfo`` section is loaded into read-only module memory alongside
+the module text.  No additional runtime memory allocation is required; the data
+is freed when the module is unloaded.
+
 Memory Overhead
 ===============
 
-The lineinfo tables are stored in ``.rodata`` and typically add approximately
-44 MiB to the kernel image for a standard configuration (~4.6 million DWARF
-line entries, ~10 bytes per entry after deduplication).
+The vmlinux lineinfo tables are stored in ``.rodata`` and typically add
+approximately 44 MiB to the kernel image for a standard configuration
+(~4.6 million DWARF line entries, ~10 bytes per entry after deduplication).
+
+Per-module lineinfo adds approximately 10 bytes per DWARF line entry to each
+``.ko`` file.
 
 Known Limitations
 =================
 
-- **vmlinux only**: Only symbols in the core kernel image are annotated.
-  Module symbols are not covered.
-- **4 GiB offset limit**: Address offsets from ``_text`` are stored as 32-bit
-  values.  Entries beyond 4 GiB from ``_text`` are skipped at build time with
-  a warning.
+- **4 GiB offset limit**: Address offsets from ``_text`` (vmlinux) or
+  ``.text`` base (modules) are stored as 32-bit values.  Entries beyond
+  4 GiB are skipped at build time with a warning.
 - **65535 file limit**: Source file IDs are stored as 16-bit values.  Builds
   with more than 65535 unique source files will fail with an error.
 - **No assembly annotations**: Functions implemented in assembly that lack
   DWARF ``.debug_line`` data are not annotated.
+- **No init text**: For modules, functions in ``.init.text`` are not annotated
+  because that memory is freed after module initialization.
diff --git a/MAINTAINERS b/MAINTAINERS
index f061e69b6e32a..535e992ca5a20 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13732,6 +13732,8 @@ KALLSYMS LINEINFO
 M:     Sasha Levin <[email protected]>
 S:     Maintained
 F:     Documentation/admin-guide/kallsyms-lineinfo.rst
+F:     include/linux/mod_lineinfo.h
+F:     scripts/gen-mod-lineinfo.sh
 F:     scripts/gen_lineinfo.c
 
 KASAN
diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h
new file mode 100644
index 0000000000000..d62e9608f0f82
--- /dev/null
+++ b/include/linux/mod_lineinfo.h
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * mod_lineinfo.h - Binary format for per-module source line information
+ *
+ * This header defines the layout of the .mod_lineinfo section embedded
+ * in loadable kernel modules.  It is dual-use: included from both the
+ * kernel and the userspace gen_lineinfo tool.
+ *
+ * Section layout (all values in target-native endianness):
+ *
+ *   struct mod_lineinfo_header     (16 bytes)
+ *   u32 addrs[num_entries]         -- offsets from .text base, sorted
+ *   u16 file_ids[num_entries]      -- parallel to addrs
+ *   <2-byte pad if num_entries is odd>
+ *   u32 lines[num_entries]         -- parallel to addrs
+ *   u32 file_offsets[num_files]    -- byte offset into filenames[]
+ *   char filenames[filenames_size] -- concatenated NUL-terminated strings
+ */
+#ifndef _LINUX_MOD_LINEINFO_H
+#define _LINUX_MOD_LINEINFO_H
+
+#ifdef __KERNEL__
+#include <linux/types.h>
+#else
+#include <stdint.h>
+typedef uint32_t u32;
+typedef uint16_t u16;
+#endif
+
+struct mod_lineinfo_header {
+       u32 num_entries;
+       u32 num_files;
+       u32 filenames_size;     /* total bytes of concatenated filenames */
+       u32 reserved;           /* padding, must be 0 */
+};
+
+/* Offset helpers: compute byte offset from start of section to each array */
+
+static inline u32 mod_lineinfo_addrs_off(void)
+{
+       return sizeof(struct mod_lineinfo_header);
+}
+
+static inline u32 mod_lineinfo_file_ids_off(u32 num_entries)
+{
+       return mod_lineinfo_addrs_off() + num_entries * sizeof(u32);
+}
+
+static inline u32 mod_lineinfo_lines_off(u32 num_entries)
+{
+       /* u16 file_ids[] may need 2-byte padding to align lines[] to 4 bytes */
+       u32 off = mod_lineinfo_file_ids_off(num_entries) +
+                 num_entries * sizeof(u16);
+       return (off + 3) & ~3u;
+}
+
+static inline u32 mod_lineinfo_file_offsets_off(u32 num_entries)
+{
+       return mod_lineinfo_lines_off(num_entries) + num_entries * sizeof(u32);
+}
+
+static inline u32 mod_lineinfo_filenames_off(u32 num_entries, u32 num_files)
+{
+       return mod_lineinfo_file_offsets_off(num_entries) +
+              num_files * sizeof(u32);
+}
+
+#endif /* _LINUX_MOD_LINEINFO_H */
diff --git a/include/linux/module.h b/include/linux/module.h
index 14f391b186c6d..1c5840e736ec7 100644
--- a/include/linux/module.h
+++ b/include/linux/module.h
@@ -508,6 +508,10 @@ struct module {
        void *btf_data;
        void *btf_base_data;
 #endif
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+       void *lineinfo_data;            /* .mod_lineinfo section in MOD_RODATA 
*/
+       unsigned int lineinfo_data_size;
+#endif
 #ifdef CONFIG_JUMP_LABEL
        struct jump_entry *jump_entries;
        unsigned int num_jump_entries;
@@ -1021,6 +1025,21 @@ static inline unsigned long 
find_kallsyms_symbol_value(struct module *mod,
 
 #endif  /* CONFIG_MODULES && CONFIG_KALLSYMS */
 
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
+                           unsigned long sym_start,
+                           const char **file, unsigned int *line);
+#else
+static inline bool module_lookup_lineinfo(struct module *mod,
+                                         unsigned long addr,
+                                         unsigned long sym_start,
+                                         const char **file,
+                                         unsigned int *line)
+{
+       return false;
+}
+#endif
+
 /* Define __free(module_put) macro for struct module *. */
 DEFINE_FREE(module_put, struct module *, if (_T) module_put(_T))
 
diff --git a/init/Kconfig b/init/Kconfig
index c39f27e6393a8..bf53275bc405a 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2070,6 +2070,19 @@ config KALLSYMS_LINEINFO
 
          If unsure, say N.
 
+config KALLSYMS_LINEINFO_MODULES
+       bool "Embed source file:line information in module stack traces"
+       depends on KALLSYMS_LINEINFO && MODULES
+       help
+         Extends KALLSYMS_LINEINFO to loadable kernel modules.  Each .ko
+         gets a lineinfo table generated from its DWARF data at build time,
+         so stack traces from module code include (file.c:123) annotations.
+
+         Requires elfutils (libdw-dev/elfutils-devel) on the build host.
+         Increases .ko sizes by approximately 10 bytes per DWARF line entry.
+
+         If unsure, say N.
+
 # end of the "standard kernel features (expert users)" menu
 
 config ARCH_HAS_MEMBARRIER_CALLBACKS
diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
index c94d8f332c5df..4ae0e9501da1a 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -546,13 +546,27 @@ static int __sprint_symbol(char *buffer, unsigned long 
address,
        }
 
 #ifdef CONFIG_KALLSYMS_LINEINFO
-       if (!modname) {
+       {
                const char *li_file;
                unsigned int li_line;
                unsigned long sym_start = address - offset;
-
-               if (kallsyms_lookup_lineinfo(address, sym_start,
-                                            &li_file, &li_line))
+               bool found = false;
+
+               if (!modname)
+                       found = kallsyms_lookup_lineinfo(address, sym_start,
+                                                        &li_file, &li_line);
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+               else {
+                       struct module *mod = __module_address(address);
+
+                       if (mod)
+                               found = module_lookup_lineinfo(mod, address,
+                                                             sym_start,
+                                                             &li_file,
+                                                             &li_line);
+               }
+#endif
+               if (found)
                        len += snprintf(buffer + len, KSYM_SYMBOL_LEN - len,
                                        " (%s:%u)", li_file, li_line);
        }
diff --git a/kernel/module/kallsyms.c b/kernel/module/kallsyms.c
index 0fc11e45df9b9..4bfb6707fb8a5 100644
--- a/kernel/module/kallsyms.c
+++ b/kernel/module/kallsyms.c
@@ -494,3 +494,94 @@ int module_kallsyms_on_each_symbol(const char *modname,
        mutex_unlock(&module_mutex);
        return ret;
 }
+
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+#include <linux/mod_lineinfo.h>
+
+/*
+ * Look up source file:line for an address within a loaded module.
+ * Uses the .mod_lineinfo section embedded in the .ko at build time.
+ *
+ * Safe in NMI/panic context: no locks, no allocations.
+ * Caller must hold RCU read lock (or be in a context where the module
+ * cannot be unloaded).
+ */
+bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
+                           unsigned long sym_start,
+                           const char **file, unsigned int *line)
+{
+       const struct mod_lineinfo_header *hdr;
+       const void *base;
+       const u32 *addrs, *lines, *file_offsets;
+       const u16 *file_ids;
+       const char *filenames;
+       u32 num_entries, num_files, filenames_size;
+       unsigned long text_base;
+       unsigned int offset;
+       unsigned long long raw_offset;
+       unsigned int low, high, mid;
+       u16 file_id;
+
+       base = mod->lineinfo_data;
+       if (!base)
+               return false;
+
+       if (mod->lineinfo_data_size < sizeof(*hdr))
+               return false;
+
+       hdr = base;
+       num_entries = hdr->num_entries;
+       num_files = hdr->num_files;
+       filenames_size = hdr->filenames_size;
+
+       if (num_entries == 0)
+               return false;
+
+       /* Validate section is large enough for all arrays */
+       if (mod->lineinfo_data_size <
+           mod_lineinfo_filenames_off(num_entries, num_files) + filenames_size)
+               return false;
+
+       addrs = base + mod_lineinfo_addrs_off();
+       file_ids = base + mod_lineinfo_file_ids_off(num_entries);
+       lines = base + mod_lineinfo_lines_off(num_entries);
+       file_offsets = base + mod_lineinfo_file_offsets_off(num_entries);
+       filenames = base + mod_lineinfo_filenames_off(num_entries, num_files);
+
+       /* Compute offset from module .text base */
+       text_base = (unsigned long)mod->mem[MOD_TEXT].base;
+       if (addr < text_base)
+               return false;
+
+       raw_offset = addr - text_base;
+       if (raw_offset > UINT_MAX)
+               return false;
+       offset = (unsigned int)raw_offset;
+
+       /* Binary search for largest entry <= offset */
+       low = 0;
+       high = num_entries;
+       while (low < high) {
+               mid = low + (high - low) / 2;
+               if (addrs[mid] <= offset)
+                       low = mid + 1;
+               else
+                       high = mid;
+       }
+
+       if (low == 0)
+               return false;
+       low--;
+
+       file_id = file_ids[low];
+       if (file_id >= num_files)
+               return false;
+
+       if (file_offsets[file_id] >= filenames_size)
+               return false;
+
+       *file = &filenames[file_offsets[file_id]];
+       *line = lines[low];
+       return true;
+}
+#endif /* CONFIG_KALLSYMS_LINEINFO_MODULES */
diff --git a/kernel/module/main.c b/kernel/module/main.c
index 2bac4c7cd019a..7b6ff9f7411b0 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -2648,6 +2648,10 @@ static int find_module_sections(struct module *mod, 
struct load_info *info)
        mod->btf_base_data = any_section_objs(info, ".BTF.base", 1,
                                              &mod->btf_base_data_size);
 #endif
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+       mod->lineinfo_data = any_section_objs(info, ".mod_lineinfo", 1,
+                                             &mod->lineinfo_data_size);
+#endif
 #ifdef CONFIG_JUMP_LABEL
        mod->jump_entries = section_objs(info, "__jump_table",
                                        sizeof(*mod->jump_entries),
diff --git a/scripts/Makefile b/scripts/Makefile
index ffe89875b3295..651df2a867ffb 100644
--- a/scripts/Makefile
+++ b/scripts/Makefile
@@ -5,6 +5,7 @@
 
 hostprogs-always-$(CONFIG_KALLSYMS)                    += kallsyms
 hostprogs-always-$(CONFIG_KALLSYMS_LINEINFO)           += gen_lineinfo
+hostprogs-always-$(CONFIG_KALLSYMS_LINEINFO_MODULES)   += gen_lineinfo
 hostprogs-always-$(BUILD_C_RECORDMCOUNT)               += recordmcount
 hostprogs-always-$(CONFIG_BUILDTIME_TABLE_SORT)                += sorttable
 hostprogs-always-$(CONFIG_ASN1)                                += asn1_compiler
diff --git a/scripts/Makefile.modfinal b/scripts/Makefile.modfinal
index adcbcde16a071..3941cf624526b 100644
--- a/scripts/Makefile.modfinal
+++ b/scripts/Makefile.modfinal
@@ -46,6 +46,9 @@ quiet_cmd_btf_ko = BTF [M] $@
                $(CONFIG_SHELL) $(srctree)/scripts/gen-btf.sh --btf_base 
$(objtree)/vmlinux $@; \
        fi;
 
+quiet_cmd_lineinfo_ko = LINEINFO [M] $@
+      cmd_lineinfo_ko = $(CONFIG_SHELL) $(srctree)/scripts/gen-mod-lineinfo.sh 
$@
+
 # Same as newer-prereqs, but allows to exclude specified extra dependencies
 newer_prereqs_except = $(filter-out $(PHONY) $(1),$?)
 
@@ -59,6 +62,9 @@ if_changed_except = $(if $(call 
newer_prereqs_except,$(2))$(cmd-check),      \
        +$(call if_changed_except,ld_ko_o,$(objtree)/vmlinux)
 ifdef CONFIG_DEBUG_INFO_BTF_MODULES
        +$(if $(newer-prereqs),$(call cmd,btf_ko))
+endif
+ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+       +$(if $(newer-prereqs),$(call cmd,lineinfo_ko))
 endif
        +$(call cmd,check_tracepoint)
 
diff --git a/scripts/gen-mod-lineinfo.sh b/scripts/gen-mod-lineinfo.sh
new file mode 100755
index 0000000000000..fa8a914b8363b
--- /dev/null
+++ b/scripts/gen-mod-lineinfo.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+#
+# gen-mod-lineinfo.sh - Embed source line info into a kernel module (.ko)
+#
+# Reads DWARF from the .ko, generates a .mod_lineinfo section, and
+# embeds it back into the .ko.  Modeled on scripts/gen-btf.sh.
+
+set -e
+
+if [ $# -ne 1 ]; then
+       echo "Usage: $0 <module.ko>" >&2
+       exit 1
+fi
+
+KO="$1"
+
+cleanup() {
+       rm -f "${KO}.lineinfo.S" "${KO}.lineinfo.o" "${KO}.lineinfo.bin"
+}
+trap cleanup EXIT
+
+case "${KBUILD_VERBOSE}" in
+*1*)
+       set -x
+       ;;
+esac
+
+# Generate assembly from DWARF -- if it fails (no DWARF), silently skip
+if ! ${objtree}/scripts/gen_lineinfo --module "${KO}" > "${KO}.lineinfo.S" 
2>/dev/null; then
+       exit 0
+fi
+
+# Compile assembly to object file
+${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \
+       ${KBUILD_AFLAGS} ${KBUILD_AFLAGS_MODULE} \
+       -c -o "${KO}.lineinfo.o" "${KO}.lineinfo.S"
+
+# Extract raw section content
+${OBJCOPY} -O binary --only-section=.mod_lineinfo \
+       "${KO}.lineinfo.o" "${KO}.lineinfo.bin"
+
+# Embed into the .ko with alloc,readonly flags
+${OBJCOPY} --add-section ".mod_lineinfo=${KO}.lineinfo.bin" \
+       --set-section-flags .mod_lineinfo=alloc,readonly \
+       "${KO}"
+
+exit 0
diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c
index 37d5e84971be4..5ced6897cbbee 100644
--- a/scripts/gen_lineinfo.c
+++ b/scripts/gen_lineinfo.c
@@ -23,8 +23,16 @@
 #include <gelf.h>
 #include <limits.h>
 
+#include "../include/linux/mod_lineinfo.h"
+
+static int module_mode;
+
 static unsigned int skipped_overflow;
 
+/* .text range for module mode (keep only runtime code) */
+static unsigned long long text_section_start;
+static unsigned long long text_section_end;
+
 struct line_entry {
        unsigned int offset;    /* offset from _text */
        unsigned int file_id;
@@ -148,27 +156,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;
+                       }
                }
 
                /*
@@ -194,9 +200,45 @@ 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);
+
+               for (p = path; (p = strchr(p, '/')) != NULL; p++) {
+                       size_t prefix = p - path;
+                       size_t rest = len - prefix - 1;
+
+                       if (rest == prefix && !memcmp(path, p + 1, prefix))
+                               return p + 1;
+               }
+       }
+
+       /*
+        * 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)
@@ -248,6 +290,159 @@ static unsigned long long find_text_addr(Elf *elf)
        exit(1);
 }
 
+static void find_text_section_range(Elf *elf)
+{
+       Elf_Scn *scn = NULL;
+       GElf_Shdr shdr;
+       size_t shstrndx;
+
+       if (elf_getshdrstrndx(elf, &shstrndx) != 0)
+               return;
+
+       while ((scn = elf_nextscn(elf, scn)) != NULL) {
+               const char *name;
+
+               if (!gelf_getshdr(scn, &shdr))
+                       continue;
+               name = elf_strptr(elf, shstrndx, shdr.sh_name);
+               if (name && !strcmp(name, ".text")) {
+                       text_section_start = shdr.sh_addr;
+                       text_section_end = shdr.sh_addr + shdr.sh_size;
+                       return;
+               }
+       }
+}
+
+/*
+ * 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 10;      /* R_X86_64_32 */
+       case EM_386:            return 1;       /* R_386_32 */
+       case EM_AARCH64:        return 258;     /* R_AARCH64_ABS32 */
+       case EM_ARM:            return 2;       /* R_ARM_ABS32 */
+       case EM_RISCV:          return 1;       /* R_RISCV_32 */
+       case EM_S390:           return 4;       /* R_390_32 */
+       case EM_MIPS:           return 2;       /* R_MIPS_32 */
+       case EM_PPC64:          return 1;       /* R_PPC64_ADDR32 */
+       case EM_PPC:            return 1;       /* R_PPC_ADDR32 */
+       case EM_LOONGARCH:      return 1;       /* R_LARCH_32 */
+       case EM_PARISC:         return 1;       /* R_PARISC_DIR32 */
+       default:                return 0;
+       }
+}
+
+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;
+       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);
+       if (!abs32_type)
+               return;
+
+       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;
+               uint32_t value;
+
+               if (!gelf_getrela(rela_data, i, &rela))
+                       continue;
+
+               r_type = GELF_R_TYPE(rela.r_info);
+               r_sym = GELF_R_SYM(rela.r_info);
+
+               /* Only handle the 32-bit absolute reloc for this arch */
+               if (r_type != abs32_type)
+                       continue;
+
+               if (!gelf_getsym(sym_data, r_sym, &sym))
+                       continue;
+
+               /* Relocated value = sym.st_value + addend */
+               value = (uint32_t)(sym.st_value + rela.r_addend);
+
+               /* Patch the .debug_line data at the relocation offset */
+               if (rela.r_offset + 4 <= dl_data->d_size)
+                       memcpy((char *)dl_data->d_buf + rela.r_offset,
+                              &value, sizeof(value));
+       }
+}
+
 static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
 {
        Dwarf_Off off = 0, next_off;
@@ -295,6 +490,16 @@ static void process_dwarf(Dwarf *dwarf, unsigned long long 
text_addr)
                        if (addr < text_addr)
                                continue;
 
+                       /*
+                        * In module mode, keep only .text addresses.
+                        * In ET_REL .ko files, .init.text/.exit.text may
+                        * overlap with .text address ranges, so we must
+                        * explicitly check against the .text bounds.
+                        */
+                       if (module_mode && text_section_end > 
text_section_start &&
+                           (addr < text_section_start || addr >= 
text_section_end))
+                               continue;
+
                        {
                                unsigned long long raw_offset = addr - 
text_addr;
 
@@ -440,6 +645,63 @@ static void output_assembly(void)
        printf("\n");
 }
 
+static void output_module_assembly(void)
+{
+       unsigned int filenames_size = 0;
+
+       for (unsigned int i = 0; i < num_files; i++)
+               filenames_size += strlen(files[i].name) + 1;
+
+       printf("/* SPDX-License-Identifier: GPL-2.0 */\n");
+       printf("/*\n");
+       printf(" * Automatically generated by scripts/gen_lineinfo --module\n");
+       printf(" * Do not edit.\n");
+       printf(" */\n\n");
+
+       printf("\t.section .mod_lineinfo, \"a\"\n\n");
+
+       /* Header: num_entries, num_files, filenames_size, reserved */
+       printf("\t.balign 4\n");
+       printf("\t.long %u\n", num_entries);
+       printf("\t.long %u\n", num_files);
+       printf("\t.long %u\n", filenames_size);
+       printf("\t.long 0\n\n");
+
+       /* addrs[] */
+       for (unsigned int i = 0; i < num_entries; i++)
+               printf("\t.long 0x%x\n", entries[i].offset);
+       if (num_entries)
+               printf("\n");
+
+       /* file_ids[] */
+       for (unsigned int i = 0; i < num_entries; i++)
+               printf("\t.short %u\n", entries[i].file_id);
+
+       /* Padding to align lines[] to 4 bytes */
+       if (num_entries & 1)
+               printf("\t.short 0\n");
+       if (num_entries)
+               printf("\n");
+
+       /* lines[] */
+       for (unsigned int i = 0; i < num_entries; i++)
+               printf("\t.long %u\n", entries[i].line);
+       if (num_entries)
+               printf("\n");
+
+       /* file_offsets[] */
+       for (unsigned int i = 0; i < num_files; i++)
+               printf("\t.long %u\n", files[i].str_offset);
+       if (num_files)
+               printf("\n");
+
+       /* filenames[] */
+       for (unsigned int i = 0; i < num_files; i++)
+               print_escaped_asciz(files[i].name);
+       if (num_files)
+               printf("\n");
+}
+
 int main(int argc, char *argv[])
 {
        int fd;
@@ -447,12 +709,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));
@@ -460,7 +733,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()));
@@ -468,7 +741,22 @@ int main(int argc, char *argv[])
                return 1;
        }
 
-       text_addr = find_text_addr(elf);
+       if (module_mode) {
+               /*
+                * .ko files are ET_REL after ld -r.  libdw does NOT apply
+                * relocations for ET_REL files, so DW_FORM_line_strp
+                * references in .debug_line are not resolved.  Apply them
+                * ourselves so that dwarf_linesrc() returns correct paths.
+                *
+                * DWARF addresses include the .text sh_addr.  Use .text
+                * sh_addr as the base so offsets are .text-relative.
+                */
+               apply_debug_line_relocations(elf);
+               find_text_section_range(elf);
+               text_addr = text_section_start;
+       } else {
+               text_addr = find_text_addr(elf);
+       }
 
        dwarf = dwarf_begin_elf(elf, DWARF_C_READ, NULL);
        if (!dwarf) {
@@ -494,7 +782,10 @@ int main(int argc, char *argv[])
        fprintf(stderr, "lineinfo: %u entries, %u files\n",
                num_entries, num_files);
 
-       output_assembly();
+       if (module_mode)
+               output_module_assembly();
+       else
+               output_assembly();
 
        dwarf_end(dwarf);
        elf_end(elf);
-- 
2.51.0


Reply via email to