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]>
---
gcc/function.cc | 47 ++++++++++++---------
gcc/testsuite/gcc.target/aarch64/pr127445.c | 23 ++++++++++
2 files changed, 51 insertions(+), 19 deletions(-)
create mode 100644 gcc/testsuite/gcc.target/aarch64/pr127445.c
diff --git a/gcc/function.cc b/gcc/function.cc
index fd9da3003cd..60031f93ea5 100644
--- a/gcc/function.cc
+++ b/gcc/function.cc
@@ -5898,9 +5898,11 @@ make_prologue_seq (void)
}
/* Emit a sequence of insns to zero the call-used registers before RET
- according to ZERO_REGS_TYPE. */
+ according to ZERO_REGS_TYPE. Return true if a sequence was emitted, in
+ which case crtl->must_be_zero_on_return has grown and the caller has to
+ refresh the data flow information. */
-static void
+static bool
gen_call_used_regs_seq (rtx_insn *ret, unsigned int zero_regs_type)
{
bool only_gpr = true;
@@ -5909,12 +5911,12 @@ gen_call_used_regs_seq (rtx_insn *ret, unsigned int
zero_regs_type)
/* No need to zero call-used-regs in main (). */
if (MAIN_NAME_P (DECL_NAME (current_function_decl)))
- return;
+ return false;
/* No need to zero call-used-regs if __builtin_eh_return is called
since it isn't a normal function return. */
if (crtl->calls_eh_return)
- return;
+ return false;
/* If only_gpr is true, only zero call-used registers that are
general-purpose registers; if only_used is true, only zero
@@ -5978,7 +5980,7 @@ gen_call_used_regs_seq (rtx_insn *ret, unsigned int
zero_regs_type)
}
if (hard_reg_set_empty_p (selected_hardregs))
- return;
+ return false;
/* Now that we have a hard register set that needs to be zeroed, pass it to
target to generate zeroing sequence. */
@@ -5995,21 +5997,20 @@ gen_call_used_regs_seq (rtx_insn *ret, unsigned int
zero_regs_type)
gcc_assert (hard_reg_set_subset_p (zeroed_hardregs, all_call_used_regs));
rtx_insn *seq = end_sequence ();
- if (seq)
- {
- /* Emit the memory blockage and register clobber asm volatile before
- the whole sequence. */
- start_sequence ();
- expand_asm_reg_clobber_mem_blockage (zeroed_hardregs);
- rtx_insn *seq_barrier = end_sequence ();
+ if (!seq)
+ return false;
- emit_insn_before (seq_barrier, ret);
- emit_insn_before (seq, ret);
+ /* Emit the memory blockage and register clobber asm volatile before
+ the whole sequence. */
+ start_sequence ();
+ expand_asm_reg_clobber_mem_blockage (zeroed_hardregs);
+ rtx_insn *seq_barrier = end_sequence ();
- /* Update the data flow information. */
- crtl->must_be_zero_on_return |= zeroed_hardregs;
- df_update_exit_block_uses ();
- }
+ emit_insn_before (seq_barrier, ret);
+ emit_insn_before (seq, ret);
+
+ crtl->must_be_zero_on_return |= zeroed_hardregs;
+ return true;
}
@@ -6836,13 +6837,21 @@ pass_zero_call_used_regs::execute (function *fun)
/* Iterate over the function's return instructions and insert any
register zeroing required by the -fzero-call-used-regs command-line
option or the "zero_call_used_regs" function attribute. */
+ bool zeroed = false;
FOR_EACH_EDGE (e, ei, EXIT_BLOCK_PTR_FOR_FN (cfun)->preds)
{
rtx_insn *insn = BB_END (e->src);
if (JUMP_P (insn) && ANY_RETURN_P (JUMP_LABEL (insn)))
- gen_call_used_regs_seq (insn, zero_regs_type);
+ zeroed |= gen_call_used_regs_seq (insn, zero_regs_type);
}
+ /* The registers in must_be_zero_on_return are uses of the exit block, see
+ df_epilogue_uses_p. Growing that set also changes which registers a
+ sibling call is recorded as clobbering, see df_get_call_refs, so the
+ calls have to be rescanned as well. */
+ if (zeroed)
+ df_update_entry_exit_and_calls ();
+
return 0;
}
diff --git a/gcc/testsuite/gcc.target/aarch64/pr127445.c
b/gcc/testsuite/gcc.target/aarch64/pr127445.c
new file mode 100644
index 00000000000..cc0974c9180
--- /dev/null
+++ b/gcc/testsuite/gcc.target/aarch64/pr127445.c
@@ -0,0 +1,23 @@
+/* PR rtl-optimization/127445 */
+/* { dg-do compile } */
+/* { dg-options "-O2 -mnarrow-gp-writes -fzero-call-used-regs=used-arg" } */
+
+/* One return path of foo is a sibling call and the other is a plain return.
+ The zeroing sequence emitted on the plain return path adds the argument
+ registers to the exit block use set, which changes the set of registers
+ that the sibling call is recorded as clobbering. used-arg only zeroes
+ registers that are already live, so nothing else forces a rescan of the
+ call to keep its data flow information correct.
+
+ The stale refs are only diagnosed by a compiler configured with
+ --enable-checking=df, which verifies them in df_analyze. */
+
+int bar (int, int, int, int, int, int, int, int);
+
+int
+foo (int a, int b, int c, int d, int e, int f, int g, int h)
+{
+ if (a)
+ return bar (a, b, c, d, e, f, g, h);
+ return a + b + c + d + e + f + g + h;
+}
--
2.50.1 (Apple Git-155)