From mboxrd@z Thu Jan 1 00:00:00 1970 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 50C8F3E0245; Mon, 4 May 2026 15:34: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=1777908895; cv=none; b=LF2/e5nSU7Y+/aezqicrFO5S1Ic1KsuwDU9N8yUqgI/Yy7oCmfjQMmArZCXwE7AyWSHnwEcTfh7ADXULliFaxCOcLPyCmcacYWKp9Wthz1eGL6C6drVGPEMZomDujGhOAzvaZw97OSY3S6djhrAkkllgfCl+oZ3/+LVUbtBMlAo= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1777908895; c=relaxed/simple; bh=AMTIyJWX16w7IJUGxsbF61qGVVwm/vyGXeuoJAlmpdY=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=LTz/DvaIUV8JIGOEisSjzYhvvDo7OCStqX+Ojv647zJKVLfhFUkdhFP7ugWWvxA4JrobJRZ2kzf6jZ5bZ6sdQJklBB0s0RGIs2Dnl8Y/2zmuLKVxajFvyN5TSMvrl7zDO6t15HrdiGIbJNNZKQaPYG2y1+R2H1yCKVp+YXAQ2II= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=uKfYGH5b; 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="uKfYGH5b" Received: by smtp.kernel.org (Postfix) with ESMTPSA id AB42AC2BCC4; Mon, 4 May 2026 15:34:48 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1777908895; bh=AMTIyJWX16w7IJUGxsbF61qGVVwm/vyGXeuoJAlmpdY=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=uKfYGH5bwR0HKaVn8/San1kDARwn393TU+1bivwQGlbuQKqIp+UzSyOgGGPe85Dwl yitr/zdspapZfmjBm4qL2bcrRxWFAD31HnJO5y5oKSWqKd1w5z6CphgrWo4rSC/NbN m9EAAuSdxLEMHY/Vqa08HBvs6sgrpcS3xBLHuavQYylHxFGQp+HQxEesyWfbhogmiY NV1MAUr+az3lE86zNOITa1/HKSO3F7VhWEN1QZaQc5wX/Z/n3qvrG9UbPft4KdUaYR /+BfKI9CRGzYEX1T/7ObytZLNUIpYTjSHJva6yniW4I8ch3ftX92GEqWZnogdD383Z rgFewhkKGK6iA== 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 , Zhen Lei , Sami Tolvanen , linux-kernel@vger.kernel.org, linux-kbuild@vger.kernel.org, linux-modules@vger.kernel.org, linux-doc@vger.kernel.org, Sasha Levin Subject: [PATCH v5 2/4] kallsyms: extend lineinfo to loadable modules Date: Mon, 4 May 2026 11:33:58 -0400 Message-ID: <20260504153401.2416391-3-sashal@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260504153401.2416391-1-sashal@kernel.org> References: <20260504153401.2416391-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: 8bit Add CONFIG_KALLSYMS_LINEINFO_MODULES, which extends the CONFIG_KALLSYMS_LINEINFO feature to loadable kernel modules. At build time, each .ko is post-processed by scripts/gen-mod-lineinfo.sh (modeled on gen-btf.sh) which runs scripts/gen_lineinfo --module on the .ko, generates per-section .mod_lineinfo and .init.mod_lineinfo sections containing compact binary tables of section-relative offsets, file IDs, line numbers, and filenames, and embeds them back into the .ko via a partial link (ld -r). At runtime, module_lookup_lineinfo() walks the section descriptors in each blob, finds the one whose runtime range contains the queried address, and binary-searches that section's table. The lookup is NMI/panic-safe (no locks, no allocations) — the data lives in read-only module memory and is freed automatically when the module (or its init memory) is unloaded. The gen_lineinfo tool gains --module mode which: - Walks an allowlist of text-like sections (.text, .exit.text, .init.text), gating each on its presence in the .ko. - Uses an ELF relocation against each covered section's symbol as the runtime "anchor", resolved by the module loader's standard apply_relocations() pass — no implicit base derivation from mod->mem[].base, no special-cased loader logic. - Disambiguates DWARF addresses across sections that all share sh_addr == 0 in ET_REL files via per-section synthetic biases applied inside apply_debug_line_relocations() (handles both abs32 and abs64 width relocs). - Handles libdw's ET_REL path-doubling quirk in make_relative(). - Declares empty section stanzas in its output assembly so the resulting lineinfo.o has LOCAL SECTION symbols rather than GLOBAL UND ones; otherwise ld -r would not bind the relocation to the .ko's existing section symbol of the same name and depmod would warn. The build pipeline runs gen-mod-lineinfo.sh after the existing modfinal step: gen_lineinfo --module ${KO} > ${KO}.lineinfo.S ${CC} -c -o ${KO}.lineinfo.o ${KO}.lineinfo.S ${LD} -r ${KO}.lineinfo.o ${KO} -o ${KO}.tmp && mv ${KO}.tmp ${KO} Order matters: lineinfo.o must come first so its zero-byte text contributions stay at offset 0 of the merged sections. The init blob lives in MOD_INIT_RODATA and is revoked via WRITE_ONCE in do_init_module() before do_free_init() releases the memory; the module_init_lineinfo_data() reader uses READ_ONCE so concurrent lookups either see the old pointer (still valid until do_free_init's synchronize_rcu) or NULL. The struct module fields are guarded by #ifdef CONFIG_KALLSYMS_LINEINFO_MODULES and accessed through inline reader accessors so callers don't duplicate the guard. Per-module overhead is approximately 14 bytes per DWARF line entry plus a small fixed cost per covered section descriptor. The next patch in this series delta-compresses the per-section streams to ~3-4 bytes per entry. Assisted-by: Claude:claude-opus-4-6 Signed-off-by: Sasha Levin --- .../admin-guide/kallsyms-lineinfo.rst | 41 +- MAINTAINERS | 108 ++- include/linux/mod_lineinfo.h | 104 +++ include/linux/module.h | 39 + init/Kconfig | 19 +- kernel/kallsyms.c | 18 +- kernel/module/kallsyms.c | 181 ++++ kernel/module/main.c | 26 + scripts/Makefile.modfinal | 6 + scripts/gen-mod-lineinfo.sh | 50 + scripts/gen_lineinfo.c | 854 ++++++++++++++++-- 11 files changed, 1320 insertions(+), 126 deletions(-) create mode 100644 include/linux/mod_lineinfo.h create mode 100644 scripts/gen-mod-lineinfo.sh diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst index c8ec124394354..dd264830c8d5b 100644 --- a/Documentation/admin-guide/kallsyms-lineinfo.rst +++ b/Documentation/admin-guide/kallsyms-lineinfo.rst @@ -51,22 +51,47 @@ With ``CONFIG_KALLSYMS_LINEINFO``:: Note that assembly routines (such as ``entry_SYSCALL_64_after_hwframe``) are not annotated because they lack DWARF debug information. +Module Support +============== + +``CONFIG_KALLSYMS_LINEINFO_MODULES`` extends the feature to loadable kernel +modules. When enabled, each ``.ko`` is post-processed at build time to embed +a ``.mod_lineinfo`` section containing the same kind of address-to-source +mapping. + +Enable in addition to the base options:: + + CONFIG_MODULES=y + CONFIG_KALLSYMS_LINEINFO_MODULES=y + +Stack traces from module code will then include annotations:: + + my_driver_func+0x30/0x100 [my_driver] (drivers/foo/bar.c:123) + +The ``.mod_lineinfo`` section is loaded into read-only module memory alongside +the module text. No additional runtime memory allocation is required; the data +is freed when the module is unloaded. + Memory Overhead =============== -The lineinfo tables are stored in ``.rodata`` and typically add approximately -44 MiB to the kernel image for a standard configuration (~4.6 million DWARF -line entries, ~10 bytes per entry after deduplication). +The vmlinux lineinfo tables are stored in ``.rodata`` and typically add +approximately 10-15 MiB to the kernel image for a standard configuration +(~4.6 million DWARF line entries, ~2-3 bytes per entry after delta +compression). + +Per-module lineinfo adds approximately 2-3 bytes per DWARF line entry to each +``.ko`` file. Known Limitations ================= -- **vmlinux only**: Only symbols in the core kernel image are annotated. - Module symbols are not covered. -- **4 GiB offset limit**: Address offsets from ``_text`` are stored as 32-bit - values. Entries beyond 4 GiB from ``_text`` are skipped at build time with - a warning. +- **4 GiB offset limit**: Address offsets from ``_text`` (vmlinux) or + ``.text`` base (modules) are stored as 32-bit values. Entries beyond + 4 GiB are skipped at build time with a warning. - **65535 file limit**: Source file IDs are stored as 16-bit values. Builds with more than 65535 unique source files will fail with an error. - **No assembly annotations**: Functions implemented in assembly that lack DWARF ``.debug_line`` data are not annotated. +- **No init text**: For modules, functions in ``.init.text`` are not annotated + because that memory is freed after module initialization. diff --git a/MAINTAINERS b/MAINTAINERS index a683a7023f6e9..f264e763d1041 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -7873,7 +7873,7 @@ F: drivers/gpu/drm/sun4i/sun8i* DRM DRIVER FOR APPLE TOUCH BARS M: Aun-Ali Zaidi -M: Aditya Garg +M: Aditya Garg L: dri-devel@lists.freedesktop.org S: Maintained T: git https://gitlab.freedesktop.org/drm/misc/kernel.git @@ -13797,6 +13797,9 @@ KALLSYMS LINEINFO M: Sasha Levin S: Maintained F: Documentation/admin-guide/kallsyms-lineinfo.rst +F: include/linux/mod_lineinfo.h +F: lib/tests/lineinfo_kunit.c +F: scripts/gen-mod-lineinfo.sh F: scripts/gen_lineinfo.c KASAN @@ -13866,7 +13869,6 @@ M: Pratyush Yadav R: Dave Young L: kexec@lists.infradead.org S: Maintained -W: http://lse.sourceforge.net/kdump/ F: Documentation/admin-guide/kdump/ F: fs/proc/vmcore.c F: include/linux/crash_core.h @@ -15258,7 +15260,7 @@ M: Andrea Cervesato M: Cyril Hrubis M: Jan Stancek M: Petr Vorel -M: Li Wang +M: Li Wang M: Yang Xu M: Xiao Yang L: ltp@lists.linux.it (subscribers-only) @@ -15405,7 +15407,7 @@ F: include/net/netns/mctp.h F: net/mctp/ MAPLE TREE -M: Liam R. Howlett +M: Liam R. Howlett R: Alice Ryhl R: Andrew Ballance L: maple-tree@lists.infradead.org @@ -16765,7 +16767,7 @@ MEMORY MANAGEMENT - CORE M: Andrew Morton M: David Hildenbrand R: Lorenzo Stoakes -R: Liam R. Howlett +R: Liam R. Howlett R: Vlastimil Babka R: Mike Rapoport R: Suren Baghdasaryan @@ -16811,7 +16813,7 @@ F: mm/sparse.c F: mm/util.c F: mm/vmpressure.c F: mm/vmstat.c -N: include/linux/page[-_]* +N: include\/linux\/page[-_][a-zA-Z]* MEMORY MANAGEMENT - EXECMEM M: Andrew Morton @@ -16901,7 +16903,7 @@ MEMORY MANAGEMENT - MISC M: Andrew Morton M: David Hildenbrand R: Lorenzo Stoakes -R: Liam R. Howlett +R: Liam R. Howlett R: Vlastimil Babka R: Mike Rapoport R: Suren Baghdasaryan @@ -16968,6 +16970,7 @@ S: Maintained F: include/linux/compaction.h F: include/linux/gfp.h F: include/linux/page-isolation.h +F: include/linux/pageblock-flags.h F: mm/compaction.c F: mm/debug_page_alloc.c F: mm/debug_page_ref.c @@ -16989,7 +16992,7 @@ M: Andrew Morton M: Johannes Weiner R: David Hildenbrand R: Michal Hocko -R: Qi Zheng +R: Qi Zheng R: Shakeel Butt R: Lorenzo Stoakes L: linux-mm@kvack.org @@ -17002,7 +17005,7 @@ M: Andrew Morton M: David Hildenbrand M: Lorenzo Stoakes R: Rik van Riel -R: Liam R. Howlett +R: Liam R. Howlett R: Vlastimil Babka R: Harry Yoo R: Jann Horn @@ -17049,7 +17052,7 @@ M: David Hildenbrand M: Lorenzo Stoakes R: Zi Yan R: Baolin Wang -R: Liam R. Howlett +R: Liam R. Howlett R: Nico Pache R: Ryan Roberts R: Dev Jain @@ -17087,7 +17090,7 @@ F: tools/testing/selftests/mm/uffd-*.[ch] MEMORY MANAGEMENT - RUST M: Alice Ryhl R: Lorenzo Stoakes -R: Liam R. Howlett +R: Liam R. Howlett L: linux-mm@kvack.org L: rust-for-linux@vger.kernel.org S: Maintained @@ -17101,7 +17104,7 @@ F: rust/kernel/page.rs MEMORY MAPPING M: Andrew Morton -M: Liam R. Howlett +M: Liam R. Howlett M: Lorenzo Stoakes R: Vlastimil Babka R: Jann Horn @@ -17133,7 +17136,7 @@ F: tools/testing/vma/ MEMORY MAPPING - LOCKING M: Andrew Morton M: Suren Baghdasaryan -M: Liam R. Howlett +M: Liam R. Howlett M: Lorenzo Stoakes R: Vlastimil Babka R: Shakeel Butt @@ -17148,7 +17151,7 @@ F: mm/mmap_lock.c MEMORY MAPPING - MADVISE (MEMORY ADVICE) M: Andrew Morton -M: Liam R. Howlett +M: Liam R. Howlett M: Lorenzo Stoakes M: David Hildenbrand R: Vlastimil Babka @@ -18678,19 +18681,59 @@ F: net/xfrm/ F: tools/testing/selftests/net/ipsec.c NETWORKING [IPv4/IPv6] -M: "David S. Miller" M: David Ahern +M: Ido Schimmel L: netdev@vger.kernel.org S: Maintained -T: git git://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git -F: arch/x86/net/* -F: include/linux/ip.h -F: include/linux/ipv6* +F: Documentation/netlink/specs/rt-addr.yaml +F: Documentation/netlink/specs/rt-neigh.yaml +F: Documentation/netlink/specs/rt-route.yaml +F: Documentation/netlink/specs/rt-rule.yaml +F: include/linux/inetdevice.h +F: include/linux/mroute* +F: include/net/addrconf.h +F: include/net/arp.h F: include/net/fib* +F: include/net/if_inet6.h +F: include/net/inetpeer.h F: include/net/ip* +F: include/net/lwtunnel.h +F: include/net/ndisc.h +F: include/net/netns/nexthop.h +F: include/net/nexthop.h F: include/net/route.h -F: net/ipv4/ -F: net/ipv6/ +F: include/uapi/linux/fib_rules.h +F: include/uapi/linux/in_route.h +F: include/uapi/linux/mroute* +F: include/uapi/linux/nexthop.h +F: net/core/fib* +F: net/core/lwtunnel.c +F: net/ipv4/arp.c +F: net/ipv4/devinet.c +F: net/ipv4/fib* +F: net/ipv4/icmp.c +F: net/ipv4/igmp.c +F: net/ipv4/inet_fragment.c +F: net/ipv4/inetpeer.c +F: net/ipv4/ip* +F: net/ipv4/metrics.c +F: net/ipv4/netlink.c +F: net/ipv4/nexthop.c +F: net/ipv4/route.c +F: net/ipv6/addr* +F: net/ipv6/anycast.c +F: net/ipv6/exthdrs.c +F: net/ipv6/exthdrs_core.c +F: net/ipv6/fib* +F: net/ipv6/icmp.c +F: net/ipv6/ip* +F: net/ipv6/mcast* +F: net/ipv6/ndisc.c +F: net/ipv6/output_core.c +F: net/ipv6/reassembly.c +F: net/ipv6/route.c +F: tools/testing/selftests/net/fib* +F: tools/testing/selftests/net/forwarding/ NETWORKING [LABELED] (NetLabel, Labeled IPsec, SECMARK) M: Paul Moore @@ -18825,18 +18868,11 @@ F: Documentation/networking/net_failover.rst F: drivers/net/net_failover.c F: include/net/net_failover.h -NEXTHOP -M: David Ahern -L: netdev@vger.kernel.org -S: Maintained -F: include/net/netns/nexthop.h -F: include/net/nexthop.h -F: include/uapi/linux/nexthop.h -F: net/ipv4/nexthop.c - NFC SUBSYSTEM -L: netdev@vger.kernel.org -S: Orphan +M: David Heidelberg +L: oe-linux-nfc@lists.linux.dev +S: Maintained +T: git https://codeberg.org/linux-nfc/linux.git F: Documentation/devicetree/bindings/net/nfc/ F: drivers/nfc/ F: include/net/nfc/ @@ -20780,6 +20816,7 @@ M: Dominik Brodowski S: Odd Fixes T: git git://git.kernel.org/pub/scm/linux/kernel/git/brodo/linux.git F: Documentation/pcmcia/ +F: drivers/net/ethernet/8390/pcnet_cs.c F: drivers/pcmcia/ F: include/pcmcia/ F: tools/pcmcia/ @@ -23375,7 +23412,7 @@ RUST [ALLOC] M: Danilo Krummrich R: Lorenzo Stoakes R: Vlastimil Babka -R: Liam R. Howlett +R: Liam R. Howlett R: Uladzislau Rezki L: rust-for-linux@vger.kernel.org S: Maintained @@ -23527,7 +23564,7 @@ F: drivers/s390/net/ S390 PCI SUBSYSTEM M: Niklas Schnelle -M: Gerald Schaefer +M: Gerd Bayer L: linux-s390@vger.kernel.org S: Supported F: Documentation/arch/s390/pci.rst @@ -24320,7 +24357,7 @@ F: include/media/i2c/rj54n1cb0c.h SHRINKER M: Andrew Morton M: Dave Chinner -R: Qi Zheng +R: Qi Zheng R: Roman Gushchin R: Muchun Song L: linux-mm@kvack.org @@ -24770,6 +24807,7 @@ SOFTWARE RAID (Multiple Disks) SUPPORT M: Song Liu M: Yu Kuai R: Li Nan +R: Xiao Ni L: linux-raid@vger.kernel.org S: Supported Q: https://patchwork.kernel.org/project/linux-raid/list/ diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h new file mode 100644 index 0000000000000..9cda3263a0784 --- /dev/null +++ b/include/linux/mod_lineinfo.h @@ -0,0 +1,104 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * mod_lineinfo.h - Binary format for per-module source line information + * + * This header defines the layout of the .mod_lineinfo and + * .init.mod_lineinfo sections embedded in loadable kernel modules. It + * is dual-use: included from both the kernel and the userspace + * gen_lineinfo tool. + * + * Top-level layout (all values in target-native endianness): + * + * struct mod_lineinfo_root + * struct mod_lineinfo_section sections[hdr.num_sections] + * ... per-section sub-tables, each pointed at by sections[i].table_offset + * + * Each mod_lineinfo_section descriptor identifies one ELF text section + * covered by the lineinfo blob. Its .anchor field is an ELF relocation + * resolved at module-load time to the runtime base of the named section, + * eliminating the need to derive the base from mod->mem[].base segments. + * If the relocation fails to resolve (e.g. unknown reloc type), .anchor + * stays zero and lookups silently degrade to "no annotation". + * + * Each per-section sub-table is laid out as a stand-alone + * mod_lineinfo_header followed by parallel arrays: + * + * struct mod_lineinfo_header (16 bytes) + * u32 addrs[num_entries] -- offsets from this section's base, sorted + * u16 file_ids[num_entries] -- parallel to addrs + * <2-byte pad if num_entries is odd> + * u32 lines[num_entries] -- parallel to addrs + * u32 file_offsets[num_files] -- byte offset into filenames[] + * char filenames[filenames_size] -- concatenated NUL-terminated strings + */ +#ifndef _LINUX_MOD_LINEINFO_H +#define _LINUX_MOD_LINEINFO_H + +#ifdef __KERNEL__ +#include +#else +#include +typedef uint32_t u32; +typedef uint16_t u16; +typedef uint64_t u64; +#endif + +/* + * Per-section descriptor. One entry per ELF text section covered by the + * blob (.text, .exit.text, .init.text, ...). + */ +struct mod_lineinfo_section { + u64 anchor; /* RELOC: runtime base of covered section, or 0 */ + u32 size; /* covered section size in bytes */ + u32 table_offset; /* byte offset from blob start to this section's + * mod_lineinfo_header */ +}; + +/* + * Top-level header. Sits at offset 0 of every .mod_lineinfo / + * .init.mod_lineinfo section. The compiler inserts 4 bytes of trailing + * padding so the u64 anchor in sections[0] starts 8-byte aligned. + */ +struct mod_lineinfo_root { + u32 num_sections; + struct mod_lineinfo_section sections[]; +}; + +struct mod_lineinfo_header { + u32 num_entries; + u32 num_files; + u32 filenames_size; /* total bytes of concatenated filenames */ +}; + +/* Offset helpers: compute byte offset from the per-section header to each array. */ + +static inline u32 mod_lineinfo_addrs_off(void) +{ + return sizeof(struct mod_lineinfo_header); +} + +static inline u32 mod_lineinfo_file_ids_off(u32 num_entries) +{ + return mod_lineinfo_addrs_off() + num_entries * sizeof(u32); +} + +static inline u32 mod_lineinfo_lines_off(u32 num_entries) +{ + /* u16 file_ids[] may need 2-byte padding to align lines[] to 4 bytes */ + u32 off = mod_lineinfo_file_ids_off(num_entries) + + num_entries * sizeof(u16); + return (off + 3) & ~3u; +} + +static inline u32 mod_lineinfo_file_offsets_off(u32 num_entries) +{ + return mod_lineinfo_lines_off(num_entries) + num_entries * sizeof(u32); +} + +static inline u32 mod_lineinfo_filenames_off(u32 num_entries, u32 num_files) +{ + return mod_lineinfo_file_offsets_off(num_entries) + + num_files * sizeof(u32); +} + +#endif /* _LINUX_MOD_LINEINFO_H */ diff --git a/include/linux/module.h b/include/linux/module.h index 7566815fabbe8..2bc0263b086d2 100644 --- a/include/linux/module.h +++ b/include/linux/module.h @@ -507,6 +507,12 @@ struct module { void *btf_data; void *btf_base_data; #endif +#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES + void *lineinfo_data; /* .mod_lineinfo section in MOD_RODATA */ + unsigned int lineinfo_data_size; + void *init_lineinfo_data; /* .init.mod_lineinfo, NULL after init runs */ + unsigned int init_lineinfo_data_size; +#endif #ifdef CONFIG_JUMP_LABEL struct jump_entry *jump_entries; unsigned int num_jump_entries; @@ -1020,6 +1026,39 @@ static inline unsigned long find_kallsyms_symbol_value(struct module *mod, #endif /* CONFIG_MODULES && CONFIG_KALLSYMS */ +bool module_lookup_lineinfo(struct module *mod, unsigned long addr, + const char **file, unsigned int *line); + +/* + * Reader accessors so callers don't need to duplicate the + * CONFIG_KALLSYMS_LINEINFO_MODULES guard around mod->lineinfo_data / + * mod->init_lineinfo_data field access. Setters/clearers in the loader + * use the field directly under a matching #ifdef. + */ +static inline void *module_lineinfo_data(const struct module *mod, + unsigned int *size) +{ +#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES + *size = mod->lineinfo_data_size; + return mod->lineinfo_data; +#else + *size = 0; + return NULL; +#endif +} + +static inline void *module_init_lineinfo_data(const struct module *mod, + unsigned int *size) +{ +#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES + *size = READ_ONCE(mod->init_lineinfo_data_size); + return READ_ONCE(mod->init_lineinfo_data); +#else + *size = 0; + return NULL; +#endif +} + /* Define __free(module_put) macro for struct module *. */ DEFINE_FREE(module_put, struct module *, if (_T) module_put(_T)) diff --git a/init/Kconfig b/init/Kconfig index 99e78c253056a..3e3acfc37be7e 100644 --- a/init/Kconfig +++ b/init/Kconfig @@ -2085,8 +2085,23 @@ config KALLSYMS_LINEINFO anon_vma_clone+0x2ed/0xcf0 (mm/rmap.c:412) This requires elfutils (libdw-dev/elfutils-devel) on the build host. - Adds approximately 44MB to a typical kernel image (10 bytes per - DWARF line-table entry, ~4.6M entries for a typical config). + Adds approximately 10-15MB to a typical kernel image (~2-3 bytes + per entry after delta compression, ~4.6M entries for a typical + config). + + If unsure, say N. + +config KALLSYMS_LINEINFO_MODULES + bool "Embed source file:line information in module stack traces" + depends on KALLSYMS_LINEINFO && MODULES + help + Extends KALLSYMS_LINEINFO to loadable kernel modules. Each .ko + gets a lineinfo table generated from its DWARF data at build time, + so stack traces from module code include (file.c:123) annotations. + + Requires elfutils (libdw-dev/elfutils-devel) on the build host. + Increases .ko sizes by approximately 2-3 bytes per DWARF line + entry after delta compression. If unsure, say N. diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c index 1e3f527b13988..d95387f51b4c0 100644 --- a/kernel/kallsyms.c +++ b/kernel/kallsyms.c @@ -551,12 +551,24 @@ static int __sprint_symbol(char *buffer, unsigned long address, * replaced with bar()"); appending lineinfo there would produce a * confusing "foo (file:line)()". */ - if (add_lineinfo && IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) && !modname) { + if (add_lineinfo && IS_ENABLED(CONFIG_KALLSYMS_LINEINFO)) { const char *li_file; unsigned int li_line; + bool found = false; + + if (!modname) { + found = kallsyms_lookup_lineinfo(address, + &li_file, &li_line); + } else if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES)) { + struct module *mod = __module_address(address); + + if (mod) + found = module_lookup_lineinfo(mod, address, + &li_file, + &li_line); + } - if (kallsyms_lookup_lineinfo(address, - &li_file, &li_line)) + if (found) len += snprintf(buffer + len, KSYM_SYMBOL_LEN - len, " (%s:%u)", li_file, li_line); } diff --git a/kernel/module/kallsyms.c b/kernel/module/kallsyms.c index 0fc11e45df9b9..819d6594c2937 100644 --- a/kernel/module/kallsyms.c +++ b/kernel/module/kallsyms.c @@ -494,3 +494,184 @@ int module_kallsyms_on_each_symbol(const char *modname, mutex_unlock(&module_mutex); return ret; } + +#include + +/* + * Search one per-section sub-table for @section_offset using flat parallel + * arrays. @hdr is the per-section header at byte offset @hdr_offset within + * @blob. Returns true on hit and populates @file / @line. + */ +static bool module_lookup_lineinfo_section(const void *blob, u32 blob_size, + u32 hdr_offset, + unsigned int section_offset, + const char **file, + unsigned int *line) +{ + const struct mod_lineinfo_header *hdr; + const u8 *base; + const u32 *addrs, *lines, *file_offsets; + const u16 *file_ids; + const char *filenames; + u32 num_entries, num_files, filenames_size; + unsigned int low, high, mid; + u16 file_id; + + if (hdr_offset > blob_size || + blob_size - hdr_offset < sizeof(*hdr)) + return false; + + base = (const u8 *)blob + hdr_offset; + hdr = (const struct mod_lineinfo_header *)base; + num_entries = hdr->num_entries; + num_files = hdr->num_files; + filenames_size = hdr->filenames_size; + + if (num_entries == 0) + return false; + + /* + * Validate counts before multiplying — sizing arithmetic could + * otherwise overflow on 32-bit with a malformed blob. Each entry + * contributes one u32 (addrs), one u16 (file_ids), and one u32 + * (lines); each file contributes one u32 (file_offsets). + */ + { + u32 avail = blob_size - hdr_offset; + u32 needed = mod_lineinfo_filenames_off(num_entries, num_files); + + if (num_entries > U32_MAX / sizeof(u32)) + return false; + if (num_files > U32_MAX / sizeof(u32)) + return false; + if (needed > avail || filenames_size > avail - needed) + return false; + } + + /* + * Filenames are read as NUL-terminated C strings. Require the blob + * to end in NUL so a malformed file_offsets entry can never lead the + * later "%s" consumer past the end of the section. + */ + if (filenames_size == 0 || + base[mod_lineinfo_filenames_off(num_entries, num_files) + + filenames_size - 1] != 0) + return false; + + addrs = (const u32 *)(base + mod_lineinfo_addrs_off()); + file_ids = (const u16 *)(base + mod_lineinfo_file_ids_off(num_entries)); + lines = (const u32 *)(base + mod_lineinfo_lines_off(num_entries)); + file_offsets = (const u32 *)(base + mod_lineinfo_file_offsets_off(num_entries)); + filenames = (const char *)(base + mod_lineinfo_filenames_off(num_entries, num_files)); + + /* Binary search for largest entry <= section_offset. */ + low = 0; + high = num_entries; + while (low < high) { + mid = low + (high - low) / 2; + if (addrs[mid] <= section_offset) + low = mid + 1; + else + high = mid; + } + + if (low == 0) + return false; + low--; + + file_id = file_ids[low]; + if (file_id >= num_files) + return false; + if (file_offsets[file_id] >= filenames_size) + return false; + + *file = &filenames[file_offsets[file_id]]; + *line = lines[low]; + return true; +} + +/* + * Walk a single .mod_lineinfo / .init.mod_lineinfo blob, find the section + * descriptor whose [anchor, anchor+size) range contains @addr, then search + * that section's sub-table. + */ +static bool module_lookup_lineinfo_blob(const void *blob, u32 blob_size, + unsigned long addr, + const char **file, unsigned int *line) +{ + const struct mod_lineinfo_root *root; + u32 i, sections_end; + + if (!blob || blob_size < sizeof(*root)) + return false; + + root = blob; + if (root->num_sections == 0) + return false; + + if (root->num_sections > U32_MAX / sizeof(struct mod_lineinfo_section)) + return false; + sections_end = sizeof(*root) + + root->num_sections * sizeof(struct mod_lineinfo_section); + if (sections_end > blob_size) + return false; + + for (i = 0; i < root->num_sections; i++) { + const struct mod_lineinfo_section *s = &root->sections[i]; + unsigned long base = (unsigned long)s->anchor; + unsigned long offset; + + if (!base) + continue; /* relocation didn't resolve */ + if (addr < base) + continue; + offset = addr - base; + if (offset >= s->size) + continue; + if (offset > U32_MAX) + continue; + + return module_lookup_lineinfo_section(blob, blob_size, + s->table_offset, + (unsigned int)offset, + file, line); + } + + return false; +} + +/* + * Look up source file:line for an address within a loaded module. + * + * Safe in NMI/panic context: no locks, no allocations. + * Caller must hold RCU read lock (or be in a context where the module + * cannot be unloaded). + */ +bool module_lookup_lineinfo(struct module *mod, unsigned long addr, + const char **file, unsigned int *line) +{ + const void *blob; + unsigned int size; + + if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES)) + return false; + + blob = module_lineinfo_data(mod, &size); + if (blob && module_lookup_lineinfo_blob(blob, size, addr, file, line)) + return true; + + /* + * The init blob lives in MOD_INIT_RODATA and is revoked by + * do_init_module() before do_free_init() releases the memory. The + * READ_ONCE inside module_init_lineinfo_data() pairs with the + * WRITE_ONCE in do_init_module so we never see a partial + * pointer/size pair, and an RCU grace period in do_free_init() + * guarantees the memory still exists for the duration of any lookup + * that captured the pointer before the revocation. + */ + blob = module_init_lineinfo_data(mod, &size); + if (blob && module_lookup_lineinfo_blob(blob, size, addr, file, line)) + return true; + + return false; +} diff --git a/kernel/module/main.c b/kernel/module/main.c index 46dd8d25a6058..46bb2bf799d1e 100644 --- a/kernel/module/main.c +++ b/kernel/module/main.c @@ -2712,6 +2712,19 @@ static int find_module_sections(struct module *mod, struct load_info *info) mod->btf_base_data = any_section_objs(info, ".BTF.base", 1, &mod->btf_base_data_size); #endif +#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES + /* + * Use section_objs() (not any_section_objs) — both blobs carry an + * ELF anchor relocation that the module loader resolves via its + * standard apply_relocations() pass, which only walks SHF_ALLOC + * sections. Picking up a non-ALLOC section here would also leave + * the pointer dangling into the temporary load image once freed. + */ + mod->lineinfo_data = section_objs(info, ".mod_lineinfo", 1, + &mod->lineinfo_data_size); + mod->init_lineinfo_data = section_objs(info, ".init.mod_lineinfo", 1, + &mod->init_lineinfo_data_size); +#endif #ifdef CONFIG_JUMP_LABEL mod->jump_entries = section_objs(info, "__jump_table", sizeof(*mod->jump_entries), @@ -3165,6 +3178,19 @@ static noinline int do_init_module(struct module *mod) /* .BTF is not SHF_ALLOC and will get removed, so sanitize pointers */ mod->btf_data = NULL; mod->btf_base_data = NULL; +#endif +#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES + /* + * .init.mod_lineinfo lives in MOD_INIT_RODATA which do_free_init() is + * about to release. Clear the pointer so concurrent stack-trace + * lookups stop dereferencing it; do_free_init()'s synchronize_rcu() + * then waits out any reader that already captured the old pointer. + * WRITE_ONCE pairs with the READ_ONCE inside module_init_lineinfo_data() + * so the compiler can't tear or reorder the revocation across the + * llist_add() that follows. + */ + WRITE_ONCE(mod->init_lineinfo_data, NULL); + WRITE_ONCE(mod->init_lineinfo_data_size, 0); #endif /* * We want to free module_init, but be aware that kallsyms may be diff --git a/scripts/Makefile.modfinal b/scripts/Makefile.modfinal index adcbcde16a071..3941cf624526b 100644 --- a/scripts/Makefile.modfinal +++ b/scripts/Makefile.modfinal @@ -46,6 +46,9 @@ quiet_cmd_btf_ko = BTF [M] $@ $(CONFIG_SHELL) $(srctree)/scripts/gen-btf.sh --btf_base $(objtree)/vmlinux $@; \ fi; +quiet_cmd_lineinfo_ko = LINEINFO [M] $@ + cmd_lineinfo_ko = $(CONFIG_SHELL) $(srctree)/scripts/gen-mod-lineinfo.sh $@ + # Same as newer-prereqs, but allows to exclude specified extra dependencies newer_prereqs_except = $(filter-out $(PHONY) $(1),$?) @@ -59,6 +62,9 @@ if_changed_except = $(if $(call newer_prereqs_except,$(2))$(cmd-check), \ +$(call if_changed_except,ld_ko_o,$(objtree)/vmlinux) ifdef CONFIG_DEBUG_INFO_BTF_MODULES +$(if $(newer-prereqs),$(call cmd,btf_ko)) +endif +ifdef CONFIG_KALLSYMS_LINEINFO_MODULES + +$(if $(newer-prereqs),$(call cmd,lineinfo_ko)) endif +$(call cmd,check_tracepoint) diff --git a/scripts/gen-mod-lineinfo.sh b/scripts/gen-mod-lineinfo.sh new file mode 100644 index 0000000000000..832d290f3bf4c --- /dev/null +++ b/scripts/gen-mod-lineinfo.sh @@ -0,0 +1,50 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# +# gen-mod-lineinfo.sh - Embed source line info into a kernel module (.ko) +# +# Reads DWARF from the .ko, generates a .mod_lineinfo section that contains +# an ELF relocation against the module's .text section symbol, and partial- +# links the result back into the .ko via "ld -r" so the relocation rides +# along to the module loader. Modeled on scripts/gen-btf.sh. + +set -e + +if [ $# -ne 1 ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +KO="$1" + +cleanup() { + rm -f "${KO}.lineinfo.S" "${KO}.lineinfo.o" "${KO}.lineinfo.tmp" +} +trap cleanup EXIT + +case "${KBUILD_VERBOSE}" in +*1*) + set -x + ;; +esac + +# Generate assembly from DWARF -- if it fails (no DWARF), silently skip +if ! ${objtree}/scripts/gen_lineinfo --module "${KO}" > "${KO}.lineinfo.S"; then + exit 0 +fi + +# Compile assembly to object file +${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \ + ${KBUILD_AFLAGS} ${KBUILD_AFLAGS_MODULE} \ + -c -o "${KO}.lineinfo.o" "${KO}.lineinfo.S" + +# Partial-link lineinfo.o INTO the .ko. Order matters: lineinfo.o must come +# FIRST so its empty .text contributes 0 bytes at offset 0 of the merged +# .text, which keeps the .quad .text relocation (against lineinfo.o's local +# .text symbol, which after merge points at offset 0 of merged .text) +# resolving to the start of the module's .text. Reversing inputs here +# silently breaks lookup correctness. +${LD} -r "${KO}.lineinfo.o" "${KO}" -o "${KO}.lineinfo.tmp" +mv "${KO}.lineinfo.tmp" "${KO}" + +exit 0 diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c index 699e760178f09..e1e08469b4f2f 100644 --- a/scripts/gen_lineinfo.c +++ b/scripts/gen_lineinfo.c @@ -24,16 +24,66 @@ #include #include +#include "../include/linux/mod_lineinfo.h" + +static int module_mode; + static unsigned int skipped_overflow; +/* Target ELF traits, captured once in main() and reused at emit time. */ +static bool target_64bit; +static bool target_le; + /* - * vmlinux mode: end of the invariant .text region. Zero means "no cap" - * (graceful fallback when _etext is absent on some build). + * Vmlinux mode only: address range of the *invariant* .text region. + * See find_text_end_addr() for why we cap on _etext. text_end_addr == 0 + * means "no cap available; capture everything above text_addr" (v3 + * behavior, used as graceful fallback if _etext is absent). */ static unsigned long long text_end_addr; +/* + * In module mode we cover several text-like sections, split across two + * output blobs by lifecycle: + * + * .mod_lineinfo -- persistent code (.text, .exit.text); MOD_RODATA + * .init.mod_lineinfo -- init code (.init.text); freed with init memory + * + * In ET_REL .ko files .text/.init.text/.exit.text all have sh_addr == 0, + * so DWARF line addresses (which become sh_addr + addend after relocation) + * collide across sections. We disambiguate by giving each *present* + * covered section a unique synthetic "bias" — a u32 base address — and + * adding that bias to relocated values inside apply_debug_line_relocations. + * libdw then yields biased addresses that classify_address() can map back + * to a single section unambiguously. The bias is internal to gen_lineinfo + * and never leaks into the emitted blob. + */ +enum mod_lineinfo_blob { + BLOB_PERSISTENT, + BLOB_INIT, + NUM_BLOBS, +}; + +struct covered_section { + const char *name; /* ELF section name (e.g. ".text") */ + enum mod_lineinfo_blob blob; + unsigned long long bias;/* synthetic base address (set in resolve_*) */ + unsigned long long size; + bool present; /* found in this .ko */ + unsigned int sec_index; /* ELF section header index, for reloc matching */ + unsigned int n_entries; /* DWARF line entries collected for this section */ +}; + +static struct covered_section all_sections[] = { + { .name = ".text", .blob = BLOB_PERSISTENT }, + { .name = ".exit.text", .blob = BLOB_PERSISTENT }, + { .name = ".init.text", .blob = BLOB_INIT }, +}; +#define ALL_SECTIONS (sizeof(all_sections) / sizeof(all_sections[0])) + struct line_entry { - unsigned int offset; /* offset from _text */ + unsigned int offset; /* offset from covered section's start */ + unsigned int section_id;/* index into covered_sections[] (module mode only) */ unsigned int file_id; unsigned int line; }; @@ -52,7 +102,12 @@ static struct file_entry *files; static unsigned int num_files; static unsigned int files_capacity; -#define FILE_HASH_BITS 13 +/* + * Hash size must comfortably exceed the 65535-file cap below so the open + * addressing in find_or_add_file() always has a free slot to land on. + * 17 bits = 131072 entries gives ~50% max load factor. + */ +#define FILE_HASH_BITS 17 #define FILE_HASH_SIZE (1 << FILE_HASH_BITS) struct file_hash_entry { @@ -71,8 +126,8 @@ static unsigned int hash_str(const char *s) return h & (FILE_HASH_SIZE - 1); } -static void add_entry(unsigned int offset, unsigned int file_id, - unsigned int line) +static void add_entry(unsigned int offset, unsigned int section_id, + unsigned int file_id, unsigned int line) { if (num_entries >= entries_capacity) { entries_capacity = entries_capacity ? entries_capacity * 2 : 65536; @@ -83,6 +138,7 @@ static void add_entry(unsigned int offset, unsigned int file_id, } } entries[num_entries].offset = offset; + entries[num_entries].section_id = section_id; entries[num_entries].file_id = file_id; entries[num_entries].line = line; num_entries++; @@ -155,27 +211,25 @@ static const char *make_relative(const char *path, const char *comp_dir) { const char *p; - /* If already relative, use as-is */ - if (path[0] != '/') - return path; - - /* comp_dir from DWARF is the most reliable method */ - if (comp_dir) { - size_t len = strlen(comp_dir); - - if (!strncmp(path, comp_dir, len) && path[len] == '/') { - const char *rel = path + len + 1; - - /* - * If comp_dir pointed to a subdirectory - * (e.g. arch/parisc/kernel) rather than - * the tree root, stripping it leaves a - * bare filename. Fall through to the - * kernel_dirs scan so we recover the full - * relative path instead. - */ - if (strchr(rel, '/')) - return rel; + if (path[0] == '/') { + /* Try comp_dir prefix from DWARF */ + if (comp_dir) { + size_t len = strlen(comp_dir); + + if (!strncmp(path, comp_dir, len) && path[len] == '/') { + const char *rel = path + len + 1; + + /* + * If comp_dir pointed to a subdirectory + * (e.g. arch/parisc/kernel) rather than + * the tree root, stripping it leaves a + * bare filename. Fall through to the + * kernel_dirs scan so we recover the full + * relative path instead. + */ + if (strchr(rel, '/')) + return rel; + } } /* @@ -201,9 +255,42 @@ static const char *make_relative(const char *path, const char *comp_dir) return p ? p + 1 : path; } - /* Fall back to basename */ - p = strrchr(path, '/'); - return p ? p + 1 : path; + /* + * Relative path — check for duplicated-path quirk from libdw + * on ET_REL files (e.g., "a/b.c/a/b.c" → "a/b.c"). + */ + { + size_t len = strlen(path); + size_t mid = len / 2; + + if (len > 1 && path[mid] == '/' && + !memcmp(path, path + mid + 1, mid)) + return path + mid + 1; + } + + /* + * Bare filename with no directory component — try to recover the + * relative path using comp_dir. Some toolchains/elfutils combos + * produce bare filenames where comp_dir holds the source directory. + * Construct the absolute path and run the kernel_dirs scan. + */ + if (!strchr(path, '/') && comp_dir && comp_dir[0] == '/') { + static char buf[PATH_MAX]; + + snprintf(buf, sizeof(buf), "%s/%s", comp_dir, path); + for (p = buf + 1; *p; p++) { + if (*(p - 1) == '/') { + for (unsigned int i = 0; i < sizeof(kernel_dirs) / + sizeof(kernel_dirs[0]); i++) { + if (!strncmp(p, kernel_dirs[i], + strlen(kernel_dirs[i]))) + return p; + } + } + } + } + + return path; } static int compare_entries(const void *a, const void *b) @@ -211,6 +298,9 @@ static int compare_entries(const void *a, const void *b) const struct line_entry *ea = a; const struct line_entry *eb = b; + /* Group by section first so each per-section table is contiguous. */ + if (ea->section_id != eb->section_id) + return ea->section_id < eb->section_id ? -1 : 1; if (ea->offset != eb->offset) return ea->offset < eb->offset ? -1 : 1; if (ea->file_id != eb->file_id) @@ -222,7 +312,8 @@ static int compare_entries(const void *a, const void *b) /* * Look up a vmlinux symbol by exact name and return its st_value, or - * @fallback if absent. Aborts when @required and the symbol is missing. + * @fallback if the symbol is absent (lets callers gracefully skip + * optional bounds like _etext). */ static unsigned long long find_vmlinux_sym(Elf *elf, const char *name, unsigned long long fallback, @@ -270,22 +361,325 @@ static unsigned long long find_text_addr(Elf *elf) } /* - * vmlinux is linked in multiple passes: gen_lineinfo runs against - * .tmp_vmlinux1 (which carries an empty lineinfo stub), then real tables - * are linked in for the final image. Sections placed AFTER .rodata - * (.init.text, .exit.text, ...) shift forward as .rodata grows to hold - * the real lineinfo blob, so DWARF addresses we'd capture for them in - * pass 1 would be stale in the final kernel. Cap captured addresses at - * _etext, the symbol that marks the end of .text — placed before .rodata - * in every architecture's vmlinux.lds.S, so its addresses are invariant - * across the relink. Returns 0 if _etext is absent (no cap; v3 behavior). + * Vmlinux is linked in multiple passes: gen_lineinfo runs against + * .tmp_vmlinux1 (which carries the empty lineinfo stub), and the resulting + * tables are then linked into the final vmlinux. Sections placed AFTER + * .rodata (.init.text, .exit.text, ...) shift forward as the real lineinfo + * tables replace the empty stub, so DWARF addresses we'd capture for them + * here are stale by the time the kernel runs. + * + * Cap the captured range at _etext, the symbol that marks the end of the + * .text section. .text is placed BEFORE .rodata in every architecture's + * vmlinux.lds.S, so its addresses are invariant across the relink. + * Returns 0 on architectures or builds that don't expose _etext, in which + * case the cap is disabled (preserving the v3 behavior — addresses past + * .text remain captured but may be off in stack traces). */ static unsigned long long find_text_end_addr(Elf *elf) { return find_vmlinux_sym(elf, "_etext", 0, false); } -static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr) +/* + * Populate @sections[].present/sec_index/size/bias. Sections that don't + * exist stay marked absent. Biases are assigned in array order: each + * present section gets a base equal to the running total of preceding + * present sections' sizes, rounded up to 16 to keep ranges sparse. This + * guarantees [bias, bias+size) ranges are pairwise disjoint and fit in + * u32 as long as the sum of all covered text sizes is below 4 GiB. + */ +static void resolve_covered_sections(Elf *elf, + struct covered_section *sections, + unsigned int num_sections) +{ + Elf_Scn *scn = NULL; + GElf_Shdr shdr; + size_t shstrndx; + unsigned long long cursor = 0; + + if (elf_getshdrstrndx(elf, &shstrndx) != 0) + return; + + while ((scn = elf_nextscn(elf, scn)) != NULL) { + const char *name; + + if (!gelf_getshdr(scn, &shdr)) + continue; + name = elf_strptr(elf, shstrndx, shdr.sh_name); + if (!name) + continue; + for (unsigned int i = 0; i < num_sections; i++) { + if (sections[i].present) + continue; + if (strcmp(name, sections[i].name)) + continue; + if (shdr.sh_size > UINT_MAX) { + fprintf(stderr, + "lineinfo: section %s exceeds 4 GiB (size=%llu); skipping\n", + name, + (unsigned long long)shdr.sh_size); + break; + } + sections[i].sec_index = elf_ndxscn(scn); + sections[i].size = shdr.sh_size; + sections[i].present = true; + break; + } + } + + /* Pack present sections into non-overlapping bias ranges. */ + for (unsigned int i = 0; i < num_sections; i++) { + if (!sections[i].present) + continue; + sections[i].bias = cursor; + cursor += sections[i].size; + cursor = (cursor + 15) & ~15ULL; /* pad for separation */ + } +} + +/* Look up a covered_section by ELF section header index. */ +static struct covered_section *section_by_index(struct covered_section *sections, + unsigned int num_sections, + unsigned int sec_index) +{ + for (unsigned int i = 0; i < num_sections; i++) { + if (sections[i].present && sections[i].sec_index == sec_index) + return §ions[i]; + } + return NULL; +} + +/* + * Apply .rela.debug_line relocations to a mutable copy of .debug_line data. + * + * elfutils libdw (through at least 0.194) does NOT apply relocations for + * ET_REL files when using dwarf_begin_elf(). The internal libdwfl layer + * does this via __libdwfl_relocate(), but that API is not public. + * + * For DWARF5, the .debug_line file name table uses DW_FORM_line_strp + * references into .debug_line_str. Without relocation, all these offsets + * resolve to 0 (or garbage), causing dwarf_linesrc()/dwarf_filesrc() to + * return wrong filenames (typically the comp_dir for every file). + * + * This function applies the relocations manually so that the patched + * .debug_line data can be fed to dwarf_begin_elf() and produce correct + * results. + * + * See elfutils bug https://sourceware.org/bugzilla/show_bug.cgi?id=31447 + * A fix (dwelf_elf_apply_relocs) was proposed but not yet merged as of + * elfutils 0.194: https://sourceware.org/pipermail/elfutils-devel/2024q3/007388.html + */ +/* + * Determine the relocation type for a 32-bit absolute reference + * on the given architecture. Returns 0 if unknown. + */ +static unsigned int r_type_abs32(unsigned int e_machine) +{ + switch (e_machine) { + case EM_X86_64: return R_X86_64_32; + case EM_386: return R_386_32; + case EM_AARCH64: return R_AARCH64_ABS32; + case EM_ARM: return R_ARM_ABS32; + case EM_RISCV: return R_RISCV_32; + case EM_S390: return R_390_32; + case EM_MIPS: return R_MIPS_32; + case EM_PPC64: return R_PPC64_ADDR32; + case EM_PPC: return R_PPC_ADDR32; + case EM_LOONGARCH: return R_LARCH_32; + case EM_PARISC: return R_PARISC_DIR32; + default: return 0; + } +} + +/* + * Determine the relocation type for a 64-bit absolute reference + * on the given architecture. Returns 0 on 32-bit-only architectures + * (where DW_LNE_set_address fits in 32 bits and r_type_abs32 covers it). + */ +static unsigned int r_type_abs64(unsigned int e_machine) +{ + switch (e_machine) { + case EM_X86_64: return R_X86_64_64; + case EM_AARCH64: return R_AARCH64_ABS64; + case EM_RISCV: return R_RISCV_64; + case EM_S390: return R_390_64; + case EM_MIPS: return R_MIPS_64; + case EM_PPC64: return R_PPC64_ADDR64; + case EM_LOONGARCH: return R_LARCH_64; + case EM_PARISC: return R_PARISC_DIR64; + default: return 0; + } +} + +/* + * Write a 4- or 8-byte unsigned integer in target byte order. + * Cross-builds (e.g. x86_64 host -> s390 module) need the patched + * .debug_line bytes laid out per the .ko's e_ident[EI_DATA], not the host's. + */ +static void elf_write_uint(unsigned char *dst, uint64_t value, size_t size, + bool little_endian) +{ + if (little_endian) { + for (size_t i = 0; i < size; i++) + dst[i] = (value >> (i * 8)) & 0xff; + } else { + for (size_t i = 0; i < size; i++) + dst[i] = (value >> ((size - 1 - i) * 8)) & 0xff; + } +} + +static void apply_debug_line_relocations(Elf *elf) +{ + Elf_Scn *scn = NULL; + Elf_Scn *debug_line_scn = NULL; + Elf_Scn *rela_debug_line_scn = NULL; + Elf_Scn *symtab_scn = NULL; + GElf_Shdr shdr; + GElf_Ehdr ehdr; + unsigned int abs32_type, abs64_type; + bool target_le; + size_t shstrndx; + Elf_Data *dl_data, *rela_data, *sym_data; + GElf_Shdr rela_shdr, sym_shdr; + size_t nrels, i; + + if (gelf_getehdr(elf, &ehdr) == NULL) + return; + + abs32_type = r_type_abs32(ehdr.e_machine); + abs64_type = r_type_abs64(ehdr.e_machine); + if (!abs32_type && !abs64_type) + return; + target_le = (ehdr.e_ident[EI_DATA] == ELFDATA2LSB); + + if (elf_getshdrstrndx(elf, &shstrndx) != 0) + return; + + /* Find the relevant sections */ + while ((scn = elf_nextscn(elf, scn)) != NULL) { + const char *name; + + if (!gelf_getshdr(scn, &shdr)) + continue; + name = elf_strptr(elf, shstrndx, shdr.sh_name); + if (!name) + continue; + + if (!strcmp(name, ".debug_line")) + debug_line_scn = scn; + else if (!strcmp(name, ".rela.debug_line")) + rela_debug_line_scn = scn; + else if (shdr.sh_type == SHT_SYMTAB) + symtab_scn = scn; + } + + if (!debug_line_scn || !rela_debug_line_scn || !symtab_scn) + return; + + dl_data = elf_getdata(debug_line_scn, NULL); + rela_data = elf_getdata(rela_debug_line_scn, NULL); + sym_data = elf_getdata(symtab_scn, NULL); + if (!dl_data || !rela_data || !sym_data) + return; + + if (!gelf_getshdr(rela_debug_line_scn, &rela_shdr)) + return; + if (!gelf_getshdr(symtab_scn, &sym_shdr)) + return; + + nrels = rela_shdr.sh_size / rela_shdr.sh_entsize; + + for (i = 0; i < nrels; i++) { + GElf_Rela rela; + GElf_Sym sym; + unsigned int r_type; + size_t r_sym; + bool is_abs64; + + if (!gelf_getrela(rela_data, i, &rela)) + continue; + + r_type = GELF_R_TYPE(rela.r_info); + r_sym = GELF_R_SYM(rela.r_info); + + /* + * Two reloc widths matter for .debug_line: + * abs32 - DW_FORM_line_strp file-table refs into .debug_line_str + * abs64 - DW_LNE_set_address arguments (sequence start PCs) + * Without both, libdw sees zeros and reports wrong filenames or + * collapses every sequence to address 0 (collision after dedup). + */ + if (abs32_type && r_type == abs32_type) { + is_abs64 = false; + } else if (abs64_type && r_type == abs64_type) { + is_abs64 = true; + } else { + continue; + } + + if (!gelf_getsym(sym_data, r_sym, &sym)) + continue; + + size_t width = is_abs64 ? 8 : 4; + uint64_t value = (uint64_t)(sym.st_value + rela.r_addend); + + /* + * If the relocation targets one of our covered text sections, + * fold in that section's synthetic bias so the patched DWARF + * address lands in a unique numeric range. String-ref relocs + * (DW_FORM_line_strp into .debug_line_str) target a different + * section, so the symbol-based check correctly excludes them + * from biasing — for both abs64 (64-bit ELF) and abs32 (32-bit + * ELF, where DW_LNE_set_address is also 4 bytes wide). + */ + if (module_mode) { + struct covered_section *cs; + + cs = section_by_index(all_sections, ALL_SECTIONS, + sym.st_shndx); + if (cs) + value += cs->bias; + } + + if (!is_abs64) + value &= 0xffffffffULL; + + if (rela.r_offset + width <= dl_data->d_size) + elf_write_uint((unsigned char *)dl_data->d_buf + + rela.r_offset, + value, width, target_le); + } +} + +/* + * Decide which covered_section a (biased) DWARF address belongs to. + * apply_debug_line_relocations() has already added the section's bias to + * each line-program PC, so [bias, bias+size) ranges are pairwise disjoint + * and a simple linear scan picks the right bucket. Returns the index + * within @sections, or @num_sections if @addr falls outside every + * present range (caller skips the entry). + */ +static unsigned int classify_address(struct covered_section *sections, + unsigned int num_sections, + unsigned long long addr, + unsigned long long *out_offset) +{ + for (unsigned int i = 0; i < num_sections; i++) { + if (!sections[i].present) + continue; + if (addr < sections[i].bias) + continue; + if (addr >= sections[i].bias + sections[i].size) + continue; + *out_offset = addr - sections[i].bias; + return i; + } + return num_sections; +} + +static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr, + struct covered_section *sections, + unsigned int num_sections) { Dwarf_Off off = 0, next_off; size_t hdr_size; @@ -312,7 +706,8 @@ static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr) Dwarf_Addr addr; const char *src; const char *rel; - unsigned int file_id, loffset; + unsigned int file_id, loffset, sec_id; + unsigned long long sec_off; int lineno; if (!line) @@ -329,56 +724,87 @@ static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr) if (!src) continue; - if (addr < text_addr) - continue; - /* - * Skip addresses past _etext. Sections after .rodata - * shift when the real lineinfo replaces the empty stub - * during the multi-pass vmlinux link, so any address - * we'd capture there would be stale by the time the - * final kernel runs. - */ - if (text_end_addr && addr >= text_end_addr) - continue; - - { - unsigned long long raw_offset = addr - text_addr; + if (module_mode) { + /* + * In ET_REL .ko files .text/.init.text/.exit.text + * all share sh_addr == 0; classify_address picks + * the right bucket from the explicit ranges we + * captured. + */ + sec_id = classify_address(sections, num_sections, + addr, &sec_off); + if (sec_id == num_sections) + continue; + if (sec_off > UINT_MAX) { + skipped_overflow++; + continue; + } + loffset = (unsigned int)sec_off; + sections[sec_id].n_entries++; + } else { + unsigned long long raw_offset; + if (addr < text_addr) + continue; + /* + * Skip addresses past _etext. Sections after + * .rodata shift when the real lineinfo replaces + * the empty stub during the multi-pass vmlinux + * link, so any address we'd capture there would + * be stale by the time the final kernel runs. + */ + if (text_end_addr && addr >= text_end_addr) + continue; + raw_offset = addr - text_addr; if (raw_offset > UINT_MAX) { skipped_overflow++; continue; } loffset = (unsigned int)raw_offset; + sec_id = 0; } rel = make_relative(src, comp_dir); file_id = find_or_add_file(rel); - add_entry(loffset, file_id, (unsigned int)lineno); + add_entry(loffset, sec_id, file_id, (unsigned int)lineno); } next: off = next_off; } } -static void deduplicate(void) +static void deduplicate(struct covered_section *sections, + unsigned int num_sections) { unsigned int i, j; if (num_entries < 2) return; - /* Sort by offset, then file_id, then line for stability */ + /* + * Sort by section_id, then offset, then file_id, line. This groups + * each section's entries contiguously so the per-section emit can + * iterate a simple range, and ensures the binary search invariant + * (offsets ascending) holds within each section. + */ qsort(entries, num_entries, sizeof(*entries), compare_entries); /* - * Remove duplicate entries: - * - Same offset: keep first (deterministic from stable sort keys) - * - Same file:line as previous kept entry: redundant for binary - * search -- any address between them resolves to the earlier one + * Remove duplicates. Reset on a section_id boundary: the same offset + * can legitimately appear in two different sections (they all start + * at sh_addr 0 in ET_REL), and the "same as previous kept entry" + * collapse is only meaningful inside one section's binary-search + * domain. */ j = 0; for (i = 1; i < num_entries; i++) { + if (entries[i].section_id != entries[j].section_id) { + j++; + if (j != i) + entries[j] = entries[i]; + continue; + } if (entries[i].offset == entries[j].offset) continue; if (entries[i].file_id == entries[j].file_id && @@ -389,6 +815,14 @@ static void deduplicate(void) entries[j] = entries[i]; } num_entries = j + 1; + + /* Recompute per-section n_entries from the deduped array. */ + if (sections) { + for (unsigned int k = 0; k < num_sections; k++) + sections[k].n_entries = 0; + for (i = 0; i < num_entries; i++) + sections[entries[i].section_id].n_entries++; + } } static void compute_file_offsets(void) @@ -486,6 +920,199 @@ static void output_assembly(void) printf("\n"); } +/* + * Emit one per-section table in the simple flat-array layout: + * + * mod_lineinfo_header + * addrs[count] (u32, sorted) + * file_ids[count] (u16) + 2-byte pad if count is odd + * lines[count] (u32) + * file_offsets[] (u32) + * filenames[] + * + * @suffix uniquifies labels so multiple tables can coexist in one blob. + * Caller has sorted entries[] so this section's entries occupy [first, + * first + count). + */ +static void emit_section_table(unsigned int first, unsigned int count, + const char *suffix) +{ + printf(".Lhdr%s:\n", suffix); + printf("\t.balign 4\n"); + printf("\t.long %u\t\t/* num_entries */\n", count); + printf("\t.long %u\t\t/* num_files */\n", num_files); + printf("\t.long .Lfilenames_end%s - .Lfilenames%s\n\n", suffix, suffix); + + /* addrs[] */ + for (unsigned int i = 0; i < count; i++) + printf("\t.long 0x%x\n", entries[first + i].offset); + + /* file_ids[] */ + for (unsigned int i = 0; i < count; i++) + printf("\t.short %u\n", entries[first + i].file_id); + if (count & 1) + printf("\t.short 0\t\t/* pad to align lines[] */\n"); + + /* lines[] */ + for (unsigned int i = 0; i < count; i++) + printf("\t.long %u\n", entries[first + i].line); + + /* file_offsets[] */ + printf("\t.balign 4\n"); + for (unsigned int i = 0; i < num_files; i++) + printf("\t.long %u\n", files[i].str_offset); + + /* filenames[] */ + printf(".Lfilenames%s:\n", suffix); + for (unsigned int i = 0; i < num_files; i++) + print_escaped_asciz(files[i].name); + printf(".Lfilenames_end%s:\n", suffix); +} + +/* + * Emit one mod_lineinfo_section descriptor. The "anchor" field is a + * relocation against the named ELF section symbol; the module loader + * resolves it on load to the runtime base of that section. + * + * On 64-bit ELF: 8-byte slot via .quad (R_*_64 reloc). + * On 32-bit ELF: 4-byte reloc via .long , plus 4 bytes of zero + * padding. The two halves are ordered to match target endianness so a + * naive u64 read on the kernel side recovers the relocated value. + */ +static void emit_section_descriptor(const char *section_name, + unsigned long long size, + const char *table_label, + const char *root_label) +{ + if (target_64bit) { + printf("\t.quad %s\t/* sections[].anchor (RELOC) */\n", + section_name); + } else if (target_le) { + printf("\t.long %s\t/* sections[].anchor low (RELOC) */\n", + section_name); + printf("\t.long 0\t\t/* sections[].anchor high pad */\n"); + } else { + printf("\t.long 0\t\t/* sections[].anchor high pad */\n"); + printf("\t.long %s\t/* sections[].anchor low (RELOC) */\n", + section_name); + } + printf("\t.long %llu\t/* sections[].size */\n", size); + printf("\t.long %s - %s\t/* sections[].table_offset */\n", + table_label, root_label); +} + +/* + * Emit one .mod_lineinfo / .init.mod_lineinfo blob. Walks all_sections[] + * picking only entries that (a) belong to the requested blob and (b) + * actually produced at least one DWARF line entry — sections present in + * the .ko but without DWARF (e.g. compiler-generated stub thunks) are + * silently skipped. The caller-supplied entries[] is already sorted by + * section_id, so each section's entries are contiguous; we walk the + * master array in order to compute per-section starting indices. + */ +static void emit_blob(const char *output_section, + const char *blob_tag, + enum mod_lineinfo_blob blob) +{ + unsigned int active = 0; + unsigned int section_starts[ALL_SECTIONS]; + unsigned int cursor = 0; + + for (unsigned int i = 0; i < ALL_SECTIONS; i++) { + section_starts[i] = cursor; + cursor += all_sections[i].n_entries; + if (all_sections[i].blob == blob && all_sections[i].n_entries) + active++; + } + + if (!active) + return; + + printf("\t.section %s, \"a\"\n\n", output_section); + + printf("\t.balign 8\n"); + printf(".Lroot_%s:\n", blob_tag); + printf("\t.long %u\t\t/* num_sections */\n", active); + /* Pad to align the u64 anchor in sections[0] to 8 bytes. */ + printf("\t.balign 8\n"); + + { + unsigned int slot = 0; + for (unsigned int i = 0; i < ALL_SECTIONS; i++) { + char table_label[64]; + char root_label[64]; + + if (all_sections[i].blob != blob) + continue; + if (!all_sections[i].n_entries) + continue; + snprintf(table_label, sizeof(table_label), + ".Lhdr_%s_%u", blob_tag, slot); + snprintf(root_label, sizeof(root_label), + ".Lroot_%s", blob_tag); + emit_section_descriptor(all_sections[i].name, + all_sections[i].size, + table_label, root_label); + slot++; + } + } + printf("\n"); + + { + unsigned int slot = 0; + + for (unsigned int i = 0; i < ALL_SECTIONS; i++) { + char suffix[64]; + + if (all_sections[i].blob != blob) + continue; + if (!all_sections[i].n_entries) + continue; + snprintf(suffix, sizeof(suffix), "_%s_%u", + blob_tag, slot); + emit_section_table(section_starts[i], + all_sections[i].n_entries, + suffix); + slot++; + } + } + printf("\n"); +} + +/* + * Declare each text-like section we plan to reference as an empty + * SHF_EXECINSTR section in this object. Without these stanzas the + * assembler treats `.quad .exit.text` as an undefined external symbol; + * after ld -r the resulting GLOBAL UND `.exit.text` doesn't bind to the + * .ko's LOCAL SECTION symbol of the same name, leaving depmod with an + * unresolved-symbol warning and the loader unable to relocate the anchor. + * + * Declaring the section here gives lineinfo.o its own local SECTION + * symbol; ld -r merges sections by name so the local symbol simply + * relocates to offset 0 of the merged section (lineinfo.o is linked + * FIRST so its zero-byte contribution stays at the start). + */ +static void declare_empty_text_sections(void) +{ + for (unsigned int i = 0; i < ALL_SECTIONS; i++) { + if (!all_sections[i].present) + continue; + printf("\t.section %s, \"ax\"\n", all_sections[i].name); + } + printf("\n"); +} + +static void output_module_assembly(void) +{ + printf("/* SPDX-License-Identifier: GPL-2.0 */\n"); + printf("/*\n"); + printf(" * Automatically generated by scripts/gen_lineinfo --module\n"); + printf(" * Do not edit.\n"); + printf(" */\n\n"); + + declare_empty_text_sections(); +} + int main(int argc, char *argv[]) { int fd; @@ -493,12 +1120,23 @@ int main(int argc, char *argv[]) Dwarf *dwarf; unsigned long long text_addr; + if (argc >= 2 && !strcmp(argv[1], "--module")) { + module_mode = 1; + argv++; + argc--; + } + if (argc != 2) { - fprintf(stderr, "Usage: %s \n", argv[0]); + fprintf(stderr, "Usage: %s [--module] \n", argv[0]); return 1; } - fd = open(argv[1], O_RDONLY); + /* + * For module mode, open O_RDWR so we can apply debug section + * relocations to the in-memory ELF data. The modifications + * are NOT written back to disk (no elf_update() call). + */ + fd = open(argv[1], module_mode ? O_RDWR : O_RDONLY); if (fd < 0) { fprintf(stderr, "Cannot open %s: %s\n", argv[1], strerror(errno)); @@ -506,7 +1144,7 @@ int main(int argc, char *argv[]) } elf_version(EV_CURRENT); - elf = elf_begin(fd, ELF_C_READ, NULL); + elf = elf_begin(fd, module_mode ? ELF_C_RDWR : ELF_C_READ, NULL); if (!elf) { fprintf(stderr, "elf_begin failed: %s\n", elf_errmsg(elf_errno())); @@ -514,8 +1152,34 @@ int main(int argc, char *argv[]) return 1; } - text_addr = find_text_addr(elf); - text_end_addr = find_text_end_addr(elf); + { + GElf_Ehdr ehdr; + + if (gelf_getehdr(elf, &ehdr) == NULL) { + fprintf(stderr, "gelf_getehdr failed\n"); + elf_end(elf); + close(fd); + return 1; + } + target_64bit = (ehdr.e_ident[EI_CLASS] == ELFCLASS64); + target_le = (ehdr.e_ident[EI_DATA] == ELFDATA2LSB); + } + + if (module_mode) { + /* + * .ko files are ET_REL after ld -r. Resolve covered text + * sections FIRST so apply_debug_line_relocations() can use + * the assigned biases when patching line-program addresses; + * libdw does NOT apply relocations for ET_REL files, so we + * also handle DW_FORM_line_strp refs into .debug_line_str. + */ + resolve_covered_sections(elf, all_sections, ALL_SECTIONS); + apply_debug_line_relocations(elf); + text_addr = 0; /* unused in module mode */ + } else { + text_addr = find_text_addr(elf); + text_end_addr = find_text_end_addr(elf); + } dwarf = dwarf_begin_elf(elf, DWARF_C_READ, NULL); if (!dwarf) { @@ -528,20 +1192,55 @@ int main(int argc, char *argv[]) return 1; } - process_dwarf(dwarf, text_addr); + if (module_mode) { + unsigned int persistent_total, init_total; + + output_module_assembly(); /* file header only */ - if (skipped_overflow) + /* + * Single DWARF pass classifies every line entry into its + * covering section (or skips it). Each entry is tagged with + * the master-array section_id so per-blob emit can filter. + */ + process_dwarf(dwarf, 0, all_sections, ALL_SECTIONS); + deduplicate(all_sections, ALL_SECTIONS); + compute_file_offsets(); + + emit_blob(".mod_lineinfo", "p", BLOB_PERSISTENT); + emit_blob(".init.mod_lineinfo", "i", BLOB_INIT); + + persistent_total = 0; + init_total = 0; + for (unsigned int i = 0; i < ALL_SECTIONS; i++) { + if (all_sections[i].blob == BLOB_PERSISTENT) + persistent_total += all_sections[i].n_entries; + else if (all_sections[i].blob == BLOB_INIT) + init_total += all_sections[i].n_entries; + } fprintf(stderr, - "lineinfo: warning: %u entries skipped (offset > 4 GiB from _text)\n", - skipped_overflow); + "lineinfo: persistent %u entries, init %u entries, %u files\n", + persistent_total, init_total, num_files); + + if (skipped_overflow) + fprintf(stderr, + "lineinfo: warning: %u entries skipped (offset > 4 GiB)\n", + skipped_overflow); + } else { + process_dwarf(dwarf, text_addr, NULL, 0); - deduplicate(); - compute_file_offsets(); + if (skipped_overflow) + fprintf(stderr, + "lineinfo: warning: %u entries skipped (offset > 4 GiB from _text)\n", + skipped_overflow); - fprintf(stderr, "lineinfo: %u entries, %u files\n", - num_entries, num_files); + deduplicate(NULL, 0); + compute_file_offsets(); - output_assembly(); + fprintf(stderr, "lineinfo: %u entries, %u files\n", + num_entries, num_files); + + output_assembly(); + } dwarf_end(dwarf); elf_end(elf); @@ -552,6 +1251,5 @@ int main(int argc, char *argv[]) for (unsigned int i = 0; i < num_files; i++) free(files[i].name); free(files); - return 0; } -- 2.53.0