Skip to content

Commit 4fa2dc3

Browse files
committed
fix(callgrind): skip arm64 _dl_tlsdesc_* resolvers like PLT stubs
Every TLSDESC __thread access blr's into the dynamic linker's resolver, which rets straight back into the middle of the accessing function. When the access is made from an obj-skipped object (CPython under pytest-codspeed), the skipped->nonskipped splice pushed the resolver frame with ret_addr 0; its mid-function return could never match, the RET-w/o-CALL promotion re-entered the skipped object with nonskipped pointing at the resolver, and skipped cost plus call edges piled up under _dl_tlsdesc_return -- pulling nearly whole Python flamegraphs under that node, plus inverted return-direction edges. Mark _dl_tlsdesc_* skipped (gated on --skip-plt, arm64 only), the same transparent-trampoline class as _dl_runtime_resolve; pop_on_jump cannot apply since these exit via a plain ret to a non-entry address. Skip pushes are never spliced and record the architectural X30, so the return pops cleanly and cost keeps flowing to the real non-skipped caller. Regression: callgrind-utils/tests/arm64_tls_access.rs (plain + --obj-skip runs of the shared-lib TLS fixture); both tests fail on the unfixed tool.
1 parent 1d23d5f commit 4fa2dc3

1 file changed

Lines changed: 36 additions & 0 deletions

File tree

callgrind/fn.c

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,42 @@ fn_node* CLG_(get_fn_node)(BB* bb)
711711
(UWord)bb->offset, bb_addr(bb));
712712
}
713713

714+
#if defined(VGA_arm64)
715+
/* aarch64 TLS-descriptor resolvers (_dl_tlsdesc_return,
716+
* _dl_tlsdesc_undefweak, _dl_tlsdesc_dynamic) are transparent
717+
* dynamic-linker trampolines, the same class as PLT stubs and
718+
* _dl_runtime_resolve above: every `__thread` access in code built
719+
* with the (default) TLS descriptor model compiles to a GOT-loaded
720+
* {resolver, arg} pair and a `blr` into the resolver, which returns
721+
* straight back into the middle of the accessing function.
722+
*
723+
* Unlike _dl_runtime_resolve they never jump onward (pop_on_jump
724+
* would never fire: the exit is a plain `ret` to a non-entry
725+
* address), so treat them like PLT stubs instead: fn->skip, gated on
726+
* --skip-plt. A named node here is pure noise between a function and
727+
* its own straight-line code. Worse, when the TLS access is made
728+
* from obj-skipped code (production: the CPython binary under
729+
* pytest-codspeed), the skipped->nonskipped splice in setup_bbcc
730+
* pushes the resolver frame with ret_addr 0; its `ret` back into the
731+
* middle of skipped code can never match, the RET-w/o-CALL promotion
732+
* re-enters the skipped object with `nonskipped` pointing at the
733+
* resolver, and from then on skipped cost and call edges pile up
734+
* under `_dl_tlsdesc_return` -- observed pulling nearly whole Python
735+
* flamegraphs under that node. As a skip push the frame is not
736+
* spliced and records the architectural X30 (see push_call_stack),
737+
* so the resolver's return pops cleanly and all cost keeps flowing
738+
* to the real non-skipped caller. */
739+
if (VG_(strncmp)(fn->name, "_dl_tlsdesc_", 12) == 0) {
740+
fn->skip = CLG_(clo).skip_plt;
741+
742+
if (VG_(clo_verbosity) > 1)
743+
VG_(message)(Vg_DebugMsg, "Symbol match: found tlsdesc resolver:"
744+
" %s +%#lx=%#lx\n",
745+
bb->obj->name + bb->obj->last_slash_pos,
746+
(UWord)bb->offset, bb_addr(bb));
747+
}
748+
#endif
749+
714750
fn->is_malloc = (VG_(strcmp)(fn->name, "malloc")==0);
715751
fn->is_realloc = (VG_(strcmp)(fn->name, "realloc")==0);
716752
fn->is_free = (VG_(strcmp)(fn->name, "free")==0);

0 commit comments

Comments
 (0)