mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v3 0/2] perf unwind: Don't load symbols when the caller doesn't need them
@ 2026-10-30 19:32 Matthew Wood
  2026-10-30 19:32 ` [PATCH v3 1/2] perf symbol: Add dso__load_symsrc() Matthew Wood
  2026-10-30 19:32 ` [PATCH v3 2/2] perf unwind: Don't look up symbols when the caller doesn't want them Matthew Wood
  0 siblings, 2 replies; 3+ messages in thread
From: Matthew Wood @ 2026-10-30 19:32 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
	Namhyung Kim, Mark Rutland, Alexander Shishkin, Jiri Olsa,
	Ian Rogers, Adrian Hunter, James Clark
  Cc: linux-kernel, linux-perf-users

When perf only needs to know which DSOs a callchain passes through (as
build-id marking does at the end of 'perf record' and in 'perf inject
-b') it resolves the callchain without symbols. The DWARF unwinders
ignore that and look up the symbol of every frame anyway, which loads
and demangles the whole symbol table of each DSO they visit.

On arm64 every frame pointer sample also gets a short DWARF
unwind to find the leaf function's caller; since v6.17 (commit
29be60c93d2d, "perf build-id: Mark DSO in sample callchains") the end of
'perf record -a --call-graph fp' loads the symbols of every user binary
it sampled. On a 128-CPU arm64 host,

  perf record -a -e cycles:u --call-graph fp -- sleep 2

never finishes writing perf.data, with over 1GB of heap growth in 5
seconds.

Skipping the symbol lookup isn't enough because loading the
symbols also finds two things the unwinders depend on: the DSO's
build-id, which libdw needs to fall back to the build-id cache, and any
separate debuginfo file, through dso__symsrc_filename(), which is how
both unwinders find a .debug_frame there.

  1: add dso__load_symsrc(), which does the same search for debuginfo
     and the build-id as symbol loading, but doesn't read any symbols
  2: pass the symbols flag down to the unwinders and honour it, using
     dso__load_symsrc() when symbols aren't wanted, which also makes
     the arm64 leaf frame caller unwind skip symbols

Testing compared v7.3-rc7 with this series, on x86 and natively on a
CentOS Stream 10 arm64 host:

 - The 'perf test' dwarf unwind and 'perf inject' DWARF callchain
   conversion tests pass on both, also in a REFCNT_CHECKING build on
   x86. The libdw multi-threaded unwind and addr2line inline unwinding
   tests pass on x86; on the arm64 host the former is skipped and the
   latter fails both with and without this series.

 - 'perf inject -b' on an x86 'perf record --call-graph dwarf' profile
   of a Python workload goes from 0.19s and 69MB max RSS to 0.10s and
   33MB, with the same build-ids injected.

 - A test library built with -fno-asynchronous-unwind-tables, so that
   its CFI is only in .debug_frame, then split with 'objcopy
   --only-keep-debug', sits between two other DSOs in the sampled
   stacks. On both architectures, with the debuginfo found either
   through .gnu_debuglink or only in perf's build-id cache, 'perf
   script' output for a --call-graph dwarf profile, and on arm64 for an
   fp profile, is unchanged with both libdw and libunwind, and 'perf
   inject -b' unwinds the same frames and marks the same DSOs. v1,
   which only passed the flag down, lost every frame past the library
   in all but 5 of 4045 samples in 'perf script'.

libunwind's .debug_frame support couldn't be exercised: EL9's x86
libunwind is built without it, and on arm64 perf's libunwind support
has been compiled out since v7.2, as libunwind-arm64.c checks
HAVE_LIBUNWIND_ARM64_SUPPORT while Makefile.config defines
HAVE_LIBUNWIND_AARCH64_SUPPORT. With that macro defined by hand, and a
cast for the build error it then hits, arm64 libunwind results are also
the same with and without this series. That breakage is independent of
this series.

Not tested: the build-id cache fallback for DSOs in another mount
namespace when the MMAP2 events don't carry build-ids.

Changes since v2:
  - Instead of a separate debuginfo lookup, reuse dso__load()'s search
    and stop before reading symbols (dso__load_symsrc()) so candidate
    files are checked against the build-id, --symfs, mount namespaces
    and chroots are honoured, and the search is done once per DSO
  - Drop moving dso__read_build_id() out of 'perf inject'
  - Extend the dwarf unwind test to unwind without symbols, check that
    no symbols get loaded, and compare with unwinding with symbols
  - Link:
    https://lore.kernel.org/all/20261017000158.1178269-1-thepacketgeek@gmail.com/

Changes since v1:
  - Complete re-work on approach based on feedback
  - Link: https://lore.kernel.org/linux-perf-users/20261014151326.4142140-1-thepacketgeek@gmail.com/

Matthew Wood (2):
  perf symbol: Add dso__load_symsrc()
  perf unwind: Don't look up symbols when the caller doesn't want them

 tools/perf/tests/dwarf-unwind.c               | 71 ++++++++++++++++---
 .../util/arm64-frame-pointer-unwind-support.c |  7 +-
 tools/perf/util/dso.h                         | 11 +++
 tools/perf/util/machine.c                     |  7 +-
 tools/perf/util/symbol.c                      | 40 +++++++++--
 tools/perf/util/symbol.h                      |  1 +
 tools/perf/util/unwind-libdw.c                | 17 ++++-
 tools/perf/util/unwind-libdw.h                |  1 +
 tools/perf/util/unwind-libunwind.c            | 22 ++++--
 tools/perf/util/unwind.c                      |  7 +-
 tools/perf/util/unwind.h                      | 11 ++-
 11 files changed, 164 insertions(+), 31 deletions(-)


base-commit: 24a19b953e2a3d0ea5a8c4951a0ab8f32720c907
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 3+ messages in thread

* [PATCH v3 1/2] perf symbol: Add dso__load_symsrc()
  2026-10-30 19:32 [PATCH v3 0/2] perf unwind: Don't load symbols when the caller doesn't need them Matthew Wood
@ 2026-10-30 19:32 ` Matthew Wood
  2026-10-30 19:32 ` [PATCH v3 2/2] perf unwind: Don't look up symbols when the caller doesn't want them Matthew Wood
  1 sibling, 0 replies; 3+ messages in thread
From: Matthew Wood @ 2026-10-30 19:32 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
	Namhyung Kim, Mark Rutland, Alexander Shishkin, Jiri Olsa,
	Ian Rogers, Adrian Hunter, James Clark
  Cc: linux-kernel, linux-perf-users

dso__load() goes through the candidate files for a DSO, such as the DSO
itself, separate debuginfo found through .gnu_debuglink, the build-id
cache and /usr/lib/debug, checks their build-ids, records the one with a
symbol table in dso__symsrc_filename(), and then reads and demangles
that symbol table.

The DWARF unwinders depend on the first part. dso__symsrc_filename() is
how they find a separate debuginfo file, for instance to use its
.debug_frame, and libdw's build-id cache fallback needs the build-id
that dso__load() reads when the DSO doesn't have one yet. They get both
today as a side effect of loading the symbols of every frame they visit.

Add dso__load_symsrc(), which does the same search, inside the DSO's
mount namespace and honouring --symfs but stops before reading any
symbols. A new symsrc_loaded flag records that it ran, so the search is
done once per DSO, and the DSO's symbols stay unloaded, so dso__load()
still loads them when they are wanted. Kernel and perf map DSOs are
skipped, as the unwinders only handle user space ELF images.

No functional change on its own; the next patch uses it.

Signed-off-by: Matthew Wood <thepacketgeek@gmail.com>
---
 tools/perf/util/dso.h    | 11 +++++++++++
 tools/perf/util/symbol.c | 40 ++++++++++++++++++++++++++++++++++++----
 tools/perf/util/symbol.h |  1 +
 3 files changed, 48 insertions(+), 4 deletions(-)

diff --git a/tools/perf/util/dso.h b/tools/perf/util/dso.h
index 55c4aaa53c38..fcf235186978 100644
--- a/tools/perf/util/dso.h
+++ b/tools/perf/util/dso.h
@@ -340,6 +340,7 @@ DECLARE_RC_STRUCT(dso) {
 	u8		 is_64_bit:1;
 	bool		 sorted_by_name;
 	bool		 loaded;
+	bool		 symsrc_loaded;
 	u8		 rel;
 	char		 name[];
 };
@@ -603,6 +604,16 @@ static inline void dso__set_loaded(struct dso *dso)
 	RC_CHK_ACCESS(dso)->loaded = true;
 }
 
+static inline bool dso__symsrc_loaded(const struct dso *dso)
+{
+	return RC_CHK_ACCESS(dso)->symsrc_loaded;
+}
+
+static inline void dso__set_symsrc_loaded(struct dso *dso)
+{
+	RC_CHK_ACCESS(dso)->symsrc_loaded = true;
+}
+
 static inline struct mutex *dso__lock(struct dso *dso)
 {
 	return &RC_CHK_ACCESS(dso)->lock;
diff --git a/tools/perf/util/symbol.c b/tools/perf/util/symbol.c
index 3587ad243159..ed2f8e89b191 100644
--- a/tools/perf/util/symbol.c
+++ b/tools/perf/util/symbol.c
@@ -1798,7 +1798,7 @@ static int dso__find_perf_map(char *filebuf, size_t bufsz,
 	return rc;
 }
 
-int dso__load(struct dso *dso, struct map *map)
+static int __dso__load(struct dso *dso, struct map *map, bool symbols)
 {
 	char *name;
 	int ret = -1;
@@ -1828,11 +1828,17 @@ int dso__load(struct dso *dso, struct map *map)
 	nsinfo__mountns_enter(dso__nsinfo(dso), &nsc);
 
 	/* check again under the dso->lock */
-	if (dso__loaded(dso)) {
+	if (dso__loaded(dso) || (!symbols && dso__symsrc_loaded(dso))) {
 		ret = 1;
 		goto out;
 	}
 
+	/* Unwinders only need ELF images, not kernel or perf map symbols. */
+	if (!symbols && (dso__kernel(dso) || perfmap)) {
+		ret = 0;
+		goto out;
+	}
+
 	kmod = dso__is_kmod(dso);
 
 	if (dso__kernel(dso) && !kmod) {
@@ -1915,7 +1921,7 @@ int dso__load(struct dso *dso, struct map *map)
 		}
 
 #ifdef HAVE_LIBBFD_SUPPORT
-		if (is_reg)
+		if (is_reg && symbols)
 			bfdrc = dso__load_bfd_symbols(dso, name);
 #endif
 		if (is_reg && bfdrc < 0)
@@ -1978,6 +1984,11 @@ int dso__load(struct dso *dso, struct map *map)
 	if (!runtime_ss && syms_ss)
 		runtime_ss = syms_ss;
 
+	if (!symbols) {
+		ret = 0;
+		goto out_destroy;
+	}
+
 	if (syms_ss)
 		ret = dso__load_sym(dso, map, syms_ss, runtime_ss, kmod);
 	else
@@ -1991,6 +2002,7 @@ int dso__load(struct dso *dso, struct map *map)
 			ret += nr_plt;
 	}
 
+out_destroy:
 	for (; ss_pos > 0; ss_pos--)
 		symsrc__destroy(&ss_[ss_pos - 1]);
 out_free:
@@ -1998,13 +2010,33 @@ int dso__load(struct dso *dso, struct map *map)
 	if (ret < 0 && strstr(dso__name(dso), " (deleted)") != NULL)
 		ret = 0;
 out:
-	dso__set_loaded(dso);
+	if (symbols)
+		dso__set_loaded(dso);
+	dso__set_symsrc_loaded(dso);
 	mutex_unlock(dso__lock(dso));
 	nsinfo__mountns_exit(&nsc);
 
 	return ret;
 }
 
+int dso__load(struct dso *dso, struct map *map)
+{
+	return __dso__load(dso, map, /*symbols=*/true);
+}
+
+/*
+ * Discover the same debug images as symbol loading, since they may contain
+ * unwind information. Do not read symbol tables, demangle names, synthesize
+ * PLT symbols, or mark the DSO's symbols as loaded.
+ */
+int dso__load_symsrc(struct dso *dso)
+{
+	if (dso__loaded(dso) || dso__symsrc_loaded(dso))
+		return 1;
+
+	return __dso__load(dso, NULL, /*symbols=*/false);
+}
+
 /*
  * Always takes ownership of vmlinux when vmlinux_allocated == true, even if
  * it returns an error.
diff --git a/tools/perf/util/symbol.h b/tools/perf/util/symbol.h
index d0bac824c79c..7dd7924a098d 100644
--- a/tools/perf/util/symbol.h
+++ b/tools/perf/util/symbol.h
@@ -196,6 +196,7 @@ struct ref_reloc_sym {
 };
 
 int dso__load(struct dso *dso, struct map *map);
+int dso__load_symsrc(struct dso *dso);
 int dso__load_vmlinux(struct dso *dso, struct map *map,
 		      const char *vmlinux, bool vmlinux_allocated);
 int dso__load_vmlinux_path(struct dso *dso, struct map *map);
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 3+ messages in thread

* [PATCH v3 2/2] perf unwind: Don't look up symbols when the caller doesn't want them
  2026-10-30 19:32 [PATCH v3 0/2] perf unwind: Don't load symbols when the caller doesn't need them Matthew Wood
  2026-10-30 19:32 ` [PATCH v3 1/2] perf symbol: Add dso__load_symsrc() Matthew Wood
@ 2026-10-30 19:32 ` Matthew Wood
  1 sibling, 0 replies; 3+ messages in thread
From: Matthew Wood @ 2026-10-30 19:32 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
	Namhyung Kim, Mark Rutland, Alexander Shishkin, Jiri Olsa,
	Ian Rogers, Adrian Hunter, James Clark
  Cc: linux-kernel, Arnaldo Carvalho de Melo, linux-perf-users

Commit 02b2705017d2 ("perf callchain: Allow symbols to be optional when
resolving a callchain") let callers that only need maps, such as
build-id marking in 'perf record' and 'perf inject', resolve callchains
without loading symbols. The DWARF unwinders were left out:
thread__resolve_callchain_unwind() just notes that "Not resolving
symbols with an unwinder isn't currently supported", and both libdw and
libunwind look up the symbol of every frame they visit. The first lookup
in a DSO loads its whole symbol table and demangles every name in it.

On arm64 this happens even for frame pointer callchains, because
get_leaf_frame_caller_aarch64() runs a two frame DWARF unwind for every
sample to recover the leaf function's caller. Since commit 29be60c93d2d
("perf build-id: Mark DSO in sample callchains"), process_buildids()
does that for every sample at the end of 'perf record', so with -a it
loads and demangles the symbol tables of every user binary it sampled.
A C++ binary with a pathological mangled name sent __cxa_demangle() into
exponential time and memory, and

  perf record -a -e cycles:u --call-graph fp -- sleep 2

never finished writing perf.data after the workload exited.

Pass a symbols flag down to the unwinders. When it is false, look up
only the map of each frame and leave entry->ms.sym NULL, which
unwind_entry() already handles just as add_callchain_ip() does for
symbols=false. get_leaf_frame_caller_aarch64() only uses the addresses,
and its caller looks up the leaf frame caller's symbol through
add_callchain_ip() when it wants it, so it never asks for symbols.

The unwinders still need what loading the symbols used to find: the
DSO's build-id and any separate debuginfo, through
dso__symsrc_filename(). Without symbols, they get those from
dso__load_symsrc(). libdw does that before reporting each module, and
both its report_module() and entry() follow the flag, so with symbols
the module's symbols are still loaded before libdw first asks
__find_debuginfo() for its debuginfo, which it does while unwinding the
frame to find out whether it is an activation. libunwind does it in
entry(), where it used to load the symbols. The frames returned are
unchanged.

The dwarf unwind test now unwinds without symbols first, checking that
no symbols get loaded, and then checks that unwinding with symbols
returns the same addresses and maps.

With libdw, 'perf inject -b' on an x86 'perf record --call-graph dwarf'
profile of a Python workload goes from 0.19s and 69MB max RSS to 0.10s
and 33MB, with the same build-ids injected.

Fixes: 02b2705017d2 ("perf callchain: Allow symbols to be optional when resolving a callchain")
Fixes: 29be60c93d2d ("perf build-id: Mark DSO in sample callchains")
Signed-off-by: Matthew Wood <thepacketgeek@gmail.com>
---
 tools/perf/tests/dwarf-unwind.c               | 71 ++++++++++++++++---
 .../util/arm64-frame-pointer-unwind-support.c |  7 +-
 tools/perf/util/machine.c                     |  7 +-
 tools/perf/util/unwind-libdw.c                | 17 ++++-
 tools/perf/util/unwind-libdw.h                |  1 +
 tools/perf/util/unwind-libunwind.c            | 22 ++++--
 tools/perf/util/unwind.c                      |  7 +-
 tools/perf/util/unwind.h                      | 11 ++-
 8 files changed, 116 insertions(+), 27 deletions(-)

diff --git a/tools/perf/tests/dwarf-unwind.c b/tools/perf/tests/dwarf-unwind.c
index 9ed78d00fb87..2cbb95c577a9 100644
--- a/tools/perf/tests/dwarf-unwind.c
+++ b/tools/perf/tests/dwarf-unwind.c
@@ -15,6 +15,7 @@
 #include "map.h"
 #include "symbol.h"
 #include "thread.h"
+#include "dso.h"
 #include "callchain.h"
 
 /* For bsearch. We try to unwind functions in shared object. */
@@ -53,9 +54,38 @@ int test__dwarf_unwind(struct test_suite *test, int subtest);
 
 #define MAX_STACK 8
 
+struct unwind_test_data {
+	unsigned long cnt;
+	unsigned long nr;
+	u64 ips[MAX_STACK];
+	struct map *maps[MAX_STACK];
+	bool check_unloaded;
+};
+
+static int unwind_entry_maps(struct unwind_entry *entry, void *arg)
+{
+	struct unwind_test_data *data = arg;
+	unsigned long idx = data->cnt;
+
+	if (idx >= MAX_STACK || entry->ms.sym || !entry->ms.map) {
+		pr_debug("failed: invalid entry without symbols\n");
+		return -1;
+	}
+	if (data->check_unloaded && dso__loaded(map__dso(entry->ms.map))) {
+		pr_debug("failed: unwinding loaded symbols\n");
+		return -1;
+	}
+
+	data->ips[idx] = entry->ip;
+	data->maps[idx] = map__get(entry->ms.map);
+	data->cnt++;
+	return 0;
+}
+
 static int unwind_entry(struct unwind_entry *entry, void *arg)
 {
-	unsigned long *cnt = (unsigned long *) arg;
+	struct unwind_test_data *data = arg;
+	unsigned long *cnt = &data->cnt;
 	char *symbol = entry->ms.sym ? entry->ms.sym->name : NULL;
 	static const char *funcs[MAX_STACK] = {
 		"test__arch_unwind_sample",
@@ -85,16 +115,26 @@ static int unwind_entry(struct unwind_entry *entry, void *arg)
 		return -1;
 	}
 
-	(*cnt)++;
+	if (*cnt >= data->nr || entry->ip != data->ips[*cnt] ||
+	    !RC_CHK_EQUAL(entry->ms.map, data->maps[*cnt])) {
+		pr_debug("failed: unwinding with symbols changed the callchain\n");
+		return -1;
+	}
+
 	pr_debug("got: %s 0x%" PRIx64 ", expecting %s\n",
 		 symbol, entry->ip, funcs[idx]);
-	return strcmp((const char *) symbol, funcs[idx]);
+	if (strcmp(symbol, funcs[idx]))
+		return -1;
+	(*cnt)++;
+	return 0;
 }
 
 NO_TAIL_CALL_ATTRIBUTE noinline int test_dwarf_unwind__thread(struct thread *thread)
 {
 	struct perf_sample sample;
-	unsigned long cnt = 0;
+	struct unwind_test_data data = {
+		.check_unloaded = callchain_param.order == ORDER_CALLER,
+	};
 	int err = -1;
 
 	perf_sample__init(&sample, /*all=*/true);
@@ -103,17 +143,32 @@ NO_TAIL_CALL_ATTRIBUTE noinline int test_dwarf_unwind__thread(struct thread *thr
 		goto out;
 	}
 
-	err = unwind__get_entries(unwind_entry, &cnt, thread,
-				  &sample, MAX_STACK, false);
+	/* Resolve maps first so symbol loading cannot hide a regression. */
+	err = unwind__get_entries(unwind_entry_maps, &data, thread,
+				  &sample, MAX_STACK, /*symbols=*/false,
+				  /*best_effort=*/false);
+	if (err || data.cnt != MAX_STACK) {
+		pr_debug("unwind without symbols failed\n");
+		err = -1;
+		goto out;
+	}
+
+	data.nr = data.cnt;
+	data.cnt = 0;
+	err = unwind__get_entries(unwind_entry, &data, thread,
+				  &sample, MAX_STACK, /*symbols=*/true,
+				  /*best_effort=*/false);
 	if (err)
 		pr_debug("unwind failed\n");
-	else if (cnt != MAX_STACK) {
+	else if (data.cnt != MAX_STACK) {
 		pr_debug("got wrong number of stack entries %lu != %d\n",
-			 cnt, MAX_STACK);
+			 data.cnt, MAX_STACK);
 		err = -1;
 	}
 
  out:
+	for (unsigned long i = 0; i < MAX_STACK; i++)
+		map__put(data.maps[i]);
 	zfree(&sample.user_stack.data);
 	zfree(&sample.user_regs->regs);
 	perf_sample__exit(&sample);
diff --git a/tools/perf/util/arm64-frame-pointer-unwind-support.c b/tools/perf/util/arm64-frame-pointer-unwind-support.c
index 3af8c7a466e0..c16eeee34211 100644
--- a/tools/perf/util/arm64-frame-pointer-unwind-support.c
+++ b/tools/perf/util/arm64-frame-pointer-unwind-support.c
@@ -67,7 +67,12 @@ u64 get_leaf_frame_caller_aarch64(struct perf_sample *sample, struct thread *thr
 		regs->cache_regs[PERF_REG_ARM64_SP] = 0;
 	}
 
-	ret = unwind__get_entries(add_entry, &entries, thread, sample, 2, true);
+	/*
+	 * Only the addresses are used. The caller looks up the leaf frame
+	 * caller's symbol itself if it wants it, so don't load symbols here.
+	 */
+	ret = unwind__get_entries(add_entry, &entries, thread, sample, 2,
+				  /*symbols=*/false, /*best_effort=*/true);
 	memcpy(regs, &old_regs, sizeof(*regs));
 
 	if (ret || entries.length != 2)
diff --git a/tools/perf/util/machine.c b/tools/perf/util/machine.c
index f86b3b7df742..16847e26ea09 100644
--- a/tools/perf/util/machine.c
+++ b/tools/perf/util/machine.c
@@ -3061,11 +3061,8 @@ static int thread__resolve_callchain_unwind(struct thread *thread,
 	    !sample->user_stack.size)
 		return 0;
 
-	if (!symbols)
-		pr_debug("Not resolving symbols with an unwinder isn't currently supported\n");
-
-	return unwind__get_entries(unwind_entry, cursor,
-				   thread, sample, max_stack, false);
+	return unwind__get_entries(unwind_entry, cursor, thread, sample,
+				   max_stack, symbols, /*best_effort=*/false);
 }
 
 int __thread__resolve_callchain(struct thread *thread,
diff --git a/tools/perf/util/unwind-libdw.c b/tools/perf/util/unwind-libdw.c
index 63a5c2253174..bc6b8fd40110 100644
--- a/tools/perf/util/unwind-libdw.c
+++ b/tools/perf/util/unwind-libdw.c
@@ -73,11 +73,17 @@ static int __report_module(struct addr_location *al, u64 ip,
 	Dwfl_Module *mod;
 	struct dso *dso = NULL;
 	Dwarf_Addr base;
+
 	/*
-	 * Some callers will use al->sym, so we can't just use the
-	 * cheaper thread__find_map() here.
+	 * Callers that want symbols will use al->sym. Loading the symbols
+	 * also finds the DSO's build-id and any separate debuginfo, which
+	 * are needed below and by __find_debuginfo(), so look for those
+	 * without loading the symbols when they aren't wanted.
 	 */
-	thread__find_symbol(ui->thread, PERF_RECORD_MISC_USER, ip, al);
+	if (ui->symbols)
+		thread__find_symbol(ui->thread, PERF_RECORD_MISC_USER, ip, al);
+	else
+		thread__find_map(ui->thread, PERF_RECORD_MISC_USER, ip, al);
 
 	if (al->map)
 		dso = map__dso(al->map);
@@ -85,6 +91,9 @@ static int __report_module(struct addr_location *al, u64 ip,
 	if (!dso)
 		return 0;
 
+	if (!ui->symbols)
+		dso__load_symsrc(dso);
+
 	/*
 	 * The generated JIT DSO files only map the code segment without
 	 * ELF headers.  Since JIT codes used to be packed in a memory
@@ -361,6 +370,7 @@ int libdw__get_entries(unwind_entry_cb_t cb, void *arg,
 			struct thread *thread,
 			struct perf_sample *data,
 			int max_stack,
+			bool symbols,
 			bool best_effort)
 {
 	struct maps *maps = thread__maps(thread);
@@ -389,6 +399,7 @@ int libdw__get_entries(unwind_entry_cb_t cb, void *arg,
 		.max_stack	= max_stack,
 		.e_machine	= e_machine,
 		.e_flags	= e_flags,
+		.symbols	= symbols,
 		.best_effort    = best_effort
 	};
 
diff --git a/tools/perf/util/unwind-libdw.h b/tools/perf/util/unwind-libdw.h
index 6423bf5a2492..ab37bf5cb105 100644
--- a/tools/perf/util/unwind-libdw.h
+++ b/tools/perf/util/unwind-libdw.h
@@ -22,6 +22,7 @@ struct unwind_info {
 	int			idx;
 	uint32_t		e_flags;
 	uint16_t		e_machine;
+	bool			symbols;
 	bool			best_effort;
 	struct unwind_entry	entries[];
 };
diff --git a/tools/perf/util/unwind-libunwind.c b/tools/perf/util/unwind-libunwind.c
index 73d191ce51a5..3c5acc052715 100644
--- a/tools/perf/util/unwind-libunwind.c
+++ b/tools/perf/util/unwind-libunwind.c
@@ -589,14 +589,26 @@ void unwind__finish_access(struct maps *maps)
 	libunwind_arch__finish_access(maps);
 }
 
-static int entry(uint64_t ip, struct thread *thread, unwind_entry_cb_t cb, void *arg)
+static int entry(uint64_t ip, struct thread *thread, bool symbols,
+		 unwind_entry_cb_t cb, void *arg)
 {
 	struct unwind_entry e;
 	struct addr_location al;
 	int ret;
 
 	addr_location__init(&al);
-	e.ms.sym = thread__find_symbol(thread, PERF_RECORD_MISC_USER, ip, &al);
+	if (symbols) {
+		thread__find_symbol(thread, PERF_RECORD_MISC_USER, ip, &al);
+	} else {
+		/*
+		 * Still find the DSO's separate debuginfo, which loading
+		 * the symbols would have, for read_unwind_spec_debug_frame().
+		 */
+		thread__find_map(thread, PERF_RECORD_MISC_USER, ip, &al);
+		if (al.map)
+			dso__load_symsrc(map__dso(al.map));
+	}
+	e.ms.sym = al.sym;
 	e.ip     = ip;
 	e.ms.map = al.map;
 	e.ms.thread = thread__get(al.thread);
@@ -614,7 +626,7 @@ static int entry(uint64_t ip, struct thread *thread, unwind_entry_cb_t cb, void
 int libunwind__get_entries(unwind_entry_cb_t cb, void *arg,
 			 struct thread *thread,
 			 struct perf_sample *sample, int max_stack,
-			 bool best_effort)
+			 bool symbols, bool best_effort)
 {
 	struct unwind_info *ui;
 	uint64_t first_ip;
@@ -640,7 +652,7 @@ int libunwind__get_entries(unwind_entry_cb_t cb, void *arg,
 
 	if (max_stack == 1) {
 		/* Special case for a single entry. */
-		ret = entry(first_ip, thread, cb, arg);
+		ret = entry(first_ip, thread, symbols, cb, arg);
 		return ret ? (ret == -ENOMEM ? -ENOMEM : 0) : 1;
 	}
 
@@ -662,7 +674,7 @@ int libunwind__get_entries(unwind_entry_cb_t cb, void *arg,
 		int j = callchain_param.order == ORDER_CALLEE ? i : ui->cur_ip - i - 1;
 
 		if (ui->ips[j]) {
-			ret = entry(ui->ips[j], thread, cb, arg);
+			ret = entry(ui->ips[j], thread, symbols, cb, arg);
 			if (ret)
 				break;
 			entries++;
diff --git a/tools/perf/util/unwind.c b/tools/perf/util/unwind.c
index 4ed4b1d55c69..8555ece4b6f3 100644
--- a/tools/perf/util/unwind.c
+++ b/tools/perf/util/unwind.c
@@ -10,6 +10,7 @@ int unwind__get_entries(unwind_entry_cb_t cb __maybe_unused, void *arg __maybe_u
 			struct thread *thread __maybe_unused,
 			struct perf_sample *data __maybe_unused,
 			int max_stack __maybe_unused,
+			bool symbols __maybe_unused,
 			bool best_effort __maybe_unused)
 {
 	int ret = 0;
@@ -29,10 +30,12 @@ int unwind__get_entries(unwind_entry_cb_t cb __maybe_unused, void *arg __maybe_u
 	for (size_t i = 0; i < ARRAY_SIZE(symbol_conf.unwind_style); i++) {
 		switch (symbol_conf.unwind_style[i]) {
 		case UNWIND_STYLE_LIBDW:
-			ret = libdw__get_entries(cb, arg, thread, data, max_stack, best_effort);
+			ret = libdw__get_entries(cb, arg, thread, data, max_stack,
+						 symbols, best_effort);
 			break;
 		case UNWIND_STYLE_LIBUNWIND:
-			ret = libunwind__get_entries(cb, arg, thread, data, max_stack, best_effort);
+			ret = libunwind__get_entries(cb, arg, thread, data, max_stack,
+						     symbols, best_effort);
 			break;
 		case UNWIND_STYLE_UNKNOWN:
 		default:
diff --git a/tools/perf/util/unwind.h b/tools/perf/util/unwind.h
index a2d4af694fde..59dfb234cbea 100644
--- a/tools/perf/util/unwind.h
+++ b/tools/perf/util/unwind.h
@@ -23,6 +23,9 @@ int unwind__configure(const char *var, const char *value, void *cb);
 int unwind__option(const struct option *opt, const char *arg, int unset);
 
 /*
+ * When symbols is false, only the map of each frame is looked up and
+ * entry->ms.sym is left NULL, so that no symbol tables are loaded.
+ *
  * When best_effort is set, don't report errors and fail silently. This could
  * be expanded in the future to be more permissive about things other than
  * error messages.
@@ -30,19 +33,20 @@ int unwind__option(const struct option *opt, const char *arg, int unset);
 int unwind__get_entries(unwind_entry_cb_t cb, void *arg,
 			struct thread *thread,
 			struct perf_sample *data, int max_stack,
-			bool best_effort);
+			bool symbols, bool best_effort);
 
 #ifdef HAVE_LIBDW_SUPPORT
 int libdw__get_entries(unwind_entry_cb_t cb, void *arg,
 		       struct thread *thread,
 		       struct perf_sample *data, int max_stack,
-		       bool best_effort);
+		       bool symbols, bool best_effort);
 #else
 #include "debug.h"
 static inline int libdw__get_entries(unwind_entry_cb_t cb __maybe_unused, void *arg __maybe_unused,
 				     struct thread *thread __maybe_unused,
 				     struct perf_sample *data __maybe_unused,
 				     int max_stack __maybe_unused,
+				     bool symbols __maybe_unused,
 				     bool best_effort __maybe_unused)
 {
 	pr_warning_once("Error: libdw dwarf unwinding not built into perf\n");
@@ -55,7 +59,7 @@ static inline int libdw__get_entries(unwind_entry_cb_t cb __maybe_unused, void *
 int libunwind__get_entries(unwind_entry_cb_t cb, void *arg,
 			   struct thread *thread,
 			   struct perf_sample *data, int max_stack,
-			   bool best_effort);
+			   bool symbols, bool best_effort);
 int unwind__prepare_access(struct maps *maps, uint16_t e_machine);
 void unwind__flush_access(struct maps *maps);
 void unwind__finish_access(struct maps *maps);
@@ -66,6 +70,7 @@ static inline int libunwind__get_entries(unwind_entry_cb_t cb __maybe_unused,
 					 struct thread *thread __maybe_unused,
 					 struct perf_sample *data __maybe_unused,
 					 int max_stack __maybe_unused,
+					 bool symbols __maybe_unused,
 					 bool best_effort __maybe_unused)
 {
 	pr_warning_once("Error: libunwind dwarf unwinding not built into perf\n");
-- 
2.53.0-Meta


^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-10-30 19:32 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-30 19:32 [PATCH v3 0/2] perf unwind: Don't load symbols when the caller doesn't need them Matthew Wood
2026-10-30 19:32 ` [PATCH v3 1/2] perf symbol: Add dso__load_symsrc() Matthew Wood
2026-10-30 19:32 ` [PATCH v3 2/2] perf unwind: Don't look up symbols when the caller doesn't want them Matthew Wood

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®