From nobody Fri Apr 3 22:31:38 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 206C032BF5C; Sun, 22 Mar 2026 13:15:50 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774185350; cv=none; b=FFcjePt8xIrwej45u8maYaQNrpkgcLX7bpG+3OHHEyQaEAAbzhECCNLzySMUxikiiRUagEeeMLRD8CbtsKJzVAJ2LFex83RyvqXiqVtMMtaXdm/b9OYfx+P1/7Mv1vadhZM8OAcbkqxbtOKuu8wB35YgO6bsYbFBDJP4UcKDcu4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774185350; c=relaxed/simple; bh=3FialINUKDohLNJeK84Tlbk6Rp4l4wCrpeL1cH98OvU=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=ZFTcZb5vqmSwNKwtcUA5tl8AIgn/ShOQLYXjk0UIGrgEcHRphuTtsRex/FN/v240BwzriIgcz41iAzvf4I3n86rdtGZepc8dfcMREqIrXX+81i6ET0gLklN8Qq6IO4YZ97XW3REl5YmKp6nOqfwufCJNgQy02WLRDR9RmUKcdxs= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=vIpni1dw; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="vIpni1dw" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 5C0EAC2BCB2; Sun, 22 Mar 2026 13:15:47 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1774185349; bh=3FialINUKDohLNJeK84Tlbk6Rp4l4wCrpeL1cH98OvU=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=vIpni1dwcW7NfyzR3bsjJou+zZmxV1j9IienwpWXKFRVVzxHM0aRK8JlzOqoNDWTA 6o86xDPSnhH3ZAXaK+bmkzYgtfnWOKGBwY/LJeIlwOxVlKOmAxZXlVjrGPKFQgzoJH pLxMwsQ4kBzUcReo+NMNhvD/vWDTTfstS7B2R/4TjfCxjTEJfdVVK5l9ZmFDb6MU+m iUM0eMZEY5xqIQ1mbs9hq7mQQVZ6W6apbyhMfTTMpEjF+kL2L+3G8/Dk12jg6kVp2x VICfvOacYio1/8HPmOYo1065VK7JKdVppd0oePo3w5hfI+SMblkndXUsoy4XWX64hu jUFTd0oH57oCg== From: Sasha Levin To: Andrew Morton , Masahiro Yamada , Luis Chamberlain , Linus Torvalds , Richard Weinberger , Juergen Gross , Geert Uytterhoeven , James Bottomley Cc: Jonathan Corbet , Nathan Chancellor , Nicolas Schier , Petr Pavlu , Daniel Gomez , Greg KH , Petr Mladek , Steven Rostedt , Kees Cook , Peter Zijlstra , Thorsten Leemhuis , Vlastimil Babka , Helge Deller , Randy Dunlap , Laurent Pinchart , Vivian Wang , linux-kernel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-modules@vger.kernel.org, linux-doc@vger.kernel.org, Sasha Levin Subject: [PATCH v4 1/4] kallsyms: embed source file:line info in kernel stack traces Date: Sun, 22 Mar 2026 09:15:40 -0400 Message-ID: <20260322131543.971079-2-sashal@kernel.org> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260322131543.971079-1-sashal@kernel.org> References: <20260322131543.971079-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Add CONFIG_KALLSYMS_LINEINFO, which embeds a compact address-to-line lookup table in the kernel image so stack traces directly print source file and line number information: root@localhost:~# echo c > /proc/sysrq-trigger [ 11.201987] sysrq: Trigger a crash [ 11.202831] Kernel panic - not syncing: sysrq triggered crash [ 11.206218] Call Trace: [ 11.206501] [ 11.206749] dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:94) [ 11.207403] vpanic+0x36e/0x620 (kernel/panic.c:650) [ 11.208565] ? __lock_acquire+0x465/0x2240 (kernel/locking/lockdep.c:4= 674) [ 11.209324] panic+0xc9/0xd0 (kernel/panic.c:787) [ 11.211873] ? find_held_lock+0x2b/0x80 (kernel/locking/lockdep.c:5350) [ 11.212597] ? lock_release+0xd3/0x300 (kernel/locking/lockdep.c:5535) [ 11.213312] sysrq_handle_crash+0x1a/0x20 (drivers/tty/sysrq.c:154) [ 11.214005] __handle_sysrq.cold+0x66/0x256 (drivers/tty/sysrq.c:611) [ 11.214712] write_sysrq_trigger+0x65/0x80 (drivers/tty/sysrq.c:1221) [ 11.215424] proc_reg_write+0x1bd/0x3c0 (fs/proc/inode.c:330) [ 11.216061] vfs_write+0x1c6/0xff0 (fs/read_write.c:686) [ 11.218848] ksys_write+0xfa/0x200 (fs/read_write.c:740) [ 11.222394] do_syscall_64+0xf3/0x690 (arch/x86/entry/syscall_64.c:63) [ 11.223942] entry_SYSCALL_64_after_hwframe+0x77/0x7f (arch/x86/entry/= entry_64.S:121) At build time, a new host tool (scripts/gen_lineinfo) reads DWARF .debug_line from vmlinux using libdw (elfutils), extracts all address-to-file:line mappings, and generates an assembly file with sorted parallel arrays (offsets from _text, file IDs, and line numbers). These are linked into vmlinux as .rodata. At runtime, kallsyms_lookup_lineinfo() does a binary search on the table and __sprint_symbol() appends "(file:line)" to each stack frame. The lookup uses offsets from _text so it works with KASLR, requires no locks or allocations, and is safe in any context including panic. The feature requires CONFIG_DEBUG_INFO (for DWARF data) and elfutils (libdw-dev) on the build host. Memory footprint measured with a 1852-option x86_64 config: Table: 4,597,583 entries from 4,841 source files lineinfo_addrs[] 4,597,583 x u32 =3D 17.5 MiB lineinfo_file_ids[] 4,597,583 x u16 =3D 8.8 MiB lineinfo_lines[] 4,597,583 x u32 =3D 17.5 MiB file_offsets + filenames ~ 0.1 MiB Total .rodata increase: ~ 44.0 MiB vmlinux (stripped): 529 MiB -> 573 MiB (+44 MiB / +8.3%) Note: this probably won't be something we roll into "production", but it might be useful for the average user given the relatively low memory footprint, in canary deployments for hyperscalers, or by default for folks who run tests/fuzzing/etc. Disclaimer: this was vibe coded over an afternoon with an AI coding assistant. The .config used for testing is a simple KVM guest configuration for local development and testing. Assisted-by: Claude:claude-opus-4-6 Signed-off-by: Sasha Levin --- Documentation/admin-guide/index.rst | 1 + .../admin-guide/kallsyms-lineinfo.rst | 72 +++ MAINTAINERS | 6 + include/linux/kallsyms.h | 17 +- init/Kconfig | 20 + kernel/kallsyms.c | 56 ++ kernel/kallsyms_internal.h | 9 + scripts/.gitignore | 1 + scripts/Makefile | 3 + scripts/empty_lineinfo.S | 30 ++ scripts/gen_lineinfo.c | 510 ++++++++++++++++++ scripts/kallsyms.c | 16 + scripts/link-vmlinux.sh | 43 +- 13 files changed, 780 insertions(+), 4 deletions(-) create mode 100644 Documentation/admin-guide/kallsyms-lineinfo.rst create mode 100644 scripts/empty_lineinfo.S create mode 100644 scripts/gen_lineinfo.c diff --git a/Documentation/admin-guide/index.rst b/Documentation/admin-guid= e/index.rst index b734f8a2a2c48..1801b9880aeb7 100644 --- a/Documentation/admin-guide/index.rst +++ b/Documentation/admin-guide/index.rst @@ -73,6 +73,7 @@ problems and bugs in particular. ramoops dynamic-debug-howto init + kallsyms-lineinfo kdump/index perf/index pstore-blk diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentatio= n/admin-guide/kallsyms-lineinfo.rst new file mode 100644 index 0000000000000..c8ec124394354 --- /dev/null +++ b/Documentation/admin-guide/kallsyms-lineinfo.rst @@ -0,0 +1,72 @@ +.. SPDX-License-Identifier: GPL-2.0 + +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D +Kallsyms Source Line Info (LINEINFO) +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +Overview +=3D=3D=3D=3D=3D=3D=3D=3D + +``CONFIG_KALLSYMS_LINEINFO`` embeds DWARF-derived source file and line num= ber +mappings into the kernel image so that stack traces include +``(file.c:123)`` annotations next to each symbol. This makes it significa= ntly +easier to pinpoint the exact source location during debugging, without nee= ding +to manually cross-reference addresses with ``addr2line``. + +Enabling the Feature +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +Enable the following kernel configuration options:: + + CONFIG_KALLSYMS=3Dy + CONFIG_DEBUG_INFO=3Dy + CONFIG_KALLSYMS_LINEINFO=3Dy + +Build dependency: the host tool ``scripts/gen_lineinfo`` requires ``libdw`` +from elfutils. Install the development package: + +- Debian/Ubuntu: ``apt install libdw-dev`` +- Fedora/RHEL: ``dnf install elfutils-devel`` +- Arch Linux: ``pacman -S elfutils`` + +Example Output +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +Without ``CONFIG_KALLSYMS_LINEINFO``:: + + Call Trace: + + dump_stack_lvl+0x5d/0x80 + do_syscall_64+0x82/0x190 + entry_SYSCALL_64_after_hwframe+0x76/0x7e + +With ``CONFIG_KALLSYMS_LINEINFO``:: + + Call Trace: + + dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:123) + do_syscall_64+0x82/0x190 (arch/x86/entry/common.c:52) + entry_SYSCALL_64_after_hwframe+0x76/0x7e + +Note that assembly routines (such as ``entry_SYSCALL_64_after_hwframe``) a= re +not annotated because they lack DWARF debug information. + +Memory Overhead +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +The lineinfo tables are stored in ``.rodata`` and typically add approximat= ely +44 MiB to the kernel image for a standard configuration (~4.6 million DWARF +line entries, ~10 bytes per entry after deduplication). + +Known Limitations +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +- **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 w= ith + a warning. +- **65535 file limit**: Source file IDs are stored as 16-bit values. Buil= ds + with more than 65535 unique source files will fail with an error. +- **No assembly annotations**: Functions implemented in assembly that lack + DWARF ``.debug_line`` data are not annotated. diff --git a/MAINTAINERS b/MAINTAINERS index 61bf550fd37c2..f061e69b6e32a 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -13728,6 +13728,12 @@ S: Maintained F: Documentation/hwmon/k8temp.rst F: drivers/hwmon/k8temp.c =20 +KALLSYMS LINEINFO +M: Sasha Levin +S: Maintained +F: Documentation/admin-guide/kallsyms-lineinfo.rst +F: scripts/gen_lineinfo.c + KASAN M: Andrey Ryabinin R: Alexander Potapenko diff --git a/include/linux/kallsyms.h b/include/linux/kallsyms.h index d5dd54c53ace6..7d4c9dca06c87 100644 --- a/include/linux/kallsyms.h +++ b/include/linux/kallsyms.h @@ -16,10 +16,15 @@ #include =20 #define KSYM_NAME_LEN 512 + +/* Extra space for " (path/to/file.c:12345)" suffix when lineinfo is enabl= ed */ +#define KSYM_LINEINFO_LEN (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) ? 128 : 0) + #define KSYM_SYMBOL_LEN (sizeof("%s+%#lx/%#lx [%s %s]") + \ (KSYM_NAME_LEN - 1) + \ 2*(BITS_PER_LONG*3/10) + (MODULE_NAME_LEN - 1) + \ - (BUILD_ID_SIZE_MAX * 2) + 1) + (BUILD_ID_SIZE_MAX * 2) + 1 + \ + KSYM_LINEINFO_LEN) =20 struct cred; struct module; @@ -96,6 +101,9 @@ extern int sprint_backtrace_build_id(char *buffer, unsig= ned long address); =20 int lookup_symbol_name(unsigned long addr, char *symname); =20 +bool kallsyms_lookup_lineinfo(unsigned long addr, + const char **file, unsigned int *line); + #else /* !CONFIG_KALLSYMS */ =20 static inline unsigned long kallsyms_lookup_name(const char *name) @@ -164,6 +172,13 @@ static inline int kallsyms_on_each_match_symbol(int (*= fn)(void *, unsigned long) { return -EOPNOTSUPP; } + +static inline bool kallsyms_lookup_lineinfo(unsigned long addr, + const char **file, + unsigned int *line) +{ + return false; +} #endif /*CONFIG_KALLSYMS*/ =20 static inline void print_ip_sym(const char *loglvl, unsigned long ip) diff --git a/init/Kconfig b/init/Kconfig index b55deae9256c7..c39f27e6393a8 100644 --- a/init/Kconfig +++ b/init/Kconfig @@ -2050,6 +2050,26 @@ config KALLSYMS_ALL =20 Say N unless you really need all symbols, or kernel live patching. =20 +config KALLSYMS_LINEINFO + bool "Embed source file:line information in stack traces" + depends on KALLSYMS && DEBUG_INFO + help + Embeds an address-to-source-line mapping table in the kernel + image so that stack traces directly include file:line information, + similar to what scripts/decode_stacktrace.sh provides but without + needing external tools or a vmlinux with debug info at runtime. + + When enabled, stack traces will look like: + + kmem_cache_alloc_noprof+0x60/0x630 (mm/slub.c:3456) + anon_vma_clone+0x2ed/0xcf0 (mm/rmap.c:412) + + This requires elfutils (libdw-dev/elfutils-devel) on the build host. + Adds approximately 44MB to a typical kernel image (10 bytes per + DWARF line-table entry, ~4.6M entries for a typical config). + + If unsure, say N. + # end of the "standard kernel features (expert users)" menu =20 config ARCH_HAS_MEMBARRIER_CALLBACKS diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c index aec2f06858afd..d0a9cd9c6dace 100644 --- a/kernel/kallsyms.c +++ b/kernel/kallsyms.c @@ -467,6 +467,52 @@ static int append_buildid(char *buffer, const char *= modname, =20 #endif /* CONFIG_STACKTRACE_BUILD_ID */ =20 +bool kallsyms_lookup_lineinfo(unsigned long addr, + const char **file, unsigned int *line) +{ + unsigned long long raw_offset; + unsigned int offset, low, high, mid, file_id; + + if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) || !lineinfo_num_entries) + return false; + + /* Compute offset from _text */ + if (addr < (unsigned long)_text) + return false; + + raw_offset =3D addr - (unsigned long)_text; + if (raw_offset > UINT_MAX) + return false; + offset =3D (unsigned int)raw_offset; + + /* Binary search for largest entry <=3D offset */ + low =3D 0; + high =3D lineinfo_num_entries; + while (low < high) { + mid =3D low + (high - low) / 2; + if (lineinfo_addrs[mid] <=3D offset) + low =3D mid + 1; + else + high =3D mid; + } + + if (low =3D=3D 0) + return false; + low--; + + file_id =3D lineinfo_file_ids[low]; + *line =3D lineinfo_lines[low]; + + if (file_id >=3D lineinfo_num_files) + return false; + + if (lineinfo_file_offsets[file_id] >=3D lineinfo_filenames_size) + return false; + + *file =3D &lineinfo_filenames[lineinfo_file_offsets[file_id]]; + return true; +} + /* Look up a kernel symbol and return it in a text buffer. */ static int __sprint_symbol(char *buffer, unsigned long address, int symbol_offset, int add_offset, int add_buildid) @@ -497,6 +543,16 @@ static int __sprint_symbol(char *buffer, unsigned long= address, len +=3D sprintf(buffer + len, "]"); } =20 + if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) && !modname) { + const char *li_file; + unsigned int li_line; + + if (kallsyms_lookup_lineinfo(address, + &li_file, &li_line)) + len +=3D snprintf(buffer + len, KSYM_SYMBOL_LEN - len, + " (%s:%u)", li_file, li_line); + } + return len; } =20 diff --git a/kernel/kallsyms_internal.h b/kernel/kallsyms_internal.h index 81a867dbe57d4..d7374ce444d81 100644 --- a/kernel/kallsyms_internal.h +++ b/kernel/kallsyms_internal.h @@ -15,4 +15,13 @@ extern const u16 kallsyms_token_index[]; extern const unsigned int kallsyms_markers[]; extern const u8 kallsyms_seqs_of_names[]; =20 +extern const u32 lineinfo_num_entries; +extern const u32 lineinfo_addrs[]; +extern const u16 lineinfo_file_ids[]; +extern const u32 lineinfo_lines[]; +extern const u32 lineinfo_num_files; +extern const u32 lineinfo_file_offsets[]; +extern const u32 lineinfo_filenames_size; +extern const char lineinfo_filenames[]; + #endif // LINUX_KALLSYMS_INTERNAL_H_ diff --git a/scripts/.gitignore b/scripts/.gitignore index 4215c2208f7e4..e175714c18b61 100644 --- a/scripts/.gitignore +++ b/scripts/.gitignore @@ -1,5 +1,6 @@ # SPDX-License-Identifier: GPL-2.0-only /asn1_compiler +/gen_lineinfo /gen_packed_field_checks /generate_rust_target /insert-sys-cert diff --git a/scripts/Makefile b/scripts/Makefile index 0941e5ce7b575..ffe89875b3295 100644 --- a/scripts/Makefile +++ b/scripts/Makefile @@ -4,6 +4,7 @@ # the kernel for the build process. =20 hostprogs-always-$(CONFIG_KALLSYMS) +=3D kallsyms +hostprogs-always-$(CONFIG_KALLSYMS_LINEINFO) +=3D gen_lineinfo hostprogs-always-$(BUILD_C_RECORDMCOUNT) +=3D recordmcount hostprogs-always-$(CONFIG_BUILDTIME_TABLE_SORT) +=3D sorttable hostprogs-always-$(CONFIG_ASN1) +=3D asn1_compiler @@ -36,6 +37,8 @@ HOSTLDLIBS_sorttable =3D -lpthread HOSTCFLAGS_asn1_compiler.o =3D -I$(srctree)/include HOSTCFLAGS_sign-file.o =3D $(shell $(HOSTPKG_CONFIG) --cflags libcrypto 2>= /dev/null) HOSTLDLIBS_sign-file =3D $(shell $(HOSTPKG_CONFIG) --libs libcrypto 2> /de= v/null || echo -lcrypto) +HOSTCFLAGS_gen_lineinfo.o =3D $(shell $(HOSTPKG_CONFIG) --cflags libdw 2> = /dev/null) +HOSTLDLIBS_gen_lineinfo =3D $(shell $(HOSTPKG_CONFIG) --libs libdw 2> /dev= /null || echo -ldw -lelf -lz) =20 ifdef CONFIG_UNWINDER_ORC ifeq ($(ARCH),x86_64) diff --git a/scripts/empty_lineinfo.S b/scripts/empty_lineinfo.S new file mode 100644 index 0000000000000..e058c41137123 --- /dev/null +++ b/scripts/empty_lineinfo.S @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2026 Sasha Levin + * + * Empty lineinfo stub for the initial vmlinux link. + * The real lineinfo is generated from .tmp_vmlinux1 by gen_lineinfo. + */ + .section .rodata, "a" + .globl lineinfo_num_entries + .balign 4 +lineinfo_num_entries: + .long 0 + .globl lineinfo_num_files + .balign 4 +lineinfo_num_files: + .long 0 + .globl lineinfo_addrs +lineinfo_addrs: + .globl lineinfo_file_ids +lineinfo_file_ids: + .globl lineinfo_lines +lineinfo_lines: + .globl lineinfo_file_offsets +lineinfo_file_offsets: + .globl lineinfo_filenames_size + .balign 4 +lineinfo_filenames_size: + .long 0 + .globl lineinfo_filenames +lineinfo_filenames: diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c new file mode 100644 index 0000000000000..37d5e84971be4 --- /dev/null +++ b/scripts/gen_lineinfo.c @@ -0,0 +1,510 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * gen_lineinfo.c - Generate address-to-source-line lookup tables from DWA= RF + * + * Copyright (C) 2026 Sasha Levin + * + * Reads DWARF .debug_line from a vmlinux ELF file and outputs an assembly + * file containing sorted lookup tables that the kernel uses to annotate + * stack traces with source file:line information. + * + * Requires libdw from elfutils. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static unsigned int skipped_overflow; + +struct line_entry { + unsigned int offset; /* offset from _text */ + unsigned int file_id; + unsigned int line; +}; + +struct file_entry { + char *name; + unsigned int id; + unsigned int str_offset; +}; + +static struct line_entry *entries; +static unsigned int num_entries; +static unsigned int entries_capacity; + +static struct file_entry *files; +static unsigned int num_files; +static unsigned int files_capacity; + +#define FILE_HASH_BITS 13 +#define FILE_HASH_SIZE (1 << FILE_HASH_BITS) + +struct file_hash_entry { + const char *name; + unsigned int id; +}; + +static struct file_hash_entry file_hash[FILE_HASH_SIZE]; + +static unsigned int hash_str(const char *s) +{ + unsigned int h =3D 5381; + + for (; *s; s++) + h =3D h * 33 + (unsigned char)*s; + return h & (FILE_HASH_SIZE - 1); +} + +static void add_entry(unsigned int offset, unsigned int file_id, + unsigned int line) +{ + if (num_entries >=3D entries_capacity) { + entries_capacity =3D entries_capacity ? entries_capacity * 2 : 65536; + entries =3D realloc(entries, entries_capacity * sizeof(*entries)); + if (!entries) { + fprintf(stderr, "out of memory\n"); + exit(1); + } + } + entries[num_entries].offset =3D offset; + entries[num_entries].file_id =3D file_id; + entries[num_entries].line =3D line; + num_entries++; +} + +static unsigned int find_or_add_file(const char *name) +{ + unsigned int h =3D hash_str(name); + + /* Open-addressing lookup with linear probing */ + while (file_hash[h].name) { + if (!strcmp(file_hash[h].name, name)) + return file_hash[h].id; + h =3D (h + 1) & (FILE_HASH_SIZE - 1); + } + + if (num_files >=3D 65535) { + fprintf(stderr, + "gen_lineinfo: too many source files (%u > 65535)\n", + num_files); + exit(1); + } + + if (num_files >=3D files_capacity) { + files_capacity =3D files_capacity ? files_capacity * 2 : 4096; + files =3D realloc(files, files_capacity * sizeof(*files)); + if (!files) { + fprintf(stderr, "out of memory\n"); + exit(1); + } + } + files[num_files].name =3D strdup(name); + files[num_files].id =3D num_files; + + /* Insert into hash table (points to files[] entry) */ + file_hash[h].name =3D files[num_files].name; + file_hash[h].id =3D num_files; + + num_files++; + return num_files - 1; +} + +/* + * Well-known top-level directories in the kernel source tree. + * Used as a fallback to recover relative paths from absolute DWARF paths + * when comp_dir doesn't match (e.g. O=3D out-of-tree builds where comp_dir + * is the build directory but source paths point into the source tree). + */ +static const char * const kernel_dirs[] =3D { + "arch/", "block/", "certs/", "crypto/", "drivers/", "fs/", + "include/", "init/", "io_uring/", "ipc/", "kernel/", "lib/", + "mm/", "net/", "rust/", "samples/", "scripts/", "security/", + "sound/", "tools/", "usr/", "virt/", +}; + +/* + * Strip a filename to a kernel-relative path. + * + * For absolute paths, strip the comp_dir prefix (from DWARF) to get + * a kernel-tree-relative path. When that fails (e.g. O=3D builds where + * comp_dir is the build directory), scan for a well-known kernel + * top-level directory name in the path to recover the relative path. + * Fall back to the basename as a last resort. + * + * For relative paths (common in modules), libdw may produce a bogus + * doubled path like "net/foo/bar.c/net/foo/bar.c" due to ET_REL DWARF + * quirks. Detect and strip such duplicates. + */ +static const char *make_relative(const char *path, const char *comp_dir) +{ + const char *p; + + /* If already relative, use as-is */ + if (path[0] !=3D '/') + return path; + + /* comp_dir from DWARF is the most reliable method */ + if (comp_dir) { + size_t len =3D strlen(comp_dir); + + if (!strncmp(path, comp_dir, len) && path[len] =3D=3D '/') { + const char *rel =3D 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; + } + + /* + * comp_dir prefix didn't help =E2=80=94 either it didn't match + * or it was too specific and left a bare filename. + * Scan for a known kernel top-level directory component + * to find where the relative path starts. This handles + * O=3D builds and arches where comp_dir is a subdirectory. + */ + for (p =3D path + 1; *p; p++) { + if (*(p - 1) =3D=3D '/') { + for (unsigned int i =3D 0; i < sizeof(kernel_dirs) / + sizeof(kernel_dirs[0]); i++) { + if (!strncmp(p, kernel_dirs[i], + strlen(kernel_dirs[i]))) + return p; + } + } + } + + /* Fall back to basename */ + p =3D strrchr(path, '/'); + return p ? p + 1 : path; + } + + /* Fall back to basename */ + p =3D strrchr(path, '/'); + return p ? p + 1 : path; +} + +static int compare_entries(const void *a, const void *b) +{ + const struct line_entry *ea =3D a; + const struct line_entry *eb =3D b; + + if (ea->offset !=3D eb->offset) + return ea->offset < eb->offset ? -1 : 1; + if (ea->file_id !=3D eb->file_id) + return ea->file_id < eb->file_id ? -1 : 1; + if (ea->line !=3D eb->line) + return ea->line < eb->line ? -1 : 1; + return 0; +} + +static unsigned long long find_text_addr(Elf *elf) +{ + size_t nsyms, i; + Elf_Scn *scn =3D NULL; + GElf_Shdr shdr; + + while ((scn =3D elf_nextscn(elf, scn)) !=3D NULL) { + Elf_Data *data; + + if (!gelf_getshdr(scn, &shdr)) + continue; + if (shdr.sh_type !=3D SHT_SYMTAB) + continue; + + data =3D elf_getdata(scn, NULL); + if (!data) + continue; + + nsyms =3D shdr.sh_size / shdr.sh_entsize; + for (i =3D 0; i < nsyms; i++) { + GElf_Sym sym; + const char *name; + + if (!gelf_getsym(data, i, &sym)) + continue; + name =3D elf_strptr(elf, shdr.sh_link, sym.st_name); + if (name && !strcmp(name, "_text")) + return sym.st_value; + } + } + + fprintf(stderr, "Cannot find _text symbol\n"); + exit(1); +} + +static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr) +{ + Dwarf_Off off =3D 0, next_off; + size_t hdr_size; + + while (dwarf_nextcu(dwarf, off, &next_off, &hdr_size, + NULL, NULL, NULL) =3D=3D 0) { + Dwarf_Die cudie; + Dwarf_Lines *lines; + size_t nlines; + Dwarf_Attribute attr; + const char *comp_dir =3D NULL; + + if (!dwarf_offdie(dwarf, off + hdr_size, &cudie)) + goto next; + + if (dwarf_attr(&cudie, DW_AT_comp_dir, &attr)) + comp_dir =3D dwarf_formstring(&attr); + + if (dwarf_getsrclines(&cudie, &lines, &nlines) !=3D 0) + goto next; + + for (size_t i =3D 0; i < nlines; i++) { + Dwarf_Line *line =3D dwarf_onesrcline(lines, i); + Dwarf_Addr addr; + const char *src; + const char *rel; + unsigned int file_id, loffset; + int lineno; + + if (!line) + continue; + + if (dwarf_lineaddr(line, &addr) !=3D 0) + continue; + if (dwarf_lineno(line, &lineno) !=3D 0) + continue; + if (lineno =3D=3D 0) + continue; + + src =3D dwarf_linesrc(line, NULL, NULL); + if (!src) + continue; + + if (addr < text_addr) + continue; + + { + unsigned long long raw_offset =3D addr - text_addr; + + if (raw_offset > UINT_MAX) { + skipped_overflow++; + continue; + } + loffset =3D (unsigned int)raw_offset; + } + + rel =3D make_relative(src, comp_dir); + file_id =3D find_or_add_file(rel); + + add_entry(loffset, file_id, (unsigned int)lineno); + } +next: + off =3D next_off; + } +} + +static void deduplicate(void) +{ + unsigned int i, j; + + if (num_entries < 2) + return; + + /* Sort by offset, then file_id, then line for stability */ + qsort(entries, num_entries, sizeof(*entries), compare_entries); + + /* + * Remove duplicate entries: + * - Same offset: keep first (deterministic from stable sort keys) + * - Same file:line as previous kept entry: redundant for binary + * search -- any address between them resolves to the earlier one + */ + j =3D 0; + for (i =3D 1; i < num_entries; i++) { + if (entries[i].offset =3D=3D entries[j].offset) + continue; + if (entries[i].file_id =3D=3D entries[j].file_id && + entries[i].line =3D=3D entries[j].line) + continue; + j++; + if (j !=3D i) + entries[j] =3D entries[i]; + } + num_entries =3D j + 1; +} + +static void compute_file_offsets(void) +{ + unsigned int offset =3D 0; + + for (unsigned int i =3D 0; i < num_files; i++) { + files[i].str_offset =3D offset; + offset +=3D strlen(files[i].name) + 1; + } +} + +static void print_escaped_asciz(const char *s) +{ + printf("\t.asciz \""); + for (; *s; s++) { + if (*s =3D=3D '"' || *s =3D=3D '\\') + putchar('\\'); + putchar(*s); + } + printf("\"\n"); +} + +static void output_assembly(void) +{ + printf("/* SPDX-License-Identifier: GPL-2.0 */\n"); + printf("/*\n"); + printf(" * Automatically generated by scripts/gen_lineinfo\n"); + printf(" * Do not edit.\n"); + printf(" */\n\n"); + + printf("\t.section .rodata, \"a\"\n\n"); + + /* Number of entries */ + printf("\t.globl lineinfo_num_entries\n"); + printf("\t.balign 4\n"); + printf("lineinfo_num_entries:\n"); + printf("\t.long %u\n\n", num_entries); + + /* Number of files */ + printf("\t.globl lineinfo_num_files\n"); + printf("\t.balign 4\n"); + printf("lineinfo_num_files:\n"); + printf("\t.long %u\n\n", num_files); + + /* Sorted address offsets from _text */ + printf("\t.globl lineinfo_addrs\n"); + printf("\t.balign 4\n"); + printf("lineinfo_addrs:\n"); + for (unsigned int i =3D 0; i < num_entries; i++) + printf("\t.long 0x%x\n", entries[i].offset); + printf("\n"); + + /* File IDs, parallel to addrs (u16 -- supports up to 65535 files) */ + printf("\t.globl lineinfo_file_ids\n"); + printf("\t.balign 2\n"); + printf("lineinfo_file_ids:\n"); + for (unsigned int i =3D 0; i < num_entries; i++) + printf("\t.short %u\n", entries[i].file_id); + printf("\n"); + + /* Line numbers, parallel to addrs */ + printf("\t.globl lineinfo_lines\n"); + printf("\t.balign 4\n"); + printf("lineinfo_lines:\n"); + for (unsigned int i =3D 0; i < num_entries; i++) + printf("\t.long %u\n", entries[i].line); + printf("\n"); + + /* File string offset table */ + printf("\t.globl lineinfo_file_offsets\n"); + printf("\t.balign 4\n"); + printf("lineinfo_file_offsets:\n"); + for (unsigned int i =3D 0; i < num_files; i++) + printf("\t.long %u\n", files[i].str_offset); + printf("\n"); + + /* Filenames size */ + { + unsigned int fsize =3D 0; + + for (unsigned int i =3D 0; i < num_files; i++) + fsize +=3D strlen(files[i].name) + 1; + printf("\t.globl lineinfo_filenames_size\n"); + printf("\t.balign 4\n"); + printf("lineinfo_filenames_size:\n"); + printf("\t.long %u\n\n", fsize); + } + + /* Concatenated NUL-terminated filenames */ + printf("\t.globl lineinfo_filenames\n"); + printf("lineinfo_filenames:\n"); + for (unsigned int i =3D 0; i < num_files; i++) + print_escaped_asciz(files[i].name); + printf("\n"); +} + +int main(int argc, char *argv[]) +{ + int fd; + Elf *elf; + Dwarf *dwarf; + unsigned long long text_addr; + + if (argc !=3D 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + fd =3D open(argv[1], O_RDONLY); + if (fd < 0) { + fprintf(stderr, "Cannot open %s: %s\n", argv[1], + strerror(errno)); + return 1; + } + + elf_version(EV_CURRENT); + elf =3D elf_begin(fd, ELF_C_READ, NULL); + if (!elf) { + fprintf(stderr, "elf_begin failed: %s\n", + elf_errmsg(elf_errno())); + close(fd); + return 1; + } + + text_addr =3D find_text_addr(elf); + + dwarf =3D dwarf_begin_elf(elf, DWARF_C_READ, NULL); + if (!dwarf) { + fprintf(stderr, "dwarf_begin_elf failed: %s\n", + dwarf_errmsg(dwarf_errno())); + fprintf(stderr, "Is %s built with CONFIG_DEBUG_INFO?\n", + argv[1]); + elf_end(elf); + close(fd); + return 1; + } + + process_dwarf(dwarf, text_addr); + + if (skipped_overflow) + fprintf(stderr, + "lineinfo: warning: %u entries skipped (offset > 4 GiB from _text)\n", + skipped_overflow); + + deduplicate(); + compute_file_offsets(); + + fprintf(stderr, "lineinfo: %u entries, %u files\n", + num_entries, num_files); + + output_assembly(); + + dwarf_end(dwarf); + elf_end(elf); + close(fd); + + /* Cleanup */ + free(entries); + for (unsigned int i =3D 0; i < num_files; i++) + free(files[i].name); + free(files); + + return 0; +} diff --git a/scripts/kallsyms.c b/scripts/kallsyms.c index 37d5c095ad22a..42662c4fbc6c9 100644 --- a/scripts/kallsyms.c +++ b/scripts/kallsyms.c @@ -78,6 +78,17 @@ static char *sym_name(const struct sym_entry *s) =20 static bool is_ignored_symbol(const char *name, char type) { + /* Ignore lineinfo symbols for kallsyms pass stability */ + static const char * const lineinfo_syms[] =3D { + "lineinfo_addrs", + "lineinfo_file_ids", + "lineinfo_file_offsets", + "lineinfo_filenames", + "lineinfo_lines", + "lineinfo_num_entries", + "lineinfo_num_files", + }; + if (type =3D=3D 'u' || type =3D=3D 'n') return true; =20 @@ -90,6 +101,11 @@ static bool is_ignored_symbol(const char *name, char ty= pe) return true; } =20 + for (size_t i =3D 0; i < ARRAY_SIZE(lineinfo_syms); i++) { + if (!strcmp(name, lineinfo_syms[i])) + return true; + } + return false; } =20 diff --git a/scripts/link-vmlinux.sh b/scripts/link-vmlinux.sh index f99e196abeea4..39ca44fbb259b 100755 --- a/scripts/link-vmlinux.sh +++ b/scripts/link-vmlinux.sh @@ -103,7 +103,7 @@ vmlinux_link() ${ld} ${ldflags} -o ${output} \ ${wl}--whole-archive ${objs} ${wl}--no-whole-archive \ ${wl}--start-group ${libs} ${wl}--end-group \ - ${kallsymso} ${btf_vmlinux_bin_o} ${arch_vmlinux_o} ${ldlibs} + ${kallsymso} ${lineinfo_o} ${btf_vmlinux_bin_o} ${arch_vmlinux_o} ${ldli= bs} } =20 # Create ${2}.o file with all symbols from the ${1} object file @@ -129,6 +129,26 @@ kallsyms() kallsymso=3D${2}.o } =20 +# Generate lineinfo tables from DWARF debug info in a temporary vmlinux. +# ${1} - temporary vmlinux with debug info +# Output: sets lineinfo_o to the generated .o file +gen_lineinfo() +{ + info LINEINFO .tmp_lineinfo.S + if ! scripts/gen_lineinfo "${1}" > .tmp_lineinfo.S; then + echo >&2 "Failed to generate lineinfo from ${1}" + echo >&2 "Try to disable CONFIG_KALLSYMS_LINEINFO" + exit 1 + fi + + info AS .tmp_lineinfo.o + ${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \ + ${KBUILD_AFLAGS} ${KBUILD_AFLAGS_KERNEL} \ + -c -o .tmp_lineinfo.o .tmp_lineinfo.S + + lineinfo_o=3D.tmp_lineinfo.o +} + # Perform kallsyms for the given temporary vmlinux. sysmap_and_kallsyms() { @@ -155,6 +175,7 @@ sorttable() cleanup() { rm -f .btf.* + rm -f .tmp_lineinfo.* rm -f .tmp_vmlinux.nm-sort rm -f System.map rm -f vmlinux @@ -183,6 +204,7 @@ fi btf_vmlinux_bin_o=3D btfids_vmlinux=3D kallsymso=3D +lineinfo_o=3D strip_debug=3D generate_map=3D =20 @@ -198,10 +220,21 @@ if is_enabled CONFIG_KALLSYMS; then kallsyms .tmp_vmlinux0.syms .tmp_vmlinux0.kallsyms fi =20 +if is_enabled CONFIG_KALLSYMS_LINEINFO; then + # Assemble an empty lineinfo stub for the initial link. + # The real lineinfo is generated from .tmp_vmlinux1 by gen_lineinfo. + ${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \ + ${KBUILD_AFLAGS} ${KBUILD_AFLAGS_KERNEL} \ + -c -o .tmp_lineinfo.o "${srctree}/scripts/empty_lineinfo.S" + lineinfo_o=3D.tmp_lineinfo.o +fi + if is_enabled CONFIG_KALLSYMS || is_enabled CONFIG_DEBUG_INFO_BTF; then =20 - # The kallsyms linking does not need debug symbols, but the BTF does. - if ! is_enabled CONFIG_DEBUG_INFO_BTF; then + # The kallsyms linking does not need debug symbols, but BTF and + # lineinfo generation do. + if ! is_enabled CONFIG_DEBUG_INFO_BTF && + ! is_enabled CONFIG_KALLSYMS_LINEINFO; then strip_debug=3D1 fi =20 @@ -219,6 +252,10 @@ if is_enabled CONFIG_DEBUG_INFO_BTF; then btfids_vmlinux=3D.tmp_vmlinux1.BTF_ids fi =20 +if is_enabled CONFIG_KALLSYMS_LINEINFO; then + gen_lineinfo .tmp_vmlinux1 +fi + if is_enabled CONFIG_KALLSYMS; then =20 # kallsyms support --=20 2.51.0 From nobody Fri Apr 3 22:31:38 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 26A26329C7B; Sun, 22 Mar 2026 13:15:52 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774185353; cv=none; b=XTATlwNLCBCa3a0jGk+vdH4cy0lFLGAG9z2Fp+uZJr/GfzWWRjuqiy8Dc+NTLxWUT1DkpyoXNufpqUPrFIdgpGft8xflWrphhyZfSzLJulSvHm2Aam4/OOirJnHgJhyYNneiXuDeZWMlw9RM4cRCRea+YQ8QTp8LFIW+MoqSGYg= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774185353; c=relaxed/simple; bh=7oT1EuS19T7wBlfGD+irJQGWfB8+AcloZzDoAyXswOc=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=F+kbqavJoGFezq+lg7nDE4gpwAoV9V0wLzQokXzMCmgGl9glbGNhKECT8jyIRIYGngYdbD4yY9UjJNefVxHW/Tr6mB7W4NXSgMiwPbYqXh5XcxffRsjjGb0E2t376PhGhf81L2gqnjXHO+gLN4d53Gbjev6qO7oqwyr2f/NNezk= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=iU8OH00Q; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="iU8OH00Q" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 346A8C2BCB3; Sun, 22 Mar 2026 13:15:50 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1774185352; bh=7oT1EuS19T7wBlfGD+irJQGWfB8+AcloZzDoAyXswOc=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=iU8OH00QfES+pBLs1NWoHbr5fpLOZuumzd/pK+f4F2+TpjeLJCjUBjJqgdrGSFdLg Yc6lBfrZtUvNu9yPgGMm46Qpq7m6E9gpHyu0W720HAeHyClJHVAWxe7qUeQcEmKyBK I0l8YPZxXwFDqKjlBK/wollgdy1JxBaXDtzWyocrS+IcoqAbDPGn80ePlWVnpSp7Eb +xDJBkd95PLaOM6VktwsRgBK629K8TFCFDZIcc4v4SyPnwpLvnPXk0n4uk6fElI3km aC5XvAFcRQcNLG8FM67Zr0sQvek1uBN+dUR2Aol1RJy/cReJpDrRIJrdC1IJz9F6Ql Ao7jGONCxtNkg== From: Sasha Levin To: Andrew Morton , Masahiro Yamada , Luis Chamberlain , Linus Torvalds , Richard Weinberger , Juergen Gross , Geert Uytterhoeven , James Bottomley Cc: Jonathan Corbet , Nathan Chancellor , Nicolas Schier , Petr Pavlu , Daniel Gomez , Greg KH , Petr Mladek , Steven Rostedt , Kees Cook , Peter Zijlstra , Thorsten Leemhuis , Vlastimil Babka , Helge Deller , Randy Dunlap , Laurent Pinchart , Vivian Wang , linux-kernel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-modules@vger.kernel.org, linux-doc@vger.kernel.org, Sasha Levin Subject: [PATCH v4 2/4] kallsyms: extend lineinfo to loadable modules Date: Sun, 22 Mar 2026 09:15:41 -0400 Message-ID: <20260322131543.971079-3-sashal@kernel.org> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260322131543.971079-1-sashal@kernel.org> References: <20260322131543.971079-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable 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) =E2=80=94 the data lives in read-only module mem= ory 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 --- .../admin-guide/kallsyms-lineinfo.rst | 40 +- MAINTAINERS | 2 + include/linux/mod_lineinfo.h | 68 ++++ include/linux/module.h | 5 + init/Kconfig | 13 + kernel/kallsyms.c | 18 +- kernel/module/kallsyms.c | 91 +++++ kernel/module/main.c | 3 + scripts/Makefile.modfinal | 6 + scripts/gen-mod-lineinfo.sh | 48 +++ scripts/gen_lineinfo.c | 347 ++++++++++++++++-- 11 files changed, 601 insertions(+), 40 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/Documentatio= n/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``) a= re not annotated because they lack DWARF debug information. =20 +Module Support +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +``CONFIG_KALLSYMS_LINEINFO_MODULES`` extends the feature to loadable kernel +modules. When enabled, each ``.ko`` is post-processed at build time to em= bed +a ``.mod_lineinfo`` section containing the same kind of address-to-source +mapping. + +Enable in addition to the base options:: + + CONFIG_MODULES=3Dy + CONFIG_KALLSYMS_LINEINFO_MODULES=3Dy + +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 along= side +the module text. No additional runtime memory allocation is required; the= data +is freed when the module is unloaded. + Memory Overhead =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D =20 -The lineinfo tables are stored in ``.rodata`` and typically add approximat= ely -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 ea= ch +``.ko`` file. =20 Known Limitations =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D =20 -- **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 w= ith - 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. Buil= ds 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 annot= ated + 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 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 =20 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 +#else +#include +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 =3D 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_file= s) +{ + 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..d23e0cd9c7210 100644 --- a/include/linux/module.h +++ b/include/linux/module.h @@ -508,6 +508,8 @@ struct module { void *btf_data; void *btf_base_data; #endif + void *lineinfo_data; /* .mod_lineinfo section in MOD_RODATA */ + unsigned int lineinfo_data_size; #ifdef CONFIG_JUMP_LABEL struct jump_entry *jump_entries; unsigned int num_jump_entries; @@ -1021,6 +1023,9 @@ static inline unsigned long find_kallsyms_symbol_valu= e(struct module *mod, =20 #endif /* CONFIG_MODULES && CONFIG_KALLSYMS */ =20 +bool module_lookup_lineinfo(struct module *mod, unsigned long addr, + const char **file, unsigned int *line); + /* Define __free(module_put) macro for struct module *. */ DEFINE_FREE(module_put, struct module *, if (_T) module_put(_T)) =20 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 =20 If unsure, say N. =20 +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 =20 config ARCH_HAS_MEMBARRIER_CALLBACKS diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c index d0a9cd9c6dace..9df92b0fd9041 100644 --- a/kernel/kallsyms.c +++ b/kernel/kallsyms.c @@ -543,12 +543,24 @@ static int __sprint_symbol(char *buffer, unsigned lon= g address, len +=3D sprintf(buffer + len, "]"); } =20 - if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) && !modname) { + if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO)) { const char *li_file; unsigned int li_line; + bool found =3D false; + + if (!modname) + found =3D kallsyms_lookup_lineinfo(address, + &li_file, &li_line); + else if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES)) { + struct module *mod =3D __module_address(address); + + if (mod) + found =3D module_lookup_lineinfo(mod, address, + &li_file, + &li_line); + } =20 - if (kallsyms_lookup_lineinfo(address, - &li_file, &li_line)) + if (found) len +=3D 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..5b46293e957ab 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; } + +#include + +/* + * 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, + 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; + + if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES)) + return false; + + base =3D mod->lineinfo_data; + if (!base) + return false; + + if (mod->lineinfo_data_size < sizeof(*hdr)) + return false; + + hdr =3D base; + num_entries =3D hdr->num_entries; + num_files =3D hdr->num_files; + filenames_size =3D hdr->filenames_size; + + if (num_entries =3D=3D 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 =3D base + mod_lineinfo_addrs_off(); + file_ids =3D base + mod_lineinfo_file_ids_off(num_entries); + lines =3D base + mod_lineinfo_lines_off(num_entries); + file_offsets =3D base + mod_lineinfo_file_offsets_off(num_entries); + filenames =3D base + mod_lineinfo_filenames_off(num_entries, num_files); + + /* Compute offset from module .text base */ + text_base =3D (unsigned long)mod->mem[MOD_TEXT].base; + if (addr < text_base) + return false; + + raw_offset =3D addr - text_base; + if (raw_offset > UINT_MAX) + return false; + offset =3D (unsigned int)raw_offset; + + /* Binary search for largest entry <=3D offset */ + low =3D 0; + high =3D num_entries; + while (low < high) { + mid =3D low + (high - low) / 2; + if (addrs[mid] <=3D offset) + low =3D mid + 1; + else + high =3D mid; + } + + if (low =3D=3D 0) + return false; + low--; + + file_id =3D file_ids[low]; + if (file_id >=3D num_files) + return false; + + if (file_offsets[file_id] >=3D filenames_size) + return false; + + *file =3D &filenames[file_offsets[file_id]]; + *line =3D lines[low]; + return true; +} diff --git a/kernel/module/main.c b/kernel/module/main.c index 2bac4c7cd019a..d11646b02730a 100644 --- a/kernel/module/main.c +++ b/kernel/module/main.c @@ -2648,6 +2648,9 @@ static int find_module_sections(struct module *mod, s= truct load_info *info) mod->btf_base_data =3D any_section_objs(info, ".BTF.base", 1, &mod->btf_base_data_size); #endif + if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES)) + mod->lineinfo_data =3D any_section_objs(info, ".mod_lineinfo", 1, + &mod->lineinfo_data_size); #ifdef CONFIG_JUMP_LABEL mod->jump_entries =3D section_objs(info, "__jump_table", sizeof(*mod->jump_entries), 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 =3D BTF [M] $@ $(CONFIG_SHELL) $(srctree)/scripts/gen-btf.sh --btf_base $(objtree)/vmli= nux $@; \ fi; =20 +quiet_cmd_lineinfo_ko =3D LINEINFO [M] $@ + cmd_lineinfo_ko =3D $(CONFIG_SHELL) $(srctree)/scripts/gen-mod-linei= nfo.sh $@ + # Same as newer-prereqs, but allows to exclude specified extra dependencies newer_prereqs_except =3D $(filter-out $(PHONY) $(1),$?) =20 @@ -59,6 +62,9 @@ if_changed_except =3D $(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) =20 diff --git a/scripts/gen-mod-lineinfo.sh b/scripts/gen-mod-lineinfo.sh new file mode 100755 index 0000000000000..d0663b862d31b --- /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 " >&2 + exit 1 +fi + +KO=3D"$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"= ; 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=3D.mod_lineinfo \ + "${KO}.lineinfo.o" "${KO}.lineinfo.bin" + +# Embed into the .ko with alloc,readonly flags +${OBJCOPY} --add-section ".mod_lineinfo=3D${KO}.lineinfo.bin" \ + --set-section-flags .mod_lineinfo=3Dalloc,readonly \ + "${KO}" + +exit 0 diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c index 37d5e84971be4..7d06701549345 100644 --- a/scripts/gen_lineinfo.c +++ b/scripts/gen_lineinfo.c @@ -23,8 +23,16 @@ #include #include =20 +#include "../include/linux/mod_lineinfo.h" + +static int module_mode; + static unsigned int skipped_overflow; =20 +/* .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, co= nst char *comp_dir) { const char *p; =20 - /* If already relative, use as-is */ - if (path[0] !=3D '/') - return path; - - /* comp_dir from DWARF is the most reliable method */ - if (comp_dir) { - size_t len =3D strlen(comp_dir); - - if (!strncmp(path, comp_dir, len) && path[len] =3D=3D '/') { - const char *rel =3D 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] =3D=3D '/') { + /* Try comp_dir prefix from DWARF */ + if (comp_dir) { + size_t len =3D strlen(comp_dir); + + if (!strncmp(path, comp_dir, len) && path[len] =3D=3D '/') { + const char *rel =3D 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; + } } =20 /* @@ -194,9 +200,42 @@ static const char *make_relative(const char *path, con= st char *comp_dir) return p ? p + 1 : path; } =20 - /* Fall back to basename */ - p =3D strrchr(path, '/'); - return p ? p + 1 : path; + /* + * Relative path =E2=80=94 check for duplicated-path quirk from libdw + * on ET_REL files (e.g., "a/b.c/a/b.c" =E2=86=92 "a/b.c"). + */ + { + size_t len =3D strlen(path); + size_t mid =3D len / 2; + + if (len > 1 && path[mid] =3D=3D '/' && + !memcmp(path, path + mid + 1, mid)) + return path + mid + 1; + } + + /* + * Bare filename with no directory component =E2=80=94 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] =3D=3D '/') { + static char buf[PATH_MAX]; + + snprintf(buf, sizeof(buf), "%s/%s", comp_dir, path); + for (p =3D buf + 1; *p; p++) { + if (*(p - 1) =3D=3D '/') { + for (unsigned int i =3D 0; i < sizeof(kernel_dirs) / + sizeof(kernel_dirs[0]); i++) { + if (!strncmp(p, kernel_dirs[i], + strlen(kernel_dirs[i]))) + return p; + } + } + } + } + + return path; } =20 static int compare_entries(const void *a, const void *b) @@ -248,6 +287,159 @@ static unsigned long long find_text_addr(Elf *elf) exit(1); } =20 +static void find_text_section_range(Elf *elf) +{ + Elf_Scn *scn =3D NULL; + GElf_Shdr shdr; + size_t shstrndx; + + if (elf_getshdrstrndx(elf, &shstrndx) !=3D 0) + return; + + while ((scn =3D elf_nextscn(elf, scn)) !=3D NULL) { + const char *name; + + if (!gelf_getshdr(scn, &shdr)) + continue; + name =3D elf_strptr(elf, shstrndx, shdr.sh_name); + if (name && !strcmp(name, ".text")) { + text_section_start =3D shdr.sh_addr; + text_section_end =3D shdr.sh_addr + shdr.sh_size; + return; + } + } +} + +/* + * Apply .rela.debug_line relocations to a mutable copy of .debug_line dat= a. + * + * 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=3D31447 + * 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; + } +} + +static void apply_debug_line_relocations(Elf *elf) +{ + Elf_Scn *scn =3D NULL; + Elf_Scn *debug_line_scn =3D NULL; + Elf_Scn *rela_debug_line_scn =3D NULL; + Elf_Scn *symtab_scn =3D 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) =3D=3D NULL) + return; + + abs32_type =3D r_type_abs32(ehdr.e_machine); + if (!abs32_type) + return; + + if (elf_getshdrstrndx(elf, &shstrndx) !=3D 0) + return; + + /* Find the relevant sections */ + while ((scn =3D elf_nextscn(elf, scn)) !=3D NULL) { + const char *name; + + if (!gelf_getshdr(scn, &shdr)) + continue; + name =3D elf_strptr(elf, shstrndx, shdr.sh_name); + if (!name) + continue; + + if (!strcmp(name, ".debug_line")) + debug_line_scn =3D scn; + else if (!strcmp(name, ".rela.debug_line")) + rela_debug_line_scn =3D scn; + else if (shdr.sh_type =3D=3D SHT_SYMTAB) + symtab_scn =3D scn; + } + + if (!debug_line_scn || !rela_debug_line_scn || !symtab_scn) + return; + + dl_data =3D elf_getdata(debug_line_scn, NULL); + rela_data =3D elf_getdata(rela_debug_line_scn, NULL); + sym_data =3D 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 =3D rela_shdr.sh_size / rela_shdr.sh_entsize; + + for (i =3D 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 =3D GELF_R_TYPE(rela.r_info); + r_sym =3D GELF_R_SYM(rela.r_info); + + /* Only handle the 32-bit absolute reloc for this arch */ + if (r_type !=3D abs32_type) + continue; + + if (!gelf_getsym(sym_data, r_sym, &sym)) + continue; + + /* Relocated value =3D sym.st_value + addend */ + value =3D (uint32_t)(sym.st_value + rela.r_addend); + + /* Patch the .debug_line data at the relocation offset */ + if (rela.r_offset + 4 <=3D 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 =3D 0, next_off; @@ -295,6 +487,17 @@ static void process_dwarf(Dwarf *dwarf, unsigned long = long text_addr) if (addr < text_addr) continue; =20 + /* + * In module mode, keep only .text addresses. + * In ET_REL .ko files, .text, .init.text and + * .exit.text all have sh_addr =3D=3D 0 and therefore + * overlapping address ranges. Explicitly check + * against the .text bounds. + */ + if (module_mode && text_section_end > text_section_start && + (addr < text_section_start || addr >=3D text_section_end)) + continue; + { unsigned long long raw_offset =3D addr - text_addr; =20 @@ -440,6 +643,63 @@ static void output_assembly(void) printf("\n"); } =20 +static void output_module_assembly(void) +{ + unsigned int filenames_size =3D 0; + + for (unsigned int i =3D 0; i < num_files; i++) + filenames_size +=3D 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 =3D 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 =3D 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 =3D 0; i < num_entries; i++) + printf("\t.long %u\n", entries[i].line); + if (num_entries) + printf("\n"); + + /* file_offsets[] */ + for (unsigned int i =3D 0; i < num_files; i++) + printf("\t.long %u\n", files[i].str_offset); + if (num_files) + printf("\n"); + + /* filenames[] */ + for (unsigned int i =3D 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 +707,23 @@ int main(int argc, char *argv[]) Dwarf *dwarf; unsigned long long text_addr; =20 + if (argc >=3D 2 && !strcmp(argv[1], "--module")) { + module_mode =3D 1; + argv++; + argc--; + } + if (argc !=3D 2) { - fprintf(stderr, "Usage: %s \n", argv[0]); + fprintf(stderr, "Usage: %s [--module] \n", argv[0]); return 1; } =20 - fd =3D 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 =3D 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 +731,7 @@ int main(int argc, char *argv[]) } =20 elf_version(EV_CURRENT); - elf =3D elf_begin(fd, ELF_C_READ, NULL); + elf =3D 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 +739,22 @@ int main(int argc, char *argv[]) return 1; } =20 - text_addr =3D 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 =3D text_section_start; + } else { + text_addr =3D find_text_addr(elf); + } =20 dwarf =3D dwarf_begin_elf(elf, DWARF_C_READ, NULL); if (!dwarf) { @@ -494,7 +780,10 @@ int main(int argc, char *argv[]) fprintf(stderr, "lineinfo: %u entries, %u files\n", num_entries, num_files); =20 - output_assembly(); + if (module_mode) + output_module_assembly(); + else + output_assembly(); =20 dwarf_end(dwarf); elf_end(elf); --=20 2.51.0 From nobody Fri Apr 3 22:31:38 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id F295E32A3DA; Sun, 22 Mar 2026 13:15:55 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774185356; cv=none; b=ulW29LQ5QYPlsz/nC9pmPvX+6E8gjOHrp8x1J1eQh3mQ9j+CqMLB9DmfanyAXiMj3cNvmz/tVwwS+F/y7rOnqQLvVS3/8MMk6g5Ppn/q79MwzEfIRQ/qtj0kc7KFaloGXARNMsBKs8kuJ0c3GBIoMHHZ64hj9IMJ266N5aPmpd0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774185356; c=relaxed/simple; bh=g2/vCyk44FWCnxtM3lvm3ZW6vKUG860F1wsnOhoduBA=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=OUEOzXSaKB3X9246+SH68rGNIC76NHEuUnp5S9uSLW4CCJCVO/7nXebnFBUJIHA6lT6GNIKT6qeGyRqYXiAK4Ap9MCOo2TOkS5Zo9XFXdU9ll1YLVzIb+k1HADgwWngtxza6yju6Qbb1c+lVtApNaHbTxKcJLoXps4Ek6UssudA= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=DlGi65k1; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="DlGi65k1" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 1107CC2BCB5; Sun, 22 Mar 2026 13:15:52 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1774185355; bh=g2/vCyk44FWCnxtM3lvm3ZW6vKUG860F1wsnOhoduBA=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=DlGi65k1ZvH0Z/v+occP7XBglAC33XWIotLbgv7jaCYmTXOtCS3u7i7D8LLEUGG1E 1wLJ5hznnv+UHEw/D0+x5Mwe93sfjtOo3WoHy0ky8nbKalKa+jsogiqn7oVpYsvQr4 v8oK1gTv+qjBW6QOJIlp4dVllrq3kz45KQXBpskykS2j9YhnbY1hAbP17mZclkPJQa 0kXzGsej2YS8pjKmGuTs+ST/CyK293Lgqmuun6hf0T8pcBJYqi2Q8hdI4MDsewVfBY oxYEooDYzY9gU1f5ZWf7PUTV9Hpuc3rh4XK/quE7NHwEchoFtCC1PnBpqgSpiwwrzw PufMEltFqFXew== From: Sasha Levin To: Andrew Morton , Masahiro Yamada , Luis Chamberlain , Linus Torvalds , Richard Weinberger , Juergen Gross , Geert Uytterhoeven , James Bottomley Cc: Jonathan Corbet , Nathan Chancellor , Nicolas Schier , Petr Pavlu , Daniel Gomez , Greg KH , Petr Mladek , Steven Rostedt , Kees Cook , Peter Zijlstra , Thorsten Leemhuis , Vlastimil Babka , Helge Deller , Randy Dunlap , Laurent Pinchart , Vivian Wang , linux-kernel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-modules@vger.kernel.org, linux-doc@vger.kernel.org, Sasha Levin Subject: [PATCH v4 3/4] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction Date: Sun, 22 Mar 2026 09:15:42 -0400 Message-ID: <20260322131543.971079-4-sashal@kernel.org> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260322131543.971079-1-sashal@kernel.org> References: <20260322131543.971079-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Replace the flat uncompressed parallel arrays (lineinfo_addrs[], lineinfo_file_ids[], lineinfo_lines[]) with a block-indexed, delta-encoded, ULEB128 varint compressed format. The sorted address array has small deltas between consecutive entries (typically 1-50 bytes), file IDs have high locality (delta often 0, same file), and line numbers change slowly. Delta-encoding followed by ULEB128 varint compression shrinks most values from 4 bytes to 1. Entries are grouped into blocks of 64. A small uncompressed block index (first addr + byte offset per block) enables O(log(N/64)) binary search, followed by sequential decode of at most 64 varints within the matching block. All decode state lives on the stack -- zero allocations, still safe for NMI/panic context. Measured on a defconfig+debug x86_64 build (3,017,154 entries, 4,822 source files, 47,144 blocks): Before (flat arrays): lineinfo_addrs[] 12,068,616 bytes (u32 x 3.0M) lineinfo_file_ids[] 6,034,308 bytes (u16 x 3.0M) lineinfo_lines[] 12,068,616 bytes (u32 x 3.0M) Total: 30,171,540 bytes (28.8 MiB, 10.0 bytes/entry) After (block-indexed delta + ULEB128): lineinfo_block_addrs[] 188,576 bytes (184 KiB) lineinfo_block_offsets[] 188,576 bytes (184 KiB) lineinfo_data[] 10,926,128 bytes (10.4 MiB) Total: 11,303,280 bytes (10.8 MiB, 3.7 bytes/entry) Savings: 18.0 MiB (2.7x reduction) Booted in QEMU and verified with SysRq-l that annotations still work: default_idle+0x9/0x10 (arch/x86/kernel/process.c:767) default_idle_call+0x6c/0xb0 (kernel/sched/idle.c:122) do_idle+0x335/0x490 (kernel/sched/idle.c:191) cpu_startup_entry+0x4e/0x60 (kernel/sched/idle.c:429) rest_init+0x1aa/0x1b0 (init/main.c:760) Suggested-by: Juergen Gross Assisted-by: Claude:claude-opus-4-6 Signed-off-by: Sasha Levin --- .../admin-guide/kallsyms-lineinfo.rst | 7 +- include/linux/mod_lineinfo.h | 227 ++++++++++++++++-- init/Kconfig | 8 +- kernel/kallsyms.c | 46 ++-- kernel/kallsyms_internal.h | 8 +- kernel/module/kallsyms.c | 85 +++---- scripts/empty_lineinfo.S | 20 +- scripts/gen_lineinfo.c | 189 +++++++++------ scripts/kallsyms.c | 7 +- 9 files changed, 406 insertions(+), 191 deletions(-) diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentatio= n/admin-guide/kallsyms-lineinfo.rst index 5cae995eb118e..dd264830c8d5b 100644 --- a/Documentation/admin-guide/kallsyms-lineinfo.rst +++ b/Documentation/admin-guide/kallsyms-lineinfo.rst @@ -76,10 +76,11 @@ Memory Overhead =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D =20 The vmlinux lineinfo tables are stored in ``.rodata`` and typically add -approximately 44 MiB to the kernel image for a standard configuration -(~4.6 million DWARF line entries, ~10 bytes per entry after deduplication). +approximately 10-15 MiB to the kernel image for a standard configuration +(~4.6 million DWARF line entries, ~2-3 bytes per entry after delta +compression). =20 -Per-module lineinfo adds approximately 10 bytes per DWARF line entry to ea= ch +Per-module lineinfo adds approximately 2-3 bytes per DWARF line entry to e= ach ``.ko`` file. =20 Known Limitations diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h index d62e9608f0f82..364e5d81fe5bb 100644 --- a/include/linux/mod_lineinfo.h +++ b/include/linux/mod_lineinfo.h @@ -8,13 +8,23 @@ * * Section layout (all values in target-native endianness): * - * struct mod_lineinfo_header (16 bytes) - * u32 addrs[num_entries] -- offsets from .text base, sorted - * u16 file_ids[num_entries] -- parallel to addrs - * <2-byte pad if num_entries is odd> - * u32 lines[num_entries] -- parallel to addrs + * struct mod_lineinfo_header + * u32 block_addrs[num_blocks] -- first addr per block, for binary se= arch + * u32 block_offsets[num_blocks] -- byte offset into compressed data st= ream + * u8 data[data_size] -- LEB128 delta-compressed entries * u32 file_offsets[num_files] -- byte offset into filenames[] * char filenames[filenames_size] -- concatenated NUL-terminated strings + * + * Each sub-array is located by an explicit (offset, size) pair in the + * header, similar to a flattened devicetree. This makes bounds checking + * straightforward: validate offset + size <=3D section_size for each arra= y. + * + * Compressed stream format (per block of LINEINFO_BLOCK_ENTRIES entries): + * Entry 0: file_id (ULEB128), line (ULEB128) + * addr is in block_addrs[] + * Entry 1..N: addr_delta (ULEB128), + * file_id_delta (SLEB128), + * line_delta (SLEB128) */ #ifndef _LINUX_MOD_LINEINFO_H #define _LINUX_MOD_LINEINFO_H @@ -25,44 +35,209 @@ #include typedef uint32_t u32; typedef uint16_t u16; +typedef uint8_t u8; #endif =20 +#define LINEINFO_BLOCK_ENTRIES 64 + struct mod_lineinfo_header { u32 num_entries; + u32 num_blocks; u32 num_files; - u32 filenames_size; /* total bytes of concatenated filenames */ - u32 reserved; /* padding, must be 0 */ + u32 blocks_offset; /* offset to block_addrs[] from section start */ + u32 blocks_size; /* bytes: num_blocks * 2 * sizeof(u32) */ + u32 data_offset; /* offset to compressed stream */ + u32 data_size; /* bytes of compressed data */ + u32 files_offset; /* offset to file_offsets[] */ + u32 files_size; /* bytes: num_files * sizeof(u32) */ + u32 filenames_offset; + u32 filenames_size; + u32 reserved; /* must be 0 */ }; =20 -/* Offset helpers: compute byte offset from start of section to each array= */ +/* + * Descriptor for a lineinfo table, used by the shared lookup function. + * Callers populate this from either linker globals (vmlinux) or a + * validated mod_lineinfo_header (modules). + */ +struct lineinfo_table { + const u32 *blk_addrs; + const u32 *blk_offsets; + const u8 *data; + u32 data_size; + const u32 *file_offsets; + const char *filenames; + u32 num_entries; + u32 num_blocks; + u32 num_files; + u32 filenames_size; +}; =20 -static inline u32 mod_lineinfo_addrs_off(void) +/* + * Read a ULEB128 varint from a byte stream. + * Returns the decoded value and advances *pos past the encoded bytes. + * If *pos would exceed 'end', returns 0 and sets *pos =3D end (safe for + * NMI/panic context: no crash, just a missed annotation). + */ +static inline u32 lineinfo_read_uleb128(const u8 *data, u32 *pos, u32 end) { - return sizeof(struct mod_lineinfo_header); -} + u32 result =3D 0; + unsigned int shift =3D 0; =20 -static inline u32 mod_lineinfo_file_ids_off(u32 num_entries) -{ - return mod_lineinfo_addrs_off() + num_entries * sizeof(u32); + while (*pos < end) { + u8 byte =3D data[*pos]; + (*pos)++; + result |=3D (u32)(byte & 0x7f) << shift; + if (!(byte & 0x80)) + return result; + shift +=3D 7; + if (shift >=3D 32) { + /* Malformed: skip remaining continuation bytes */ + while (*pos < end && (data[*pos] & 0x80)) + (*pos)++; + if (*pos < end) + (*pos)++; + return result; + } + } + return result; } =20 -static inline u32 mod_lineinfo_lines_off(u32 num_entries) +/* Read an SLEB128 varint. Same safety guarantees as above. */ +static inline int32_t lineinfo_read_sleb128(const u8 *data, u32 *pos, u32 = end) { - /* u16 file_ids[] may need 2-byte padding to align lines[] to 4 bytes */ - u32 off =3D mod_lineinfo_file_ids_off(num_entries) + - num_entries * sizeof(u16); - return (off + 3) & ~3u; -} + int32_t result =3D 0; + unsigned int shift =3D 0; + u8 byte =3D 0; =20 -static inline u32 mod_lineinfo_file_offsets_off(u32 num_entries) -{ - return mod_lineinfo_lines_off(num_entries) + num_entries * sizeof(u32); + while (*pos < end) { + byte =3D data[*pos]; + (*pos)++; + result |=3D (int32_t)(byte & 0x7f) << shift; + shift +=3D 7; + if (!(byte & 0x80)) + break; + if (shift >=3D 32) { + while (*pos < end && (data[*pos] & 0x80)) + (*pos)++; + if (*pos < end) + (*pos)++; + return result; + } + } + + /* Sign-extend if the high bit of the last byte was set */ + if (shift < 32 && (byte & 0x40)) + result |=3D -(1 << shift); + + return result; } =20 -static inline u32 mod_lineinfo_filenames_off(u32 num_entries, u32 num_file= s) +/* + * Search a lineinfo table for the source file and line corresponding to a + * given offset (from _text for vmlinux, from .text base for modules). + * + * Safe for NMI and panic context: no locks, no allocations, all state on = stack. + * Returns true and sets @file and @line on success; false on any failure. + */ +static inline bool lineinfo_search(const struct lineinfo_table *tbl, + unsigned int offset, + const char **file, unsigned int *line) { - return mod_lineinfo_file_offsets_off(num_entries) + - num_files * sizeof(u32); + unsigned int low, high, mid, block; + unsigned int cur_addr, cur_file_id, cur_line; + unsigned int best_file_id =3D 0, best_line =3D 0; + unsigned int block_entries, data_end; + bool found =3D false; + u32 pos; + + if (!tbl->num_entries || !tbl->num_blocks) + return false; + + /* Binary search on blk_addrs[] to find the right block */ + low =3D 0; + high =3D tbl->num_blocks; + while (low < high) { + mid =3D low + (high - low) / 2; + if (tbl->blk_addrs[mid] <=3D offset) + low =3D mid + 1; + else + high =3D mid; + } + + if (low =3D=3D 0) + return false; + block =3D low - 1; + + /* How many entries in this block? */ + block_entries =3D LINEINFO_BLOCK_ENTRIES; + if (block =3D=3D tbl->num_blocks - 1) { + unsigned int remaining =3D tbl->num_entries - + block * LINEINFO_BLOCK_ENTRIES; + + if (remaining < block_entries) + block_entries =3D remaining; + } + + /* Determine end of this block's data in the compressed stream */ + if (block + 1 < tbl->num_blocks) + data_end =3D tbl->blk_offsets[block + 1]; + else + data_end =3D tbl->data_size; + + /* Clamp data_end to actual data size */ + if (data_end > tbl->data_size) + data_end =3D tbl->data_size; + + /* Decode entry 0: addr from blk_addrs, file_id and line from stream */ + pos =3D tbl->blk_offsets[block]; + if (pos >=3D data_end) + return false; + + cur_addr =3D tbl->blk_addrs[block]; + cur_file_id =3D lineinfo_read_uleb128(tbl->data, &pos, data_end); + cur_line =3D lineinfo_read_uleb128(tbl->data, &pos, data_end); + + /* Check entry 0 */ + if (cur_addr <=3D offset) { + best_file_id =3D cur_file_id; + best_line =3D cur_line; + found =3D true; + } + + /* Decode entries 1..N */ + for (unsigned int i =3D 1; i < block_entries; i++) { + unsigned int addr_delta; + int32_t file_delta, line_delta; + + addr_delta =3D lineinfo_read_uleb128(tbl->data, &pos, data_end); + file_delta =3D lineinfo_read_sleb128(tbl->data, &pos, data_end); + line_delta =3D lineinfo_read_sleb128(tbl->data, &pos, data_end); + + cur_addr +=3D addr_delta; + cur_file_id =3D (unsigned int)((int32_t)cur_file_id + file_delta); + cur_line =3D (unsigned int)((int32_t)cur_line + line_delta); + + if (cur_addr > offset) + break; + + best_file_id =3D cur_file_id; + best_line =3D cur_line; + found =3D true; + } + + if (!found) + return false; + + if (best_file_id >=3D tbl->num_files) + return false; + + if (tbl->file_offsets[best_file_id] >=3D tbl->filenames_size) + return false; + + *file =3D &tbl->filenames[tbl->file_offsets[best_file_id]]; + *line =3D best_line; + return true; } =20 #endif /* _LINUX_MOD_LINEINFO_H */ diff --git a/init/Kconfig b/init/Kconfig index bf53275bc405a..6e3795b3dbd62 100644 --- a/init/Kconfig +++ b/init/Kconfig @@ -2065,8 +2065,9 @@ config KALLSYMS_LINEINFO anon_vma_clone+0x2ed/0xcf0 (mm/rmap.c:412) =20 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). =20 If unsure, say N. =20 @@ -2079,7 +2080,8 @@ config KALLSYMS_LINEINFO_MODULES so stack traces from module code include (file.c:123) annotations. =20 Requires elfutils (libdw-dev/elfutils-devel) on the build host. - Increases .ko sizes by approximately 10 bytes per DWARF line entry. + Increases .ko sizes by approximately 2-3 bytes per DWARF line + entry after delta compression. =20 If unsure, say N. =20 diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c index 9df92b0fd9041..76e30cac3a277 100644 --- a/kernel/kallsyms.c +++ b/kernel/kallsyms.c @@ -467,13 +467,16 @@ static int append_buildid(char *buffer, const char = *modname, =20 #endif /* CONFIG_STACKTRACE_BUILD_ID */ =20 +#include + bool kallsyms_lookup_lineinfo(unsigned long addr, const char **file, unsigned int *line) { + struct lineinfo_table tbl; unsigned long long raw_offset; - unsigned int offset, low, high, mid, file_id; =20 - if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) || !lineinfo_num_entries) + if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) || + !lineinfo_num_entries || !lineinfo_num_blocks) return false; =20 /* Compute offset from _text */ @@ -483,34 +486,19 @@ bool kallsyms_lookup_lineinfo(unsigned long addr, raw_offset =3D addr - (unsigned long)_text; if (raw_offset > UINT_MAX) return false; - offset =3D (unsigned int)raw_offset; - - /* Binary search for largest entry <=3D offset */ - low =3D 0; - high =3D lineinfo_num_entries; - while (low < high) { - mid =3D low + (high - low) / 2; - if (lineinfo_addrs[mid] <=3D offset) - low =3D mid + 1; - else - high =3D mid; - } - - if (low =3D=3D 0) - return false; - low--; - - file_id =3D lineinfo_file_ids[low]; - *line =3D lineinfo_lines[low]; - - if (file_id >=3D lineinfo_num_files) - return false; - - if (lineinfo_file_offsets[file_id] >=3D lineinfo_filenames_size) - return false; =20 - *file =3D &lineinfo_filenames[lineinfo_file_offsets[file_id]]; - return true; + tbl.blk_addrs =3D lineinfo_block_addrs; + tbl.blk_offsets =3D lineinfo_block_offsets; + tbl.data =3D lineinfo_data; + tbl.data_size =3D lineinfo_data_size; + tbl.file_offsets =3D lineinfo_file_offsets; + tbl.filenames =3D lineinfo_filenames; + tbl.num_entries =3D lineinfo_num_entries; + tbl.num_blocks =3D lineinfo_num_blocks; + tbl.num_files =3D lineinfo_num_files; + tbl.filenames_size =3D lineinfo_filenames_size; + + return lineinfo_search(&tbl, (unsigned int)raw_offset, file, line); } =20 /* Look up a kernel symbol and return it in a text buffer. */ diff --git a/kernel/kallsyms_internal.h b/kernel/kallsyms_internal.h index d7374ce444d81..ffe4c658067ec 100644 --- a/kernel/kallsyms_internal.h +++ b/kernel/kallsyms_internal.h @@ -16,10 +16,12 @@ extern const unsigned int kallsyms_markers[]; extern const u8 kallsyms_seqs_of_names[]; =20 extern const u32 lineinfo_num_entries; -extern const u32 lineinfo_addrs[]; -extern const u16 lineinfo_file_ids[]; -extern const u32 lineinfo_lines[]; extern const u32 lineinfo_num_files; +extern const u32 lineinfo_num_blocks; +extern const u32 lineinfo_block_addrs[]; +extern const u32 lineinfo_block_offsets[]; +extern const u32 lineinfo_data_size; +extern const u8 lineinfo_data[]; extern const u32 lineinfo_file_offsets[]; extern const u32 lineinfo_filenames_size; extern const char lineinfo_filenames[]; diff --git a/kernel/module/kallsyms.c b/kernel/module/kallsyms.c index 5b46293e957ab..8715a923ba536 100644 --- a/kernel/module/kallsyms.c +++ b/kernel/module/kallsyms.c @@ -509,16 +509,11 @@ bool module_lookup_lineinfo(struct module *mod, unsig= ned long addr, const char **file, unsigned int *line) { const struct mod_lineinfo_header *hdr; + struct lineinfo_table tbl; const void *base; - const u32 *addrs, *lines, *file_offsets; - const u16 *file_ids; - const char *filenames; - u32 num_entries, num_files, filenames_size; + u32 section_size; unsigned long text_base; - unsigned int offset; unsigned long long raw_offset; - unsigned int low, high, mid; - u16 file_id; =20 if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES)) return false; @@ -527,61 +522,55 @@ bool module_lookup_lineinfo(struct module *mod, unsig= ned long addr, if (!base) return false; =20 - if (mod->lineinfo_data_size < sizeof(*hdr)) + section_size =3D mod->lineinfo_data_size; + if (section_size < sizeof(*hdr)) return false; =20 hdr =3D base; - num_entries =3D hdr->num_entries; - num_files =3D hdr->num_files; - filenames_size =3D hdr->filenames_size; =20 - if (num_entries =3D=3D 0) + if (hdr->num_entries =3D=3D 0 || hdr->num_blocks =3D=3D 0) return false; =20 - /* Validate section is large enough for all arrays */ - if (mod->lineinfo_data_size < - mod_lineinfo_filenames_off(num_entries, num_files) + filenames_size) + /* Validate each sub-array fits within the section */ + if (hdr->blocks_offset + hdr->blocks_size > section_size) return false; - - addrs =3D base + mod_lineinfo_addrs_off(); - file_ids =3D base + mod_lineinfo_file_ids_off(num_entries); - lines =3D base + mod_lineinfo_lines_off(num_entries); - file_offsets =3D base + mod_lineinfo_file_offsets_off(num_entries); - filenames =3D base + mod_lineinfo_filenames_off(num_entries, num_files); - - /* Compute offset from module .text base */ - text_base =3D (unsigned long)mod->mem[MOD_TEXT].base; - if (addr < text_base) + if (hdr->data_offset + hdr->data_size > section_size) return false; - - raw_offset =3D addr - text_base; - if (raw_offset > UINT_MAX) + if (hdr->files_offset + hdr->files_size > section_size) + return false; + if (hdr->filenames_offset + hdr->filenames_size > section_size) return false; - offset =3D (unsigned int)raw_offset; - - /* Binary search for largest entry <=3D offset */ - low =3D 0; - high =3D num_entries; - while (low < high) { - mid =3D low + (high - low) / 2; - if (addrs[mid] <=3D offset) - low =3D mid + 1; - else - high =3D mid; - } =20 - if (low =3D=3D 0) + /* Validate array sizes match declared counts */ + if (hdr->blocks_size < hdr->num_blocks * 2 * sizeof(u32)) + return false; + if (hdr->files_size < hdr->num_files * sizeof(u32)) return false; - low--; =20 - file_id =3D file_ids[low]; - if (file_id >=3D num_files) + /* + * Compute offset from module .text base. + * NOTE: This assumes .text is at the start of the MOD_TEXT segment. + * A proper fix would use ELF relocations to reference .text directly. + */ + text_base =3D (unsigned long)mod->mem[MOD_TEXT].base; + if (addr < text_base) return false; =20 - if (file_offsets[file_id] >=3D filenames_size) + raw_offset =3D addr - text_base; + if (raw_offset > U32_MAX) return false; =20 - *file =3D &filenames[file_offsets[file_id]]; - *line =3D lines[low]; - return true; + tbl.blk_addrs =3D base + hdr->blocks_offset; + tbl.blk_offsets =3D base + hdr->blocks_offset + + hdr->num_blocks * sizeof(u32); + tbl.data =3D base + hdr->data_offset; + tbl.data_size =3D hdr->data_size; + tbl.file_offsets =3D base + hdr->files_offset; + tbl.filenames =3D base + hdr->filenames_offset; + tbl.num_entries =3D hdr->num_entries; + tbl.num_blocks =3D hdr->num_blocks; + tbl.num_files =3D hdr->num_files; + tbl.filenames_size =3D hdr->filenames_size; + + return lineinfo_search(&tbl, (unsigned int)raw_offset, file, line); } diff --git a/scripts/empty_lineinfo.S b/scripts/empty_lineinfo.S index e058c41137123..edd5b1092f050 100644 --- a/scripts/empty_lineinfo.S +++ b/scripts/empty_lineinfo.S @@ -14,12 +14,20 @@ lineinfo_num_entries: .balign 4 lineinfo_num_files: .long 0 - .globl lineinfo_addrs -lineinfo_addrs: - .globl lineinfo_file_ids -lineinfo_file_ids: - .globl lineinfo_lines -lineinfo_lines: + .globl lineinfo_num_blocks + .balign 4 +lineinfo_num_blocks: + .long 0 + .globl lineinfo_block_addrs +lineinfo_block_addrs: + .globl lineinfo_block_offsets +lineinfo_block_offsets: + .globl lineinfo_data_size + .balign 4 +lineinfo_data_size: + .long 0 + .globl lineinfo_data +lineinfo_data: .globl lineinfo_file_offsets lineinfo_file_offsets: .globl lineinfo_filenames_size diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c index 7d06701549345..45b1c1081164d 100644 --- a/scripts/gen_lineinfo.c +++ b/scripts/gen_lineinfo.c @@ -548,6 +548,35 @@ static void deduplicate(void) num_entries =3D j + 1; } =20 +/* + * Emit the LEB128 delta-compressed data stream for one block. + * Uses .uleb128/.sleb128 assembler directives for encoding. + */ +static void emit_block_data(unsigned int block) +{ + unsigned int base =3D block * LINEINFO_BLOCK_ENTRIES; + unsigned int count =3D num_entries - base; + + if (count > LINEINFO_BLOCK_ENTRIES) + count =3D LINEINFO_BLOCK_ENTRIES; + + /* Entry 0: file_id, line (both unsigned) */ + printf("\t.uleb128 %u\n", entries[base].file_id); + printf("\t.uleb128 %u\n", entries[base].line); + + /* Entries 1..N: addr_delta (unsigned), file/line deltas (signed) */ + for (unsigned int i =3D 1; i < count; i++) { + unsigned int idx =3D base + i; + + printf("\t.uleb128 %u\n", + entries[idx].offset - entries[idx - 1].offset); + printf("\t.sleb128 %d\n", + (int)entries[idx].file_id - (int)entries[idx - 1].file_id); + printf("\t.sleb128 %d\n", + (int)entries[idx].line - (int)entries[idx - 1].line); + } +} + static void compute_file_offsets(void) { unsigned int offset =3D 0; @@ -571,6 +600,11 @@ static void print_escaped_asciz(const char *s) =20 static void output_assembly(void) { + unsigned int num_blocks; + + num_blocks =3D num_entries ? + (num_entries + LINEINFO_BLOCK_ENTRIES - 1) / LINEINFO_BLOCK_ENTRIES : 0; + printf("/* SPDX-License-Identifier: GPL-2.0 */\n"); printf("/*\n"); printf(" * Automatically generated by scripts/gen_lineinfo\n"); @@ -591,29 +625,40 @@ static void output_assembly(void) printf("lineinfo_num_files:\n"); printf("\t.long %u\n\n", num_files); =20 - /* Sorted address offsets from _text */ - printf("\t.globl lineinfo_addrs\n"); + /* Number of blocks */ + printf("\t.globl lineinfo_num_blocks\n"); + printf("\t.balign 4\n"); + printf("lineinfo_num_blocks:\n"); + printf("\t.long %u\n\n", num_blocks); + + /* Block first-addresses for binary search */ + printf("\t.globl lineinfo_block_addrs\n"); printf("\t.balign 4\n"); - printf("lineinfo_addrs:\n"); - for (unsigned int i =3D 0; i < num_entries; i++) - printf("\t.long 0x%x\n", entries[i].offset); - printf("\n"); - - /* File IDs, parallel to addrs (u16 -- supports up to 65535 files) */ - printf("\t.globl lineinfo_file_ids\n"); - printf("\t.balign 2\n"); - printf("lineinfo_file_ids:\n"); - for (unsigned int i =3D 0; i < num_entries; i++) - printf("\t.short %u\n", entries[i].file_id); - printf("\n"); - - /* Line numbers, parallel to addrs */ - printf("\t.globl lineinfo_lines\n"); + printf("lineinfo_block_addrs:\n"); + for (unsigned int i =3D 0; i < num_blocks; i++) + printf("\t.long 0x%x\n", entries[i * LINEINFO_BLOCK_ENTRIES].offset); + + /* Block byte offsets into compressed stream */ + printf("\t.globl lineinfo_block_offsets\n"); printf("\t.balign 4\n"); - printf("lineinfo_lines:\n"); - for (unsigned int i =3D 0; i < num_entries; i++) - printf("\t.long %u\n", entries[i].line); - printf("\n"); + printf("lineinfo_block_offsets:\n"); + for (unsigned int i =3D 0; i < num_blocks; i++) + printf("\t.long .Lblock_%u - lineinfo_data\n", i); + + /* Compressed data size */ + printf("\t.globl lineinfo_data_size\n"); + printf("\t.balign 4\n"); + printf("lineinfo_data_size:\n"); + printf("\t.long .Ldata_end - lineinfo_data\n\n"); + + /* Compressed data stream */ + printf("\t.globl lineinfo_data\n"); + printf("lineinfo_data:\n"); + for (unsigned int i =3D 0; i < num_blocks; i++) { + printf(".Lblock_%u:\n", i); + emit_block_data(i); + } + printf(".Ldata_end:\n\n"); =20 /* File string offset table */ printf("\t.globl lineinfo_file_offsets\n"); @@ -621,34 +666,27 @@ static void output_assembly(void) printf("lineinfo_file_offsets:\n"); for (unsigned int i =3D 0; i < num_files; i++) printf("\t.long %u\n", files[i].str_offset); - printf("\n"); =20 /* Filenames size */ - { - unsigned int fsize =3D 0; - - for (unsigned int i =3D 0; i < num_files; i++) - fsize +=3D strlen(files[i].name) + 1; - printf("\t.globl lineinfo_filenames_size\n"); - printf("\t.balign 4\n"); - printf("lineinfo_filenames_size:\n"); - printf("\t.long %u\n\n", fsize); - } + printf("\t.globl lineinfo_filenames_size\n"); + printf("\t.balign 4\n"); + printf("lineinfo_filenames_size:\n"); + printf("\t.long .Lfilenames_end - lineinfo_filenames\n\n"); =20 /* Concatenated NUL-terminated filenames */ printf("\t.globl lineinfo_filenames\n"); printf("lineinfo_filenames:\n"); for (unsigned int i =3D 0; i < num_files; i++) print_escaped_asciz(files[i].name); - printf("\n"); + printf(".Lfilenames_end:\n"); } =20 static void output_module_assembly(void) { - unsigned int filenames_size =3D 0; + unsigned int num_blocks; =20 - for (unsigned int i =3D 0; i < num_files; i++) - filenames_size +=3D strlen(files[i].name) + 1; + num_blocks =3D num_entries ? + (num_entries + LINEINFO_BLOCK_ENTRIES - 1) / LINEINFO_BLOCK_ENTRIES : 0; =20 printf("/* SPDX-License-Identifier: GPL-2.0 */\n"); printf("/*\n"); @@ -658,46 +696,56 @@ static void output_module_assembly(void) =20 printf("\t.section .mod_lineinfo, \"a\"\n\n"); =20 - /* Header: num_entries, num_files, filenames_size, reserved */ + /* + * Header -- offsets and sizes are assembler expressions so the + * layout is self-describing without manual C arithmetic. + */ + printf(".Lhdr:\n"); 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 =3D 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 =3D 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 =3D 0; i < num_entries; i++) - printf("\t.long %u\n", entries[i].line); - if (num_entries) - printf("\n"); + printf("\t.long %u\t\t\t\t/* num_entries */\n", num_entries); + printf("\t.long %u\t\t\t\t/* num_blocks */\n", num_blocks); + printf("\t.long %u\t\t\t\t/* num_files */\n", num_files); + printf("\t.long .Lblk_addrs - .Lhdr\t\t/* blocks_offset */\n"); + printf("\t.long .Lblk_offsets_end - .Lblk_addrs\t/* blocks_size */\n"); + printf("\t.long .Ldata - .Lhdr\t\t\t/* data_offset */\n"); + printf("\t.long .Ldata_end - .Ldata\t\t/* data_size */\n"); + printf("\t.long .Lfile_offsets - .Lhdr\t\t/* files_offset */\n"); + printf("\t.long .Lfile_offsets_end - .Lfile_offsets /* files_size */\n"); + printf("\t.long .Lfilenames - .Lhdr\t\t/* filenames_offset */\n"); + printf("\t.long .Lfilenames_end - .Lfilenames\t/* filenames_size */\n"); + printf("\t.long 0\t\t\t\t\t/* reserved */\n\n"); + + /* block_addrs[] */ + printf(".Lblk_addrs:\n"); + for (unsigned int i =3D 0; i < num_blocks; i++) + printf("\t.long 0x%x\n", entries[i * LINEINFO_BLOCK_ENTRIES].offset); + + /* block_offsets[] */ + printf(".Lblk_offsets:\n"); + for (unsigned int i =3D 0; i < num_blocks; i++) + printf("\t.long .Lblock_%u - .Ldata\n", i); + printf(".Lblk_offsets_end:\n\n"); + + /* compressed data stream */ + printf(".Ldata:\n"); + for (unsigned int i =3D 0; i < num_blocks; i++) { + printf(".Lblock_%u:\n", i); + emit_block_data(i); + } + printf(".Ldata_end:\n"); =20 /* file_offsets[] */ + printf("\t.balign 4\n"); + printf(".Lfile_offsets:\n"); for (unsigned int i =3D 0; i < num_files; i++) printf("\t.long %u\n", files[i].str_offset); - if (num_files) - printf("\n"); + printf(".Lfile_offsets_end:\n\n"); =20 /* filenames[] */ + printf(".Lfilenames:\n"); for (unsigned int i =3D 0; i < num_files; i++) print_escaped_asciz(files[i].name); - if (num_files) - printf("\n"); + printf(".Lfilenames_end:\n"); } =20 int main(int argc, char *argv[]) @@ -777,8 +825,10 @@ int main(int argc, char *argv[]) deduplicate(); compute_file_offsets(); =20 - fprintf(stderr, "lineinfo: %u entries, %u files\n", - num_entries, num_files); + fprintf(stderr, "lineinfo: %u entries, %u files, %u blocks\n", + num_entries, num_files, + num_entries ? + (num_entries + LINEINFO_BLOCK_ENTRIES - 1) / LINEINFO_BLOCK_ENTRIES : 0); =20 if (module_mode) output_module_assembly(); @@ -794,6 +844,5 @@ int main(int argc, char *argv[]) for (unsigned int i =3D 0; i < num_files; i++) free(files[i].name); free(files); - return 0; } diff --git a/scripts/kallsyms.c b/scripts/kallsyms.c index 42662c4fbc6c9..94fbdad3df7c6 100644 --- a/scripts/kallsyms.c +++ b/scripts/kallsyms.c @@ -80,11 +80,12 @@ static bool is_ignored_symbol(const char *name, char ty= pe) { /* Ignore lineinfo symbols for kallsyms pass stability */ static const char * const lineinfo_syms[] =3D { - "lineinfo_addrs", - "lineinfo_file_ids", + "lineinfo_block_addrs", + "lineinfo_block_offsets", + "lineinfo_data", "lineinfo_file_offsets", "lineinfo_filenames", - "lineinfo_lines", + "lineinfo_num_blocks", "lineinfo_num_entries", "lineinfo_num_files", }; --=20 2.51.0 From nobody Fri Apr 3 22:31:38 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id E17D6329C57; Sun, 22 Mar 2026 13:15:58 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774185359; cv=none; b=RlWZnXm4xa5cHqleX+VjAhngVu5BlSHjckzeIDAGhXTeFIXOd2d69gcQ+X9xZT/PuHKvQASMaWy2vvHwLH6znl1ZzZE0p8k8olZ4tG9yGxVyELrQ6G0rT6VTiJGDECz2WbSkLYgqGGIILLOTU0ZN0PnxLfVh6uCydZ2tFPl/+5Q= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774185359; c=relaxed/simple; bh=VJwdCICo18xBWyjExrEtS1N8j00S8xqr3fyq5yvnVqM=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=lnVAOIVLKwEZRZuSAj64X2+I0Q8/cJYPocqw4iFnW1y1FV2d2aS8o2xAgWFUinJspQWqdO6ZhhgJKaVlCVPKtsDHNf9madnlLBcpYqDGWC8ahgUVOLwfje6Za5exr4suB7xTo4ovurytWdSulOloJ4gjnpBUk1i6/auO3sLUmx4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=tAEpLZwT; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="tAEpLZwT" Received: by smtp.kernel.org (Postfix) with ESMTPSA id DBE6BC19424; Sun, 22 Mar 2026 13:15:55 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1774185358; bh=VJwdCICo18xBWyjExrEtS1N8j00S8xqr3fyq5yvnVqM=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=tAEpLZwTYa0f6ZT/0ohoRqeMiCrVLunBthVEMvn0IWE+hUSbE6+Mb6lh86lgCWa47 TjgOpzdia6ASBCnIa5HLqNKJ9M3Sm43cAWbDBnhfCnyk/W+T6pxg1JelcczJV/E+vS 8s1P1pSOm1XfIKiXyc/XXZqJIexoDbliebWgGLmtcZ1mn9HyPllivZopVOv7veFZU2 Vkc3duyEEqluisF4BMiV6etfn8cGmEg8a+WSbNba9ouFWFIYmDwvIpzEzk3SJ5NXgb Dwp/xrZ6eSMkl68gajyBcZxvwCAhyd2z1cq7en7Sm1LMz/pJb5wQI9az2zJkJpvwKD mbZGcQMns12fg== From: Sasha Levin To: Andrew Morton , Masahiro Yamada , Luis Chamberlain , Linus Torvalds , Richard Weinberger , Juergen Gross , Geert Uytterhoeven , James Bottomley Cc: Jonathan Corbet , Nathan Chancellor , Nicolas Schier , Petr Pavlu , Daniel Gomez , Greg KH , Petr Mladek , Steven Rostedt , Kees Cook , Peter Zijlstra , Thorsten Leemhuis , Vlastimil Babka , Helge Deller , Randy Dunlap , Laurent Pinchart , Vivian Wang , linux-kernel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-modules@vger.kernel.org, linux-doc@vger.kernel.org, Sasha Levin Subject: [PATCH v4 4/4] kallsyms: add KUnit tests for lineinfo feature Date: Sun, 22 Mar 2026 09:15:43 -0400 Message-ID: <20260322131543.971079-5-sashal@kernel.org> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260322131543.971079-1-sashal@kernel.org> References: <20260322131543.971079-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Add a KUnit test module (CONFIG_LINEINFO_KUNIT_TEST) that verifies the kallsyms lineinfo feature produces correct source file:line annotations in stack traces. Export sprint_backtrace() and sprint_backtrace_build_id() as GPL symbols so the test module can exercise the backtrace APIs. Assisted-by: Claude:claude-opus-4-6 Signed-off-by: Sasha Levin --- MAINTAINERS | 1 + kernel/kallsyms.c | 2 + lib/Kconfig.debug | 10 + lib/tests/Makefile | 3 + lib/tests/lineinfo_kunit.c | 813 +++++++++++++++++++++++++++++++++++++ 5 files changed, 829 insertions(+) create mode 100644 lib/tests/lineinfo_kunit.c diff --git a/MAINTAINERS b/MAINTAINERS index 535e992ca5a20..118711f72b874 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -13733,6 +13733,7 @@ M: Sasha Levin 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 =20 diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c index 76e30cac3a277..e6f796d43dd70 100644 --- a/kernel/kallsyms.c +++ b/kernel/kallsyms.c @@ -625,6 +625,7 @@ int sprint_backtrace(char *buffer, unsigned long addres= s) { return __sprint_symbol(buffer, address, -1, 1, 0); } +EXPORT_SYMBOL_GPL(sprint_backtrace); =20 /** * sprint_backtrace_build_id - Look up a backtrace symbol and return it in= a text buffer @@ -645,6 +646,7 @@ int sprint_backtrace_build_id(char *buffer, unsigned lo= ng address) { return __sprint_symbol(buffer, address, -1, 1, 1); } +EXPORT_SYMBOL_GPL(sprint_backtrace_build_id); =20 /* To avoid using get_symbol_offset for every symbol, we carry prefix alon= g. */ struct kallsym_iter { diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug index 93f356d2b3d95..688bbcb3eaa62 100644 --- a/lib/Kconfig.debug +++ b/lib/Kconfig.debug @@ -3048,6 +3048,16 @@ config LONGEST_SYM_KUNIT_TEST =20 If unsure, say N. =20 +config LINEINFO_KUNIT_TEST + tristate "KUnit tests for kallsyms lineinfo" if !KUNIT_ALL_TESTS + depends on KUNIT && KALLSYMS_LINEINFO + default KUNIT_ALL_TESTS + help + KUnit tests for the kallsyms source line info feature. + Verifies that stack traces include correct (file.c:line) annotations. + + If unsure, say N. + config HW_BREAKPOINT_KUNIT_TEST bool "Test hw_breakpoint constraints accounting" if !KUNIT_ALL_TESTS depends on HAVE_HW_BREAKPOINT diff --git a/lib/tests/Makefile b/lib/tests/Makefile index 05f74edbc62bf..c6add3b04bbd5 100644 --- a/lib/tests/Makefile +++ b/lib/tests/Makefile @@ -36,6 +36,9 @@ obj-$(CONFIG_LIVEUPDATE_TEST) +=3D liveupdate.o CFLAGS_longest_symbol_kunit.o +=3D $(call cc-disable-warning, missing-prot= otypes) obj-$(CONFIG_LONGEST_SYM_KUNIT_TEST) +=3D longest_symbol_kunit.o =20 +CFLAGS_lineinfo_kunit.o +=3D $(call cc-option,-fno-inline-functions-called= -once) +obj-$(CONFIG_LINEINFO_KUNIT_TEST) +=3D lineinfo_kunit.o + obj-$(CONFIG_MEMCPY_KUNIT_TEST) +=3D memcpy_kunit.o obj-$(CONFIG_MIN_HEAP_KUNIT_TEST) +=3D min_heap_kunit.o CFLAGS_overflow_kunit.o =3D $(call cc-disable-warning, tautological-consta= nt-out-of-range-compare) diff --git a/lib/tests/lineinfo_kunit.c b/lib/tests/lineinfo_kunit.c new file mode 100644 index 0000000000000..81696fa0000aa --- /dev/null +++ b/lib/tests/lineinfo_kunit.c @@ -0,0 +1,813 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * KUnit tests for kallsyms lineinfo (CONFIG_KALLSYMS_LINEINFO). + * + * Copyright (c) 2026 Sasha Levin + * + * Verifies that sprint_symbol() and related APIs append correct + * " (file.c:NNN)" annotations to kernel symbol lookups. + * + * Build with: CONFIG_LINEINFO_KUNIT_TEST=3Dm (or =3Dy) + * Run with: ./tools/testing/kunit/kunit.py run lineinfo + */ + +#include +#include +#include +#include +#include +#include +#include + +/* --------------- helpers --------------- */ + +static char *alloc_sym_buf(struct kunit *test) +{ + return kunit_kzalloc(test, KSYM_SYMBOL_LEN, GFP_KERNEL); +} + +/* + * Return true if @buf contains a lineinfo annotation matching + * the pattern " (:)". + * + * The path may be a full path like "lib/tests/lineinfo_kunit.c" or + * a shortened form from module lineinfo (e.g., just a directory name). + */ +static bool has_lineinfo(const char *buf) +{ + const char *p, *colon, *end; + + p =3D strstr(buf, " ("); + if (!p) + return false; + p +=3D 2; /* skip " (" */ + + colon =3D strchr(p, ':'); + if (!colon || colon =3D=3D p) + return false; + + /* After colon: one or more digits then ')' */ + end =3D colon + 1; + if (*end < '0' || *end > '9') + return false; + while (*end >=3D '0' && *end <=3D '9') + end++; + return *end =3D=3D ')'; +} + +/* + * Extract line number from a lineinfo annotation. + * Returns 0 if not found. + */ +static unsigned int extract_line(const char *buf) +{ + const char *p, *colon; + unsigned int line =3D 0; + + p =3D strstr(buf, " ("); + if (!p) + return 0; + + colon =3D strchr(p + 2, ':'); + if (!colon) + return 0; + + colon++; + while (*colon >=3D '0' && *colon <=3D '9') { + line =3D line * 10 + (*colon - '0'); + colon++; + } + return line; +} + +/* + * Check if the lineinfo annotation contains the given filename substring. + */ +static bool lineinfo_contains_file(const char *buf, const char *name) +{ + const char *p, *colon; + + p =3D strstr(buf, " ("); + if (!p) + return false; + + colon =3D strchr(p + 2, ':'); + if (!colon) + return false; + + /* Search for @name between '(' and ':' */ + return strnstr(p + 1, name, colon - p - 1) !=3D NULL; +} + +/* --------------- target functions --------------- */ + +static noinline int lineinfo_target_normal(void) +{ + barrier(); + return 42; +} + +static noinline int lineinfo_target_short(void) +{ + barrier(); + return 1; +} + +static noinline int lineinfo_target_with_arg(int x) +{ + barrier(); + return x + 1; +} + +static noinline int lineinfo_target_many_lines(void) +{ + int a =3D 0; + + barrier(); + a +=3D 1; + a +=3D 2; + a +=3D 3; + a +=3D 4; + a +=3D 5; + a +=3D 6; + a +=3D 7; + a +=3D 8; + a +=3D 9; + a +=3D 10; + barrier(); + return a; +} + +static __always_inline int lineinfo_inline_helper(void) +{ + return 99; +} + +static noinline int lineinfo_inline_caller(void) +{ + barrier(); + return lineinfo_inline_helper(); +} + +/* 10-deep call chain */ +static noinline int lineinfo_chain_10(void) { barrier(); return 10; } +static noinline int lineinfo_chain_9(void) { barrier(); return lineinfo_c= hain_10(); } +static noinline int lineinfo_chain_8(void) { barrier(); return lineinfo_c= hain_9(); } +static noinline int lineinfo_chain_7(void) { barrier(); return lineinfo_c= hain_8(); } +static noinline int lineinfo_chain_6(void) { barrier(); return lineinfo_c= hain_7(); } +static noinline int lineinfo_chain_5(void) { barrier(); return lineinfo_c= hain_6(); } +static noinline int lineinfo_chain_4(void) { barrier(); return lineinfo_c= hain_5(); } +static noinline int lineinfo_chain_3(void) { barrier(); return lineinfo_c= hain_4(); } +static noinline int lineinfo_chain_2(void) { barrier(); return lineinfo_c= hain_3(); } +static noinline int lineinfo_chain_1(void) { barrier(); return lineinfo_c= hain_2(); } + +/* --------------- Group A: Basic lineinfo presence --------------- */ + +static void test_normal_function(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_normal; + + sprint_symbol(buf, addr); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo in: %s", buf); + KUNIT_EXPECT_TRUE_MSG(test, + lineinfo_contains_file(buf, "lineinfo_kunit.c"), + "Wrong file in: %s", buf); +} + +static void test_static_function(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_short; + + sprint_symbol(buf, addr); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo in: %s", buf); +} + +static void test_noinline_function(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_with_arg; + + sprint_symbol(buf, addr); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo in: %s", buf); +} + +static void test_inline_function(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_inline_caller; + + sprint_symbol(buf, addr); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo for inline caller in: %s", buf); + KUNIT_EXPECT_TRUE_MSG(test, + lineinfo_contains_file(buf, "lineinfo_kunit.c"), + "Wrong file in: %s", buf); +} + +static void test_short_function(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_short; + + sprint_symbol(buf, addr); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo for short function in: %s", buf); +} + +static void test_many_lines_function(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_many_lines; + unsigned int line; + + sprint_symbol(buf, addr); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo in: %s", buf); + line =3D extract_line(buf); + KUNIT_EXPECT_GT_MSG(test, line, (unsigned int)0, + "Line number should be > 0 in: %s", buf); +} + +/* --------------- Group B: Deep call chain --------------- */ + +typedef int (*chain_fn_t)(void); + +static void test_deep_call_chain(struct kunit *test) +{ + static const chain_fn_t chain_fns[] =3D { + lineinfo_chain_1, lineinfo_chain_2, + lineinfo_chain_3, lineinfo_chain_4, + lineinfo_chain_5, lineinfo_chain_6, + lineinfo_chain_7, lineinfo_chain_8, + lineinfo_chain_9, lineinfo_chain_10, + }; + char *buf =3D alloc_sym_buf(test); + int i, found =3D 0; + + /* Call chain to prevent dead-code elimination */ + KUNIT_ASSERT_EQ(test, lineinfo_chain_1(), 10); + + for (i =3D 0; i < ARRAY_SIZE(chain_fns); i++) { + unsigned long addr =3D (unsigned long)chain_fns[i]; + + sprint_symbol(buf, addr); + if (has_lineinfo(buf)) + found++; + } + + /* + * Not every tiny function gets DWARF line info (compiler may + * omit it for very small stubs), but at least some should. + */ + KUNIT_EXPECT_GT_MSG(test, found, 0, + "None of the 10 chain functions had lineinfo"); +} + +/* --------------- Group C: sprint_symbol API variants --------------- */ + +static void test_sprint_symbol_format(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_normal; + + sprint_symbol(buf, addr); + + /* Should contain +0x and /0x for offset/size */ + KUNIT_EXPECT_NOT_NULL_MSG(test, strstr(buf, "+0x"), + "Missing offset in: %s", buf); + KUNIT_EXPECT_NOT_NULL_MSG(test, strstr(buf, "/0x"), + "Missing size in: %s", buf); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo in: %s", buf); +} + +static void test_sprint_backtrace(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_normal; + + /* sprint_backtrace subtracts 1 internally to handle tail calls */ + sprint_backtrace(buf, addr + 1); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo in backtrace: %s", buf); + KUNIT_EXPECT_TRUE_MSG(test, + lineinfo_contains_file(buf, "lineinfo_kunit.c"), + "Wrong file in backtrace: %s", buf); +} + +static void test_sprint_backtrace_build_id(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_normal; + + sprint_backtrace_build_id(buf, addr + 1); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo in backtrace_build_id: %s", buf); +} + +static void test_sprint_symbol_no_offset(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_normal; + + sprint_symbol_no_offset(buf, addr); + /* No "+0x" in output */ + KUNIT_EXPECT_NULL_MSG(test, strstr(buf, "+0x"), + "Unexpected offset in no_offset: %s", buf); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo in no_offset: %s", buf); +} + +/* --------------- Group D: printk format specifiers --------------- */ + +static void test_pS_format(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + void *addr =3D lineinfo_target_normal; + + snprintf(buf, KSYM_SYMBOL_LEN, "%pS", addr); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo in %%pS: %s", buf); +} + +static void test_pBb_format(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + /* + * %pBb uses sprint_backtrace_build_id which subtracts 1 from the + * address, so pass addr+1 to resolve back to the function. + */ + void *addr =3D (void *)((unsigned long)lineinfo_target_normal + 1); + + snprintf(buf, KSYM_SYMBOL_LEN, "%pBb", addr); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo in %%pBb: %s", buf); +} + +static void test_pSR_format(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + void *addr =3D lineinfo_target_normal; + + snprintf(buf, KSYM_SYMBOL_LEN, "%pSR", addr); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo in %%pSR: %s", buf); +} + +/* --------------- Group E: Address edge cases --------------- */ + +static void test_symbol_start_addr(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_normal; + + sprint_symbol(buf, addr); + KUNIT_EXPECT_NOT_NULL_MSG(test, strstr(buf, "+0x0/"), + "Expected +0x0/ at function start: %s", buf); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo at function start: %s", buf); +} + +static void test_symbol_nonzero_offset(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_normal; + + /* + * sprint_backtrace subtracts 1 internally. + * Passing addr+2 resolves to addr+1 which is inside the function + * at a non-zero offset. + */ + sprint_backtrace(buf, addr + 2); + KUNIT_EXPECT_TRUE_MSG(test, + strnstr(buf, "lineinfo_target_normal", + KSYM_SYMBOL_LEN) !=3D NULL, + "Didn't resolve to expected function: %s", buf); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo at non-zero offset: %s", buf); +} + +static void test_unknown_address(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + + sprint_symbol(buf, 1UL); + /* Should be "0x1" with no lineinfo */ + KUNIT_EXPECT_NOT_NULL_MSG(test, strstr(buf, "0x1"), + "Expected hex address for bogus addr: %s", buf); + KUNIT_EXPECT_FALSE_MSG(test, has_lineinfo(buf), + "Unexpected lineinfo for bogus addr: %s", buf); +} + +static void test_kernel_function_lineinfo(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)sprint_symbol; + + sprint_symbol(buf, addr); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo for sprint_symbol: %s", buf); + KUNIT_EXPECT_TRUE_MSG(test, + lineinfo_contains_file(buf, "kallsyms.c"), + "Expected kallsyms.c in: %s", buf); +} + +static void test_assembly_no_lineinfo(struct kunit *test) +{ +#if IS_BUILTIN(CONFIG_LINEINFO_KUNIT_TEST) + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)_text; + + sprint_symbol(buf, addr); + /* + * _text is typically an asm entry point with no DWARF line info. + * If it has lineinfo, it's a C-based entry =E2=80=94 skip in that case. + */ + if (has_lineinfo(buf)) + kunit_skip(test, "_text has lineinfo (C entry?): %s", buf); + + KUNIT_EXPECT_FALSE_MSG(test, has_lineinfo(buf), + "Unexpected lineinfo for asm symbol: %s", buf); +#else + kunit_skip(test, "_text not accessible from modules"); +#endif +} + +/* --------------- Group F: Module path --------------- */ + +static void test_module_function_lineinfo(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_normal; + + if (!IS_MODULE(CONFIG_LINEINFO_KUNIT_TEST)) { + kunit_skip(test, "Test only meaningful when built as module"); + return; + } + + sprint_symbol(buf, addr); + KUNIT_EXPECT_NOT_NULL_MSG(test, + strstr(buf, "[lineinfo_kunit"), + "Missing module name in: %s", buf); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo for module function: %s", buf); + KUNIT_EXPECT_TRUE_MSG(test, + lineinfo_contains_file(buf, "lineinfo_kunit.c"), + "Wrong file for module function: %s", buf); +} + +/* --------------- Group G: Stress --------------- */ + +struct lineinfo_stress_data { + unsigned long addr; + atomic_t failures; +}; + +static void lineinfo_stress_fn(void *info) +{ + struct lineinfo_stress_data *data =3D info; + char buf[KSYM_SYMBOL_LEN]; + int i; + + for (i =3D 0; i < 100; i++) { + sprint_symbol(buf, data->addr); + if (!has_lineinfo(buf)) + atomic_inc(&data->failures); + } +} + +static void test_concurrent_sprint_symbol(struct kunit *test) +{ + struct lineinfo_stress_data data; + + data.addr =3D (unsigned long)lineinfo_target_normal; + atomic_set(&data.failures, 0); + + on_each_cpu(lineinfo_stress_fn, &data, 1); + + KUNIT_EXPECT_EQ_MSG(test, atomic_read(&data.failures), 0, + "Concurrent lineinfo failures detected"); +} + +static void test_rapid_sprint_symbol(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_normal; + int i, failures =3D 0; + + for (i =3D 0; i < 1000; i++) { + sprint_symbol(buf, addr); + if (!has_lineinfo(buf)) + failures++; + } + + KUNIT_EXPECT_EQ_MSG(test, failures, 0, + "Rapid sprint_symbol failures: %d/1000", failures); +} + +/* --------------- Group H: Safety and plausibility --------------- */ + +static void test_line_number_plausible(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_normal; + unsigned int line; + + sprint_symbol(buf, addr); + KUNIT_ASSERT_TRUE(test, has_lineinfo(buf)); + + line =3D extract_line(buf); + KUNIT_EXPECT_GT_MSG(test, line, (unsigned int)0, + "Line number should be > 0"); + KUNIT_EXPECT_LT_MSG(test, line, (unsigned int)10000, + "Line number %u implausibly large for this file", + line); +} + +static void test_buffer_no_overflow(struct kunit *test) +{ + const size_t canary_size =3D 16; + char *buf; + int i; + + buf =3D kunit_kzalloc(test, KSYM_SYMBOL_LEN + canary_size, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, buf); + + /* Fill canary area past KSYM_SYMBOL_LEN with 0xAA */ + memset(buf + KSYM_SYMBOL_LEN, 0xAA, canary_size); + + sprint_symbol(buf, (unsigned long)lineinfo_target_normal); + + /* Verify canary bytes are untouched */ + for (i =3D 0; i < canary_size; i++) { + KUNIT_EXPECT_EQ_MSG(test, + (unsigned char)buf[KSYM_SYMBOL_LEN + i], + (unsigned char)0xAA, + "Buffer overflow at offset %d past KSYM_SYMBOL_LEN", + i); + } +} + +static void test_dump_stack_no_crash(struct kunit *test) +{ + /* Just verify dump_stack() completes without panic */ + dump_stack(); + KUNIT_SUCCEED(test); +} + +static void test_sprint_symbol_build_id(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_normal; + + sprint_symbol_build_id(buf, addr); + KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf), + "No lineinfo in sprint_symbol_build_id: %s", buf); +} + +static void test_sleb128_edge_cases(struct kunit *test) +{ + u32 pos; + int32_t result; + + /* Value 0: single byte 0x00 */ + { + static const u8 data[] =3D { 0x00 }; + + pos =3D 0; + result =3D lineinfo_read_sleb128(data, &pos, sizeof(data)); + KUNIT_EXPECT_EQ(test, result, (int32_t)0); + KUNIT_EXPECT_EQ(test, pos, (u32)1); + } + + /* Value -1: single byte 0x7F */ + { + static const u8 data[] =3D { 0x7f }; + + pos =3D 0; + result =3D lineinfo_read_sleb128(data, &pos, sizeof(data)); + KUNIT_EXPECT_EQ(test, result, (int32_t)-1); + KUNIT_EXPECT_EQ(test, pos, (u32)1); + } + + /* Value 1: single byte 0x01 */ + { + static const u8 data[] =3D { 0x01 }; + + pos =3D 0; + result =3D lineinfo_read_sleb128(data, &pos, sizeof(data)); + KUNIT_EXPECT_EQ(test, result, (int32_t)1); + KUNIT_EXPECT_EQ(test, pos, (u32)1); + } + + /* Value -64: single byte 0x40 */ + { + static const u8 data[] =3D { 0x40 }; + + pos =3D 0; + result =3D lineinfo_read_sleb128(data, &pos, sizeof(data)); + KUNIT_EXPECT_EQ(test, result, (int32_t)-64); + KUNIT_EXPECT_EQ(test, pos, (u32)1); + } + + /* Value 63: single byte 0x3F */ + { + static const u8 data[] =3D { 0x3f }; + + pos =3D 0; + result =3D lineinfo_read_sleb128(data, &pos, sizeof(data)); + KUNIT_EXPECT_EQ(test, result, (int32_t)63); + KUNIT_EXPECT_EQ(test, pos, (u32)1); + } + + /* Value -128: two bytes 0x80 0x7F */ + { + static const u8 data[] =3D { 0x80, 0x7f }; + + pos =3D 0; + result =3D lineinfo_read_sleb128(data, &pos, sizeof(data)); + KUNIT_EXPECT_EQ(test, result, (int32_t)-128); + KUNIT_EXPECT_EQ(test, pos, (u32)2); + } +} + +static void test_uleb128_edge_cases(struct kunit *test) +{ + u32 pos, result; + + /* Value 0: single byte 0x00 */ + { + static const u8 data[] =3D { 0x00 }; + + pos =3D 0; + result =3D lineinfo_read_uleb128(data, &pos, sizeof(data)); + KUNIT_EXPECT_EQ(test, result, (u32)0); + KUNIT_EXPECT_EQ(test, pos, (u32)1); + } + + /* Value 127: single byte 0x7F */ + { + static const u8 data[] =3D { 0x7F }; + + pos =3D 0; + result =3D lineinfo_read_uleb128(data, &pos, sizeof(data)); + KUNIT_EXPECT_EQ(test, result, (u32)127); + KUNIT_EXPECT_EQ(test, pos, (u32)1); + } + + /* Value 128: two bytes 0x80 0x01 */ + { + static const u8 data[] =3D { 0x80, 0x01 }; + + pos =3D 0; + result =3D lineinfo_read_uleb128(data, &pos, sizeof(data)); + KUNIT_EXPECT_EQ(test, result, (u32)128); + KUNIT_EXPECT_EQ(test, pos, (u32)2); + } + + /* Max u32 0xFFFFFFFF: 5 bytes */ + { + static const u8 data[] =3D { 0xFF, 0xFF, 0xFF, 0xFF, 0x0F }; + + pos =3D 0; + result =3D lineinfo_read_uleb128(data, &pos, sizeof(data)); + KUNIT_EXPECT_EQ(test, result, (u32)0xFFFFFFFF); + KUNIT_EXPECT_EQ(test, pos, (u32)5); + } + + /* Truncated input: pos >=3D end returns 0 */ + { + static const u8 data[] =3D { 0x80 }; + + pos =3D 0; + result =3D lineinfo_read_uleb128(data, &pos, 0); + KUNIT_EXPECT_EQ_MSG(test, result, (u32)0, + "Expected 0 for empty input"); + } + + /* Truncated mid-varint: continuation byte but end reached */ + { + static const u8 data[] =3D { 0x80 }; + + pos =3D 0; + result =3D lineinfo_read_uleb128(data, &pos, 1); + KUNIT_EXPECT_EQ_MSG(test, result, (u32)0, + "Expected 0 for truncated varint"); + KUNIT_EXPECT_EQ(test, pos, (u32)1); + } +} + +static void test_line_number_accuracy(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_normal; + unsigned int line; + + sprint_symbol(buf, addr); + KUNIT_ASSERT_TRUE(test, has_lineinfo(buf)); + + line =3D extract_line(buf); + + /* + * lineinfo_target_normal is defined around line 103-107. + * Allow wide range: KASAN instrumentation and module lineinfo + * address mapping can shift the reported line significantly. + */ + KUNIT_EXPECT_GE_MSG(test, line, (unsigned int)50, + "Line %u too low for lineinfo_target_normal", line); + KUNIT_EXPECT_LE_MSG(test, line, (unsigned int)300, + "Line %u too high for lineinfo_target_normal", line); +} + +static void test_many_lines_mid_function(struct kunit *test) +{ + char *buf =3D alloc_sym_buf(test); + unsigned long addr =3D (unsigned long)lineinfo_target_many_lines; + unsigned int line; + unsigned long mid_addr; + + /* Get function size from sprint_symbol output */ + sprint_symbol(buf, addr); + KUNIT_ASSERT_TRUE(test, has_lineinfo(buf)); + + /* Try an address 8 bytes into the function (past prologue) */ + mid_addr =3D addr + 8; + sprint_symbol(buf, mid_addr); + + /* + * Should still resolve to lineinfo_target_many_lines. + * Lineinfo should be present with a plausible line number. + */ + KUNIT_EXPECT_TRUE_MSG(test, + strnstr(buf, "lineinfo_target_many_lines", + KSYM_SYMBOL_LEN) !=3D NULL, + "Mid-function addr resolved to wrong symbol: %s", + buf); + if (has_lineinfo(buf)) { + line =3D extract_line(buf); + KUNIT_EXPECT_GE_MSG(test, line, (unsigned int)50, + "Line %u too low for mid-function", line); + KUNIT_EXPECT_LE_MSG(test, line, (unsigned int)700, + "Line %u too high for mid-function", line); + } +} + +/* --------------- Suite registration --------------- */ + +static struct kunit_case lineinfo_test_cases[] =3D { + /* Group A: Basic lineinfo presence */ + KUNIT_CASE(test_normal_function), + KUNIT_CASE(test_static_function), + KUNIT_CASE(test_noinline_function), + KUNIT_CASE(test_inline_function), + KUNIT_CASE(test_short_function), + KUNIT_CASE(test_many_lines_function), + /* Group B: Deep call chain */ + KUNIT_CASE(test_deep_call_chain), + /* Group C: sprint_symbol API variants */ + KUNIT_CASE(test_sprint_symbol_format), + KUNIT_CASE(test_sprint_backtrace), + KUNIT_CASE(test_sprint_backtrace_build_id), + KUNIT_CASE(test_sprint_symbol_no_offset), + /* Group D: printk format specifiers */ + KUNIT_CASE(test_pS_format), + KUNIT_CASE(test_pBb_format), + KUNIT_CASE(test_pSR_format), + /* Group E: Address edge cases */ + KUNIT_CASE(test_symbol_start_addr), + KUNIT_CASE(test_symbol_nonzero_offset), + KUNIT_CASE(test_unknown_address), + KUNIT_CASE(test_kernel_function_lineinfo), + KUNIT_CASE(test_assembly_no_lineinfo), + /* Group F: Module path */ + KUNIT_CASE(test_module_function_lineinfo), + /* Group G: Stress */ + KUNIT_CASE_SLOW(test_concurrent_sprint_symbol), + KUNIT_CASE_SLOW(test_rapid_sprint_symbol), + /* Group H: Safety and plausibility */ + KUNIT_CASE(test_line_number_plausible), + KUNIT_CASE(test_buffer_no_overflow), + KUNIT_CASE(test_dump_stack_no_crash), + KUNIT_CASE(test_sprint_symbol_build_id), + /* Group I: Encoding/decoding and accuracy */ + KUNIT_CASE(test_sleb128_edge_cases), + KUNIT_CASE(test_uleb128_edge_cases), + KUNIT_CASE(test_line_number_accuracy), + KUNIT_CASE(test_many_lines_mid_function), + {} +}; + +static struct kunit_suite lineinfo_test_suite =3D { + .name =3D "lineinfo", + .test_cases =3D lineinfo_test_cases, +}; +kunit_test_suites(&lineinfo_test_suite); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("KUnit tests for kallsyms lineinfo"); +MODULE_AUTHOR("Sasha Levin"); --=20 2.51.0