From: Yanbo Zhao <yzhao62@ncsu.edu>
To: Namhyung Kim <namhyung@kernel.org>,
Arnaldo Carvalho de Melo <acme@kernel.org>,
Ian Rogers <irogers@google.com>,
Kan Liang <kan.liang@linux.intel.com>
Cc: Jiri Olsa <jolsa@kernel.org>,
Adrian Hunter <adrian.hunter@intel.com>,
Peter Zijlstra <peterz@infradead.org>,
Ingo Molnar <mingo@kernel.org>,
Mark Rutland <mark.rutland@arm.com>,
Alexander Shishkin <alexander.shishkin@linux.intel.com>,
James Clark <james.clark@linaro.org>, Zecheng Li <zli94@ncsu.edu>,
Xu Liu <xliuprof@google.com>,
linux-perf-users@vger.kernel.org, linux-kernel@vger.kernel.org,
Yanbo Zhao <yzhao62@ncsu.edu>
Subject: [PATCH v2 3/3] perf annotate: Resolve C++ virtual function calls in x86 insn tracking
Date: Wed, 30 Sep 2026 17:00:38 -0400 [thread overview]
Message-ID: <20260930210038.196928-4-yzhao62@ncsu.edu> (raw)
In-Reply-To: <20260930210038.196928-1-yzhao62@ncsu.edu>
A C++ virtual function call is an indirect call through the vtable.
The instruction tracking of the data type profiling cannot resolve the
callee name for it, so the return type is lost and the type of the
register holding the returned value is unknown after the call. It
matters when the returned value is used directly to access memory, like
in 'p->next()->val', where no DWARF variable describes the temporary.
The vtable pointer and the slot index are known statically from the
type of the object pointer, so the callee can be found by DWARF. Track
the two steps of a virtual call in update_insn_state_x86():
1. Loading the vtable pointer from the object. When a load from a
register with a pointer type hits the '_vptr' member of the class
(or one of its base classes), the destination register is marked as
TSR_KIND_VTABLE_PTR with the class DIE returned by
die_get_vptr_class().
2. Calling through the vtable. GCC emits the call in two forms:
mov (%rbx),%rax # vtable pointer
call *0x8(%rax) # slot 1
or, with speculative devirtualization when it can guess the target,
the function pointer is loaded first and compared with the guess:
mov (%rbx),%rax # vtable pointer
mov 0x8(%rax),%rax # function pointer at slot 1
cmp %r14,%rax
je <inlined copy>
call *%rax
For the first form, the call handler resolves the slot from the
offset of the target operand when the base register is a vtable
pointer. For the second form, the load from a vtable pointer
register marks the destination as TSR_KIND_VFUNC_PTR holding the
return type of the function at the slot, and the call handler uses
it when the target operand is such a register. In both cases the
function DIE is found by die_find_virtual_func() and its return
type is set to the return value register like a direct call. Void
functions have no return type and are simply ignored.
The resolution happens before the caller-saved registers are
invalidated since the register used for the call is one of them.
To let the call handler see the target operand, call__parse() saves
the operand of an indirect call (without the leading '*') to
ops->target.raw so that annotate_get_insn_location() can extract the
register and offset for both '*0x8(%rax)' and '*%rax'.
The new kinds are pointer-sized and not compound when saved to the
stack, and pr_debug_type_name() knows how to print them.
The slot index is calculated with the host pointer size like the
other places for now. Only the primary vtable is handled: a virtual
call through a pointer to a non-primary base class subobject (multiple
inheritance) gets the class of that base from die_get_vptr_class() so
the slot index is looked up in the right vtable, but the receiver
adjustment (this pointer thunks) is not tracked.
For example, with the following code built with -O2 -g:
struct Node {
long val;
Node *nxt;
virtual Node *next() { return nxt; }
...
};
long run(Node *p, long n)
{
long s = 0;
for (long i = 0; i < n; i++)
s += p->next()->val;
return s;
}
'perf annotate --code-with-type' now shows the type for the access to
the returned pointer after the call which was unknown before:
1251: movq (%rbp), %rax # data-type: struct Node +0 (_vptr.Node)
1255: movq (%rax), %rax
1258: cmpq %r14, %rax
125b: je 0x1240
125d: movq %rbp, %rdi
1260: addq $1, %rbx
1264: callq *%rax
1266: addq 8(%rax), %r12 # data-type: struct Node +0x8 (val)
Signed-off-by: Yanbo Zhao <yzhao62@ncsu.edu>
---
Changes in v2:
- Remove the receiver ('this' pointer) register update after the call
and the type_state::arg0_reg field. The code was unreachable since
the register is caller-saved and already invalidated, and it's not
needed: the 'this' pointer lives in a callee-saved register or on
the stack across the call and the DWARF location lists cover it
(Sashiko, Namhyung).
- Handle 'call *%reg' in addition to 'call *N(%reg)'. GCC emits it
with speculative devirtualization: the function pointer is loaded
from the vtable, compared with the guessed target and then called.
The load is tracked as TSR_KIND_VFUNC_PTR holding the return type
of the function at the slot (Sashiko).
- Strip the leading '*' of an indirect call operand in call__parse()
when saving ops->target.raw, instead of in extract_reg_offset()
which is not reached for '*%reg' (Sashiko).
- Ignore virtual functions returning void (no return type) instead of
treating it as a failure (Namhyung).
- Keep the existing pointer dereference branch and its fall-through
to the multi-register and per-cpu paths intact. The vtable pointer
is detected from the member type resolved there, so the common
path only pays a tag check.
- Treat the new register kinds as pointer-sized and non-compound when
saved to the stack.
- Drop the unnecessary include of dwarf-aux.h and hist.h.
tools/perf/util/annotate-arch/annotate-x86.c | 76 +++++++++++++++++++-
tools/perf/util/annotate-data.c | 15 +++-
tools/perf/util/annotate-data.h | 4 ++
tools/perf/util/disasm.c | 5 ++
4 files changed, 96 insertions(+), 4 deletions(-)
diff --git a/tools/perf/util/annotate-arch/annotate-x86.c b/tools/perf/util/annotate-arch/annotate-x86.c
index 1acf31a2c759..d21192b74b6b 100644
--- a/tools/perf/util/annotate-arch/annotate-x86.c
+++ b/tools/perf/util/annotate-arch/annotate-x86.c
@@ -216,6 +216,29 @@ static void invalidate_reg_state(struct type_state_reg *reg)
reg->copied_from = -1;
}
+/*
+ * Get the return type of the C++ virtual function at the @offset in the
+ * vtable of @class_die. Returns false if it's not found or the function
+ * returns void.
+ */
+static bool vtable_get_rettype(Dwarf_Die *class_die, int offset,
+ Dwarf_Die *type_die)
+{
+ Dwarf_Die func_die;
+ int index;
+
+ if (offset < 0)
+ return false;
+
+ /* TODO: arch-dependent pointer size */
+ index = offset / sizeof(void *);
+
+ if (die_find_virtual_func(class_die, index, &func_die) == NULL)
+ return false;
+
+ return die_get_real_type(&func_die, type_die) != NULL;
+}
+
static void update_insn_state_x86(struct type_state *state,
struct data_loc_info *dloc, Dwarf_Die *cu_die,
struct disasm_line *dl)
@@ -236,6 +259,7 @@ static void update_insn_state_x86(struct type_state *state,
struct symbol *func = dl->ops.target.sym;
const char *call_name;
u64 call_addr;
+ bool has_rettype = false;
/* Try to resolve the call target name */
if (func)
@@ -252,6 +276,26 @@ static void update_insn_state_x86(struct type_state *state,
else
pr_debug_dtp("call [%x] <unknown>\n", insn_offset);
+ /*
+ * Resolve the return type of a C++ virtual function call
+ * before invalidating the caller-saved register used for the
+ * indirect call. It's either through the vtable pointer
+ * directly like 'call *0x8(%rax)' or through the function
+ * pointer loaded from the vtable like 'call *%rax'.
+ */
+ if (has_reg_type(state, dst->reg1) && state->regs[dst->reg1].ok) {
+ tsr = &state->regs[dst->reg1];
+
+ if (dst->mem_ref && !dst->multi_regs &&
+ tsr->kind == TSR_KIND_VTABLE_PTR &&
+ vtable_get_rettype(&tsr->type, dst->offset, &type_die)) {
+ has_rettype = true;
+ } else if (!dst->mem_ref && tsr->kind == TSR_KIND_VFUNC_PTR) {
+ type_die = tsr->type;
+ has_rettype = true;
+ }
+ }
+
/* Invalidate caller-saved registers after call */
call_addr = map__rip_2objdump(dloc->ms->map,
dloc->ms->sym->start + dl->al.offset);
@@ -267,7 +311,10 @@ static void update_insn_state_x86(struct type_state *state,
}
/* Update register with the return type (if any) */
- if (call_name && die_find_func_rettype(cu_die, call_name, &type_die)) {
+ if (call_name && die_find_func_rettype(cu_die, call_name, &type_die))
+ has_rettype = true;
+
+ if (has_rettype) {
tsr = &state->regs[state->ret_reg];
tsr->type = type_die;
tsr->kind = TSR_KIND_TYPE;
@@ -622,13 +669,38 @@ static void update_insn_state_x86(struct type_state *state,
}
pr_debug_type_name(&tsr->type, tsr->kind);
}
+ /* Load a function pointer from the vtable (for 'call *%reg') */
+ else if (has_reg_type(state, sreg) && state->regs[sreg].ok &&
+ state->regs[sreg].kind == TSR_KIND_VTABLE_PTR &&
+ vtable_get_rettype(&state->regs[sreg].type, src->offset,
+ &type_die)) {
+ tsr->type = type_die;
+ tsr->kind = TSR_KIND_VFUNC_PTR;
+ tsr->offset = 0;
+ tsr->ok = true;
+
+ pr_debug_dtp("mov [%x] %#x(reg%d) -> reg%d",
+ insn_offset, src->offset, sreg, dst->reg1);
+ pr_debug_type_name(&tsr->type, tsr->kind);
+ }
/* And then dereference the pointer if it has one */
else if (has_reg_type(state, sreg) && state->regs[sreg].ok &&
state->regs[sreg].kind == TSR_KIND_TYPE &&
die_deref_ptr_type(&state->regs[sreg].type,
src->offset + state->regs[sreg].offset, &type_die)) {
+ /*
+ * The vtable pointer of a C++ object needs the class
+ * to resolve virtual calls through it.
+ */
+ if (die_is_vtbl_ptr_type(&type_die) &&
+ die_deref_vptr_class(&state->regs[sreg].type,
+ src->offset + state->regs[sreg].offset,
+ &type_die))
+ tsr->kind = TSR_KIND_VTABLE_PTR;
+ else
+ tsr->kind = TSR_KIND_TYPE;
+
tsr->type = type_die;
- tsr->kind = TSR_KIND_TYPE;
tsr->offset = 0;
tsr->ok = true;
diff --git a/tools/perf/util/annotate-data.c b/tools/perf/util/annotate-data.c
index 8a9d3f2eec4d..e9bf76cc2863 100644
--- a/tools/perf/util/annotate-data.c
+++ b/tools/perf/util/annotate-data.c
@@ -67,6 +67,14 @@ void pr_debug_type_name(Dwarf_Die *die, enum type_state_kind kind)
pr_info(" pointer");
/* it also prints the type info */
break;
+ case TSR_KIND_VTABLE_PTR:
+ pr_info(" vtable pointer");
+ /* it also prints the type info */
+ break;
+ case TSR_KIND_VFUNC_PTR:
+ pr_info(" virtual function pointer returning");
+ /* it also prints the type info */
+ break;
case TSR_KIND_CANARY:
pr_info(" stack canary\n");
return;
@@ -715,8 +723,11 @@ void set_stack_state(struct type_state_stack *stack, int offset, u8 kind,
Dwarf_Die *type_die, int ptr_offset)
{
Dwarf_Word size;
+ bool is_pointer = (kind == TSR_KIND_POINTER ||
+ kind == TSR_KIND_VTABLE_PTR ||
+ kind == TSR_KIND_VFUNC_PTR);
- if (kind == TSR_KIND_POINTER) {
+ if (is_pointer) {
/* TODO: arch-dependent pointer size */
size = sizeof(void *);
}
@@ -729,7 +740,7 @@ void set_stack_state(struct type_state_stack *stack, int offset, u8 kind,
stack->ptr_offset = ptr_offset;
stack->kind = kind;
- if (kind == TSR_KIND_POINTER) {
+ if (is_pointer) {
stack->compound = false;
return;
}
diff --git a/tools/perf/util/annotate-data.h b/tools/perf/util/annotate-data.h
index bbf4fd35c1d7..14dab1beae2c 100644
--- a/tools/perf/util/annotate-data.h
+++ b/tools/perf/util/annotate-data.h
@@ -36,6 +36,10 @@ enum type_state_kind {
TSR_KIND_CONST,
TSR_KIND_PERCPU_POINTER,
TSR_KIND_POINTER,
+ /* C++ vtable pointer of the class in 'type' */
+ TSR_KIND_VTABLE_PTR,
+ /* C++ virtual function pointer with the return type in 'type' */
+ TSR_KIND_VFUNC_PTR,
TSR_KIND_CANARY,
};
diff --git a/tools/perf/util/disasm.c b/tools/perf/util/disasm.c
index 5478c134e7e3..5d96ee4c866b 100644
--- a/tools/perf/util/disasm.c
+++ b/tools/perf/util/disasm.c
@@ -300,6 +300,11 @@ static int call__parse(const struct arch *arch, struct ins_operands *ops, struct
if (tok != NULL) {
endptr++;
+ /* Save the operand (without '*') to extract register and offset */
+ ops->target.raw = strdup(tok + 1);
+ if (ops->target.raw == NULL)
+ return -1;
+
/* Indirect call can use a non-rip register and offset: callq *0x8(%rbx).
* Do not parse such instruction. */
if (strstr(endptr, "(%r") == NULL)
--
2.53.0
next prev parent reply other threads:[~2026-09-30 21:00 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-30 21:00 [PATCH v2 0/3] perf annotate: Data type profiling support for C++ classes and virtual calls Yanbo Zhao
2026-09-30 21:00 ` [PATCH v2 1/3] perf dwarf-aux: Add die_is_compound_type() to handle C++ class types Yanbo Zhao
2026-09-30 21:10 ` sashiko-bot
2026-10-01 18:14 ` Namhyung Kim
2026-10-02 16:34 ` Arnaldo Carvalho de Melo
2026-10-04 19:54 ` Yanbo Zhao
2026-09-30 21:00 ` [PATCH v2 2/3] perf dwarf-aux: Add C++ vtable helpers Yanbo Zhao
2026-09-30 21:10 ` sashiko-bot
2026-10-02 22:52 ` Namhyung Kim
2026-10-04 19:56 ` Yanbo Zhao
2026-09-30 21:00 ` Yanbo Zhao [this message]
2026-09-30 21:11 ` [PATCH v2 3/3] perf annotate: Resolve C++ virtual function calls in x86 insn tracking sashiko-bot
2026-10-02 22:56 ` Namhyung Kim
2026-10-04 19:57 ` Yanbo Zhao
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260930210038.196928-4-yzhao62@ncsu.edu \
--to=yzhao62@ncsu.edu \
--cc=acme@kernel.org \
--cc=adrian.hunter@intel.com \
--cc=alexander.shishkin@linux.intel.com \
--cc=irogers@google.com \
--cc=james.clark@linaro.org \
--cc=jolsa@kernel.org \
--cc=kan.liang@linux.intel.com \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-perf-users@vger.kernel.org \
--cc=mark.rutland@arm.com \
--cc=mingo@kernel.org \
--cc=namhyung@kernel.org \
--cc=peterz@infradead.org \
--cc=xliuprof@google.com \
--cc=zli94@ncsu.edu \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox