> On 7/8/2026 7:38 AM, Jin Ma wrote:
> > The regrename pass runs after prologue/epilogue generation and may
> > rename temporaries to callee-saved registers that the prologue did
> > not save. This corrupts the caller's register values on return or
> > exception unwind.
> >
> > The IRA callee-save cost hooks added by r15-8547-gb191e8bdecf changed
> > the cost model so that IRA leaves more callee-saved registers unused.
> > The epilogue's stack_tie references s0 (hard frame pointer), which
> > makes df_regs_ever_live_p(s0) true after the prologue is generated.
> > regrename's check_new_reg_p relies on df_regs_ever_live_p as a proxy
> > for "saved by prologue", so it treats s0 as available even though it
> > was not saved.
> >
> > Fix by checking frame.mask/frame.fmask/frame.vmask in
> > HARD_REGNO_RENAME_OK instead.
> >
> > gcc/ChangeLog:
> >
> > * config/riscv/riscv.cc (riscv_hard_regno_rename_ok): Reject
> > callee-saved registers not present in frame.mask, frame.fmask,
> > or frame.vmask.
> >
> > gcc/testsuite/ChangeLog:
> >
> > * g++.target/riscv/regrename-unwind.C: New test.
> This sounds like it might be papering over a bug in regrename. In
> theory regrename should never rename to a callee saved register which
> wasn't already used (and thus saved/restored in the prologue/epilogue).
>
> > for (i = nregs - 1; i >= 0; --i)
> > if (TEST_HARD_REG_BIT (this_unavailable, new_reg + i)
> > || fixed_regs[new_reg + i]
> > || global_regs[new_reg + i]
> > /* Can't use regs which aren't saved by the prologue. */
> > || (! df_regs_ever_live_p (new_reg + i)
> > && ! crtl->abi->clobbers_full_reg_p (new_reg + i))
> > #ifdef LEAF_REGISTERS
> > /* We can't use a non-leaf register if we're in a
> > leaf function. */
> > || (crtl->is_leaf
> > && !LEAF_REGISTERS[new_reg + i])
> > #endif
> > || ! HARD_REGNO_RENAME_OK (reg + i, new_reg + i))
> > return false;
>
> Note df_regs_ever_live test, that should be preventing this problem. So
> I think you're going to need to chase down why that's not working properly.
>
> jeff
Hi, Jeff
You're right that regrename's check_new_reg_p should already prevent
this, and that the right fix is to chase down why df_regs_ever_live_p
isn't working here. I believe I've found the root cause.
The RISC-V stack_tie pattern is a length-0 "ghost" insn:
(define_insn "stack_tie<mode>"
[(set (mem:BLK (scratch))
(unspec:BLK [(match_operand:X 0 "register_operand" "r")
(match_operand:X 1 "register_operand" "r")]
UNSPEC_TIE))]
"" ""
[(set_attr "type" "ghost") (set_attr "length" "0")])
It emits no code, but at the RTL level it is a real SET that
references its register operands. The RISC-V backend's
riscv_emit_stack_tie helper hardcodes hard_frame_pointer_rtx
(s0) as the second operand.
In the prologue, every call site guards the tie with
"if (frame_pointer_needed)", and the surrounding comment explains
why: the tie exists only to order stack adjustments WRT frame
pointer setup. When there is no frame pointer, sp is the CFA and
no such ordering barrier is needed.
But three call sites in riscv_expand_epilogue omit that guard:
- calls_alloca path
- "if (known_gt (step1, 0))" (almost every function with a frame)
- "if (need_barrier_p)" (any non-empty frame)
So for a function that has a non-empty frame but does not use a
frame pointer, the epilogue still emits a ghost insn referencing
s0. df_analyze later scans this reference and flips
df_regs_ever_live_p(s0) from false to true -- even though s0 was
never written and never saved by the prologue. regrename's
check_new_reg_p then sees the ever-live bit and allows renaming
into s0, which leads to a segfault when an exception unwinds
through the frame and s0 is not restored.
I verified this by adding the missing "frame_pointer_needed" guards
to the three epilogue tie sites. With only that change, the
original -frename-registers -fprofile-generate reproducer runs
cleanly and regrename no longer touches s0. The check_new_reg_p
logic in regrename.cc is correct as-is.
Proposed fix: add the frame_pointer_needed guard to the three
epilogue stack_tie call sites, mirroring what the prologue already
does. This keeps the tie's anti-load ordering for frame-pointer
functions while avoiding the spurious s0 reference for
non-frame-pointer functions, where sp is the CFA and the barrier
isn't needed.
Do you have any further comments on this?
BR,
Jin Ma