Skip to content

Commit 7cb97c6

Browse files
not-matthiasclaude
andcommitted
fix(callgrind): correct AArch64/PPC call-return detection for SP-equal frames
On AArch64/PPC the call instruction leaves SP unchanged (return address in the link register), so a callee's shadow-stack entry frame sits at SP *equal* to its return target rather than strictly below it as on x86. Three defects in the call/return classifier corrupted Simulation flamegraphs on AArch64 only (malloc -> driftsort_main, dealloc/sin parenting the workload, etc.) by inverting call edges and fabricating '2 recursion clones: 1. CLG_(unwind_call_stack): minpops is meant to bound only SP-equal pops, but was decremented for SP-lower pops too. A callee's still-open sub-call frames exhausted the budget before its own SP-equal entry frame was reached, leaving it stuck. SP-lower frames now pop unconditionally without consuming the budget. 2. setup_bbcc return classifier: when the returning function had made SP-lower sub-calls, popcount_on_return defaulted to 1 and left a PLT stub+callee pair or a tail-call chain (which all share the SP-equal block) stuck. It now skips the SP-lower frames and pops the SP-equal block down to the deepest frame matching the return target. Keyed on where the top frame sits, so the x86 paths (SP-lower-top return, SP-deeper jump) are preserved exactly; only the AArch64/PPC SP-equal case is changed. 3. reconstruct_call_stack_from_native: seeded ret_addr was ips[frame+1], i.e. return_PC - 1 (VG_(get_StackTrace) reports the call insn, not the return PC), but the classifier matches against the return PC. Normalize with +1 so returns across mid-run-seeded frames are not demoted to jumps. COD-2985. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ce9d871 commit 7cb97c6

2 files changed

Lines changed: 111 additions & 32 deletions

File tree

callgrind/bbcc.c

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -654,37 +654,80 @@ void CLG_(setup_bbcc)(BB* bb)
654654

655655
csp = CLG_(current_call_stack).sp;
656656

657-
/* A return not matching the top call in our callstack is a jump */
657+
/* Classify a `ret` and compute popcount_on_return: how many *SP-equal* shadow
658+
* frames it must pop. SP-lower frames (sub-calls the returning function made,
659+
* and on x86 the returning frame itself, since `call` pushes a return address)
660+
* always unwind and are not counted here. The three cases below are keyed on
661+
* where the top (newest, lowest-SP) frame sits relative to the post-return SP,
662+
* preserving the original x86 behaviour exactly: on x86 a return is always the
663+
* SP-lower-top case and never demotes; the SP-equal-top case only arises on
664+
* AArch64/PPC, where the call instruction leaves SP unchanged. */
658665
if ( (jmpkind == jk_Return) && (csp >0)) {
659-
Int csp_up = csp-1;
660-
call_entry* top_ce = &(CLG_(current_call_stack).entry[csp_up]);
661-
662-
/* We have a real return if
663-
* - the stack pointer (SP) left the current stack frame, or
664-
* - SP has the same value as when reaching the current function
665-
* and the address of this BB is the return address of last call
666-
* (we even allow to leave multiple frames if the SP stays the
667-
* same and we find a matching return address)
668-
* The latter condition is needed because on PPC, SP can stay
669-
* the same over CALL=b(c)l / RET=b(c)lr boundaries
670-
*/
671-
if (sp < top_ce->sp) popcount_on_return = 0;
672-
else if (top_ce->sp == sp) {
673-
while(1) {
674-
if (top_ce->ret_addr == bb_addr(bb)) break;
675-
if (csp_up>0) {
676-
csp_up--;
677-
top_ce = &(CLG_(current_call_stack).entry[csp_up]);
678-
if (top_ce->sp == sp) {
679-
popcount_on_return++;
680-
continue;
681-
}
666+
call_entry* top_ce = &(CLG_(current_call_stack).entry[csp-1]);
667+
Bool is_jump = False;
668+
669+
if (top_ce->sp > sp) {
670+
/* Post-return SP is below the top frame's entry SP: SP moved *deeper*,
671+
* so this `ret` did not leave the current frame — treat it as a jump.
672+
* (The top frame has the lowest SP, so every frame is SP-higher here.)
673+
* This is the original `sp < top_ce->sp` case. */
674+
is_jump = True;
675+
}
676+
else {
677+
Addr ret_target = bb_addr(bb);
678+
/* Skip the SP-lower frames the return vacated. On x86 the returning
679+
* frame itself is SP-lower (its `call` pushed a return address), so
680+
* note whether any skipped frame returns to the target: that marks an
681+
* x86-style return *from an SP-lower frame*, where the frame at the
682+
* return SP is the caller we return into — not part of the chain. */
683+
Int i = csp-1;
684+
Bool sp_lower_match = False;
685+
while (i >= 0 && CLG_(current_call_stack).entry[i].sp < sp) {
686+
if (CLG_(current_call_stack).entry[i].ret_addr == ret_target)
687+
sp_lower_match = True;
688+
i--;
689+
}
690+
691+
if (sp_lower_match) {
692+
/* x86-style return: the returning frame was SP-lower and pops on
693+
* its own; the SP-equal frame at the return SP is the caller, which
694+
* must not be popped (it may even share the return address under
695+
* recursion). No SP-equal frames to pop. */
696+
popcount_on_return = 0;
697+
}
698+
else if (i >= 0 && CLG_(current_call_stack).entry[i].sp == sp) {
699+
/* The returning frame is SP-equal — the case that exists where the
700+
* call instruction leaves SP unchanged (AArch64 bl/blr, PPC b(c)l).
701+
* Pop the SP-equal block down to AND INCLUDING the *deepest* frame
702+
* whose recorded return address is the return target; the frame
703+
* below it is the caller we return into. The deepest match pops a
704+
* whole PLT-stub + callee pair (same return address) or a tail-call
705+
* chain a->b->c (c's `ret` lands past b and a) in one go, instead of
706+
* leaving the stub/tail frames stuck — which would re-root the
707+
* caller's continuation under the callee (inverted edges) and
708+
* fabricate '2 recursion clones. */
709+
Int count = 0;
710+
popcount_on_return = 0;
711+
for (Int j = i; j >= 0 && CLG_(current_call_stack).entry[j].sp == sp; j--) {
712+
count++;
713+
if (CLG_(current_call_stack).entry[j].ret_addr == ret_target)
714+
popcount_on_return = count;
682715
}
716+
/* Top frame is SP-equal and nothing in the block returns to the
717+
* target: a `ret` that does not match our call stack — a jump (the
718+
* original SP-equal-top behaviour). */
719+
if (popcount_on_return == 0 && i == csp-1)
720+
is_jump = True;
721+
}
722+
else {
723+
/* An SP-higher caller reached after skipping SP-lower frames (the
724+
* common x86 return), or every frame was SP-lower: no SP-equal
725+
* frames to pop. */
683726
popcount_on_return = 0;
684-
break;
685727
}
686728
}
687-
if (popcount_on_return == 0) {
729+
730+
if (is_jump) {
688731
jmpkind = jk_Jump;
689732
ret_without_call = True;
690733
}
@@ -789,7 +832,12 @@ void CLG_(setup_bbcc)(BB* bb)
789832
CLG_(pop_call_stack)();
790833
}
791834
else {
792-
CLG_ASSERT(popcount_on_return >0);
835+
/* popcount_on_return is the number of *SP-equal* frames this return
836+
* pops; it may legitimately be 0 when the return vacates only SP-lower
837+
* frames (every return on x86, where `call` pushes a return address, and
838+
* AArch64/PPC returns whose SP-equal frame is the caller we stop at).
839+
* unwind_call_stack still pops the SP-lower frames in that case. */
840+
CLG_ASSERT(popcount_on_return >= 0);
793841
CLG_(unwind_call_stack)(sp, popcount_on_return);
794842
}
795843
}

callgrind/callstack.c

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -431,13 +431,33 @@ Int CLG_(unwind_call_stack)(Addr sp, Int minpops)
431431
while( (csp=CLG_(current_call_stack).sp) >0) {
432432
call_entry* top_ce = &(CLG_(current_call_stack).entry[csp-1]);
433433

434-
if ((top_ce->sp < sp) ||
435-
((top_ce->sp == sp) && minpops>0)) {
436-
434+
/* A frame whose entry SP lies strictly below the post-return SP has
435+
* been vacated by this return and is unwound unconditionally.
436+
*
437+
* `minpops` only bounds how many *SP-equal* frames a single return may
438+
* pop (the count setup_bbcc proved belong to this return by matching the
439+
* return target against recorded return addresses). SP-equal entry frames
440+
* exist on targets whose call instruction leaves SP unchanged — AArch64
441+
* bl/blr (return address in the link register) and PPC b(c)l — so a
442+
* callee's own entry frame records the caller's SP and ends up *beneath*
443+
* the SP-lower frames of any sub-calls the callee made (e.g. a libc/libm
444+
* function reached via the PLT that itself calls other functions).
445+
*
446+
* Therefore SP-lower pops must not consume the budget: if they did, the
447+
* still-open sub-call frames would exhaust it before the callee's
448+
* SP-equal entry frame is reached, leaving that frame stuck. A stuck
449+
* frame keeps the callee's context active (so the caller's continuation
450+
* is mis-attributed to the callee — inverted edges) and never decrements
451+
* the callee's recursion depth (fabricating spurious '2 clones). */
452+
if (top_ce->sp < sp) {
453+
unwind_count++;
454+
CLG_(pop_call_stack)();
455+
continue;
456+
}
457+
if ((top_ce->sp == sp) && minpops>0) {
437458
minpops--;
438459
unwind_count++;
439460
CLG_(pop_call_stack)();
440-
csp=CLG_(current_call_stack).sp;
441461
continue;
442462
}
443463
break;
@@ -521,7 +541,18 @@ void CLG_(reconstruct_call_stack_from_native)(ThreadId tid)
521541
* SP, sps[frame+1]; the outermost frame keeps its own SP as nothing
522542
* returns past it during measurement. */
523543
ce->sp = (frame + 1 < (Int)n) ? sps[frame + 1] : sps[frame];
524-
ce->ret_addr = (frame + 1 < (Int)n) ? ips[frame + 1] : 0;
544+
/* setup_bbcc classifies a return by matching the top frame's ret_addr
545+
* against bb_addr(return-target) — the return PC, i.e. the instruction
546+
* after the call — and push_call_stack stores exactly that for real
547+
* calls. VG_(get_StackTrace), however, reports each caller frame at the
548+
* last byte of its call instruction (m_stacktrace.c: `ips[i] = pc - 1`),
549+
* i.e. return_PC - 1. Seeding ret_addr straight from ips[frame+1] would
550+
* therefore be one byte low. That matters on AArch64/PPC, where a `ret`
551+
* lands at SP *equal* to the seeded entry SP so the classifier falls
552+
* back on ret_addr: the off-by-one fails the match, the return is
553+
* demoted to a jump and re-promoted to a call, inverting the edge across
554+
* the seeded frame. Normalize to the return PC with +1. */
555+
ce->ret_addr = (frame + 1 < (Int)n) ? ips[frame + 1] + 1 : 0;
525556
cs->sp++;
526557
ensure_stack_size(cs->sp + 1);
527558
cs->entry[cs->sp].cxt = 0;

0 commit comments

Comments
 (0)