On 9/18/26 3:09 AM, [email protected] wrote:
From: Kyrylo Tkachov <[email protected]>

gen_call_used_regs_seq adds the registers it zeroes to
crtl->must_be_zero_on_return.  df_epilogue_uses_p reports those registers as
used by the epilogue, so they join the exit block use set.  That set in turn
decides which registers a sibling call is recorded as clobbering:
df_get_call_refs skips the clobber def of any register that the exit block
uses.  Growing the set therefore invalidates the recorded defs of every
sibling call in the function, but pass_zero_call_used_regs only refreshed the
artificial refs, through df_update_exit_block_uses.

To hit this a function needs one return path that is a sibling call and
another that is a plain return.  The zeroing sequence goes on the plain
return path while the sibling call keeps its stale defs.  The next pass to
run df_analyze then ICEs in a df checking compiler.  On aarch64 that pass is
narrow_gp_writes:

   during RTL pass: narrow_gp_writes
   internal compiler error: in df_refs_verify, at df-scan.cc:4012
   0x87315f df_refs_verify
   0xed6b66 df_insn_refs_verify
   0xed8b63 df_bb_verify
   0xed8ee7 df_scan_verify()
   0xec2161 df_verify()

For the sibling call insn the recorded chain holds 65 defs while a fresh scan
collects 57, the difference being x0 to x7:

   (call_insn/j:TI 82 147 83 5 (parallel [
               (call (mem:DI (symbol_ref:DI ("bar")))
                   (const_int 0 [0]))
               (unspec:DI [(const_int 2 [0x2])] UNSPEC_CALLEE_ABI)
               (return)
           ]) 110 {*sibcall_insn}

   recorded defs { 0 1 2 3 4 5 6 7 8 ... 18 32 ... 86 16 17 }
   fresh defs    {               8 ... 18 32 ... 86 16 17 }

Call df_update_entry_exit_and_calls once after all return blocks have been
handled.  This is the same fix that PR rtl-optimization/78400 applied to
shrink-wrapping, and thread_prologue_and_epilogue_insns already does it for
the same reason.  The df_update_exit_block_uses call being replaced came from
PR middle-end/100775, which covered only the artificial refs of the exit
block.

df_compute_regs_ever_live also calls df_update_entry_exit_and_calls whenever
regs_ever_live changed, which is why the bug usually hides: when zeroing a
register makes it ever-live for the first time, the next df_analyze repairs
the refs by accident.  The testcase uses -fzero-call-used-regs=used-arg so
that every zeroed register is already live.

The original report used -finstrument-functions-once, which supplies the two
return paths through the tail call to __cyg_profile_func_exit.

Bootstrapped and tested on aarch64-none-linux-gnu.
Ok for trunk?
Thanks,
KYrill

gcc/ChangeLog:

        PR rtl-optimization/127445
        * function.cc (gen_call_used_regs_seq): Return whether a zeroing
        sequence was emitted.  Do not update the exit block uses here.
        (pass_zero_call_used_regs::execute): Call
        df_update_entry_exit_and_calls once when a sequence was emitted.

gcc/testsuite/ChangeLog:

        PR rtl-optimization/127445
        * gcc.target/aarch64/pr127445.c: New test.

Signed-off-by: Kyrylo Tkachov <[email protected]>
OK
jeff

Reply via email to