[PATCH][middle-end][i386][version 5]Add -fzero-call-used-regs=[skip|used-gpr-arg|used-arg|all-gpr-arg|all-arg|used-gpr|all-gpr|used|all]
Richard Sandiford
richard.sandiford@arm.com
Thu Oct 29 11:09:53 GMT 2020
Qing Zhao via Gcc-patches <gcc-patches@gcc.gnu.org> writes:
> +/* Handle a "zero_call_used_regs" attribute; arguments as in
> + struct attribute_spec.handler. */
> +
> +static tree
> +handle_zero_call_used_regs_attribute (tree *node, tree name, tree args,
> + int ARG_UNUSED (flags),
> + bool *no_add_attrs)
> +{
> + tree decl = *node;
> + tree id = TREE_VALUE (args);
> +
> + if (TREE_CODE (decl) != FUNCTION_DECL)
> + {
> + error_at (DECL_SOURCE_LOCATION (decl),
> + "%qE attribute applies only to functions", name);
> + *no_add_attrs = true;
> + }
> +
> + if (TREE_CODE (id) != STRING_CST)
> + {
> + error_at (DECL_SOURCE_LOCATION (decl),
> + "attribute %qE arguments not a string", name);
The existing message for this seems to be:
"%qE argument not a string"
(which seems a bit terse, but hey)
> + *no_add_attrs = true;
> + }
> +
> + bool found = false;
> + for (unsigned int i = 0; zero_call_used_regs_opts[i].name != NULL; ++i)
> + if (strcmp (TREE_STRING_POINTER (id),
> + zero_call_used_regs_opts[i].name) == 0)
> + {
> + found = true;
> + break;
> + }
> +
> + if (!found)
> + {
> + error_at (DECL_SOURCE_LOCATION (decl),
> + "unrecognized zero_call_used_regs attribute: %qs",
> + TREE_STRING_POINTER (id));
The attribute name needs to be quoted, and it would be good if it
wasn't hard-coded into the string:
error_at (DECL_SOURCE_LOCATION (decl),
"unrecognized %qE attribute argument %qs", name,
TREE_STRING_POINTER (id));
> @@ -228,6 +228,10 @@ unsigned int flag_sanitize_coverage
> Variable
> bool dump_base_name_prefixed = false
>
> +; What subset of registers should be zeroed
Think it would be useful to add “ on function return.”.
> +Variable
> +unsigned int flag_zero_call_used_regs
> +
> ###
> Driver
>
> diff --git a/gcc/df.h b/gcc/df.h
> index 8b6ca8c..0f098d7 100644
> --- a/gcc/df.h
> +++ b/gcc/df.h
> @@ -1085,6 +1085,7 @@ extern void df_update_entry_exit_and_calls (void);
> extern bool df_hard_reg_used_p (unsigned int);
> extern unsigned int df_hard_reg_used_count (unsigned int);
> extern bool df_regs_ever_live_p (unsigned int);
> +extern bool df_epilogue_uses_p (unsigned int);
> extern void df_set_regs_ever_live (unsigned int, bool);
> extern void df_compute_regs_ever_live (bool);
> extern void df_scan_verify (void);
> diff --git a/gcc/doc/extend.texi b/gcc/doc/extend.texi
> index c9f7299..b011c17 100644
> --- a/gcc/doc/extend.texi
> +++ b/gcc/doc/extend.texi
> @@ -3992,6 +3992,96 @@ performing a link with relocatable output (i.e.@: @code{ld -r}) on them.
> A declaration to which @code{weakref} is attached and that is associated
> with a named @code{target} must be @code{static}.
>
> +@item zero_call_used_regs ("@var{choice}")
> +@cindex @code{zero_call_used_regs} function attribute
> +
> +The @code{zero_call_used_regs} attribute causes the compiler to zero
> +a subset of all call-used registers at function return according to
> +@var{choice}.
Suggest dropping “according to @var{choice}” here, since it's now
disconnected with the part that talks about what @var{choice} is.
> +This is used to increase the program security by either mitigating
s/the program security/program security/
> +Return-Oriented Programming (ROP) or preventing information leak
leakage
(FWIW, I'm not sure “mitigating ROP” is really correct usage, but I don't
have any better suggestions.)
> +through registers.
> +
> +A ``call-used'' register is a register whose contents can be changed by
> +a function call; therefore, a caller cannot assume that the register has
> +the same contents on return from the function as it had before calling
> +the function. Such registers are also called ``call-clobbered'',
> +``caller-saved'', or ``volatile''.
Reading it back, perhaps it would be better to put this whole paragraph
in a footnote immediately after the first use of “call-used registers”,
i.e.
…call-used registers@footnote{A ``call-used'' register…}…
It obviously breaks the flow when reading the raw .texi, but I think it
reads better in the final version.
> +In order to satisfy users with different security needs and control the
> +run-time overhead at the same time, GCC provides a flexible way to choose
> +the subset of the call-used registers to be zeroed.
Maybe s/GCC/the @var{choice} parameter/.
> +
> +The three basic values of @var{choice} are:
After which, I think this should be part of the previous paragraph.
> +@itemize @bullet
> +@item
> +@samp{skip} doesn't zero any call-used registers.
> +
> +@item
> +@samp{used} only zeros call-used registers that are used in the function.
> +A ``used'' register is one whose content has been set or referenced in
> +the function.
> +
> +@item
> +@samp{all} zeros all call-used registers.
> +@end itemize
> +
> +In addition to these three basic choices, it is possible to modify
> +@samp{used} or @samp{all} as follows:
> +
> +@itemize @bullet
> +@item
> +Adding @samp{-gpr} restricts the zeroing to general-purpose registers.
> +
> +@item
> +Adding @samp{-arg} restricts the zeroing to registers that can sometimes
> +be used to pass function arguments. This includes all argument registers
> +defined by the platform's calling conversion, regardless of whether the
> +function uses those registers for function arguments or not.
> +@end itemize
> +
> +The modifiers can be used individually or together. If they are used
> +together, they must appear in the order above.
> +
> +The full list of @var{choice}s is therefore:
> +
> +@itemize @bullet
> +@item
> +@samp{skip} doesn't zero any call-used register.
> +
> +@item
> +@samp{used} only zeros call-used registers that are used in the function.
> +
> +@item
> +@samp{all} zeros all call-used registers.
> +
> +@item
> +@samp{used-arg} only zeros used call-used registers that pass arguments.
> +
> +@item
> +@samp{used-gpr} only zeros used call-used general purpose registers.
> +
> +@item
> +@samp{used-gpr-arg} only zeros used call-used general purpose registers that
> +pass arguments.
> +
> +@item
> +@samp{all-gpr-arg} zeros all call-used general purpose registers that pass
> +arguments.
> +
> +@item
> +@samp{all-arg} zeros all call-used registers that pass arguments.
> +
> +@item
> +@samp{all-gpr} zeros all call-used general purpose registers.
> +@end itemize
By using a table, I meant:
@table @samp
@item skip
…
@item used
…
etc.
@end @table
Did you try that and think the output looked odd?
I think the order should be more consistent. E.g. “all” should be listed
with the other “all” options, and the ordering of foos for “used-foo”
and “all-foo” should be the same. So maybe:
- skip
- used
- used-arg
- used-gpr
- used-gpr-arg
- all
- all-arg
- all-gpr
- all-gpr-arg
> +Among this list, @samp{used-gpr-arg}, @samp{used-arg}, @samp{all-gpr-arg},
IMO s/Among/Of/ reads slightly better.
> diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi
> index c049932..c9e3128 100644
> --- a/gcc/doc/invoke.texi
> +++ b/gcc/doc/invoke.texi
> @@ -550,7 +550,7 @@ Objective-C and Objective-C++ Dialects}.
> -funit-at-a-time -funroll-all-loops -funroll-loops @gol
> -funsafe-math-optimizations -funswitch-loops @gol
> -fipa-ra -fvariable-expansion-in-unroller -fvect-cost-model -fvpt @gol
> --fweb -fwhole-program -fwpa -fuse-linker-plugin @gol
> +-fweb -fwhole-program -fwpa -fuse-linker-plugin -fzero-call-used-regs @gol
> --param @var{name}=@var{value}
> -O -O0 -O1 -O2 -O3 -Os -Ofast -Og}
>
> @@ -12550,6 +12550,19 @@ int foo (void)
>
> Not all targets support this option.
>
> +@item -fzero-call-used-regs=@var{choice}
> +@opindex fzero-call-used-regs
> +Zero call-used registers at function return to increase the program
s/the program/program/
> +security by either mitigating Return-Oriented Programming (ROP) or
> +preventing information leak through registers.
s/leak/leakage/
> @@ -173,6 +173,9 @@ struct GTY(()) rtl_data {
> local stack. */
> unsigned int stack_alignment_estimated;
>
> + /* How to zero call-used regsiters for this routine. */
> + unsigned int zero_call_used_regs;
> +
Typo: regsiters. But I don't think we need to add this to crtl.
It's just data that's passed between pass_zero_call_used_regs::execute
and gen_call_used_regs_seq.
> /* How many NOP insns to place at each function entry by default. */
> unsigned short patch_area_size;
>
> @@ -285,6 +287,24 @@ enum sanitize_code {
> | SANITIZE_BOUNDS_STRICT
> };
>
> +/* Different settings for zeroing subset of registers. */
> +namespace zero_regs_code {
Should only be one space after “namespace”. Having “code” in the name
surprised me, think “flags” would be better.
> @@ -5815,6 +5817,101 @@ make_prologue_seq (void)
> return seq;
> }
>
> +/* Emit a sequence of insns to zero the call-used registers before RET. */
> +using namespace zero_regs_code;
Making the “using” file-wide is too much, since the file has a lot of code
unrelated to this feature. It should go in the function body instead.
> +
> +static void
> +gen_call_used_regs_seq (rtx_insn *ret)
> +{
> + bool gpr_only = true;
> + bool used_only = true;
> + bool arg_only = true;
> +
> + /* No need to zero call-used-regs in main (). */
> + if (MAIN_NAME_P (DECL_NAME (current_function_decl)))
> + return;
> +
> + /* 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;
> +
> + /* If gpr_only is true, only zero call-used registers that are
> + general-purpose registers; if used_only is true, only zero
> + call-used registers that are used in the current function;
> + if arg_only is true, only zero call-used registers that pass
> + parameters defined by the flatform's calling conversion. */
> +
> + gpr_only = crtl->zero_call_used_regs & ONLY_GPR;
> + used_only = crtl->zero_call_used_regs & ONLY_USED;
> + arg_only = crtl->zero_call_used_regs & ONLY_ARG;
Guess it would be nice to be consistent about which side the “only”
goes on. FWIW, I don't mind which way: GPR_ONLY etc. would be
OK with me if you prefer that.
> +
> + /* For each of the hard registers, we should zero it if:
> + 1. it is a call-used register;
> + and 2. it is not a fixed register;
> + and 3. it is not live at the return of the routine;
> + and 4. it is general registor if gpr_only is true;
> + and 5. it is used in the routine if used_only is true;
> + and 6. it is a register that passes parameter if arg_only is true. */
> +
> + /* First, prepare the data flow information. */
> + basic_block bb = BLOCK_FOR_INSN (ret);
> + auto_bitmap live_out;
> + bitmap_copy (live_out, df_get_live_out (bb));
> + df_simulate_initialize_backwards (bb, live_out);
> + df_simulate_one_insn_backwards (bb, ret, live_out);
> +
> + HARD_REG_SET selected_hardregs;
> + CLEAR_HARD_REG_SET (selected_hardregs);
> + for (unsigned int regno = 0; regno < FIRST_PSEUDO_REGISTER; regno++)
> + {
> + if (!crtl->abi->clobbers_full_reg_p (regno))
> + continue;
> + if (fixed_regs[regno])
> + continue;
> + if (REGNO_REG_SET_P (live_out, regno))
> + continue;
> + if (gpr_only
> + && !TEST_HARD_REG_BIT (reg_class_contents[GENERAL_REGS], regno))
> + continue;
> + if (used_only && !df_regs_ever_live_p (regno))
> + continue;
> + if (arg_only && !FUNCTION_ARG_REGNO_P (regno))
> + continue;
> +
> + /* Now this is a register that we might want to zero. */
> + SET_HARD_REG_BIT (selected_hardregs, regno);
> + }
> +
> + if (hard_reg_set_empty_p (selected_hardregs))
> + return;
> +
> + /* Now that we have a hard register set that needs to be zeroed, pass it to
> + target to generate zeroing sequence. */
> + HARD_REG_SET zeroed_hardregs;
> + start_sequence ();
> + zeroed_hardregs = targetm.calls.zero_call_used_regs (selected_hardregs);
> + rtx_insn *seq = get_insns ();
> + 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 = get_insns ();
> + end_sequence ();
> +
> + emit_insn_before (seq_barrier, ret);
> + emit_insn_before (seq, ret);
> +
> + /* Update the data flow information. */
> + crtl->must_be_zero_on_return |= zeroed_hardregs;
> + df_set_bb_dirty (EXIT_BLOCK_PTR_FOR_FN (cfun));
> + }
> +}
> +
> +
> /* Return a sequence to be used as the epilogue for the current function,
> or NULL. */
>
> @@ -6486,7 +6583,100 @@ make_pass_thread_prologue_and_epilogue (gcc::context *ctxt)
> {
> return new pass_thread_prologue_and_epilogue (ctxt);
> }
> -
>
> +
> +namespace {
> +
> +const pass_data pass_data_zero_call_used_regs =
> +{
> + RTL_PASS, /* type */
> + "zero_call_used_regs", /* name */
> + OPTGROUP_NONE, /* optinfo_flags */
> + TV_NONE, /* tv_id */
> + 0, /* properties_required */
> + 0, /* properties_provided */
> + 0, /* properties_destroyed */
> + 0, /* todo_flags_start */
> + 0, /* todo_flags_finish */
> +};
> +
> +class pass_zero_call_used_regs: public rtl_opt_pass
> +{
> +public:
> + pass_zero_call_used_regs (gcc::context *ctxt)
> + : rtl_opt_pass (pass_data_zero_call_used_regs, ctxt)
> + {}
> +
> + /* opt_pass methods: */
> + virtual unsigned int execute (function *);
> +
> +}; // class pass_zero_call_used_regs
> +
> +unsigned int
> +pass_zero_call_used_regs::execute (function *fun)
> +{
> + unsigned int zero_regs_type = UNSET;
> + unsigned int attr_zero_regs_type = UNSET;
> +
> + tree attr_zero_regs
> + = lookup_attribute ("zero_call_used_regs",
> + DECL_ATTRIBUTES (fun->decl));
The “= …” line should be indented by only two extra spaces. Although
in this case it fits on two lines anyway:
tree attr_zero_regs = lookup_attribute ("zero_call_used_regs",
DECL_ATTRIBUTES (fun->decl));
> + /* Get the type of zero_call_used_regs from function attribute. */
“from the function attribute” Might be worth adding “We have already
filtered out invalid attribute values.”, to explain why there's (rightly)
no failure path.
> + if (attr_zero_regs)
> + {
> + /* The TREE_VALUE of an attribute is a TREE_LIST whose TREE_VALUE
> + is the attribute argument's value. */
> + attr_zero_regs = TREE_VALUE (attr_zero_regs);
> + gcc_assert (TREE_CODE (attr_zero_regs) == TREE_LIST);
> + attr_zero_regs = TREE_VALUE (attr_zero_regs);
> + gcc_assert (TREE_CODE (attr_zero_regs) == STRING_CST);
> +
> + for (unsigned int i = 0; zero_call_used_regs_opts[i].name != NULL; ++i)
> + if (strcmp (TREE_STRING_POINTER (attr_zero_regs),
> + zero_call_used_regs_opts[i].name) == 0)
> + {
> + attr_zero_regs_type = zero_call_used_regs_opts[i].flag;
> + break;
> + }
> + }
> +
> + zero_regs_type = attr_zero_regs_type;
> +
> + if (!zero_regs_type)
> + zero_regs_type = flag_zero_call_used_regs;
Having two variables seems unnecessarily complicated. I think the
attribute code should assign directly to “zero_regs_type”.
> +
> + crtl->zero_call_used_regs = zero_regs_type;
> +
> + /* No need to zero call-used-regs when no user request is present. */
> + if (!(zero_regs_type & ENABLED))
> + return 0;
> +
> + edge_iterator ei;
> + edge e;
> +
> + /* This pass needs data flow information. */
> + df_analyze ();
> +
> + /* 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. */
> + 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);
As noted above, we could just pass “zero_regs_type” here rather than
store it in crtl.
> @@ -987,6 +990,35 @@ default_function_value_regno_p (const unsigned int regno ATTRIBUTE_UNUSED)
> #endif
> }
>
> +/* The default hook for TARGET_ZERO_CALL_USED_REGS. */
> +
> +HARD_REG_SET
> +default_zero_call_used_regs (HARD_REG_SET need_zeroed_hardregs)
> +{
> + gcc_assert (!hard_reg_set_empty_p (need_zeroed_hardregs));
> +
> + for (unsigned int regno = 0; regno < FIRST_PSEUDO_REGISTER; regno++)
> + if (TEST_HARD_REG_BIT (need_zeroed_hardregs, regno))
> + {
> + rtx_insn *last_insn = get_last_insn ();
> + machine_mode mode = GET_MODE (regno_reg_rtx[regno]);
> + rtx zero = CONST0_RTX (mode);
> + rtx_insn *insn = emit_move_insn (regno_reg_rtx[regno], zero);
> + if (!valid_insn_p (insn))
> + {
> + static bool issued_error;
> + if (!issued_error)
> + {
> + issued_error = true;
> + sorry ("%qs not supported on this target",
> + "fzero-call-used_regs");
"-fzero-call-used_regs"
> + }
> + delete_insns_since (last_insn);
> + }
> + }
> + return need_zeroed_hardregs;
> +}
> +
> rtx
> default_internal_arg_pointer (void)
> {
> diff --git a/gcc/targhooks.h b/gcc/targhooks.h
> index 44ab926..e0a925f 100644
> --- a/gcc/targhooks.h
> +++ b/gcc/targhooks.h
> @@ -160,6 +160,7 @@ extern unsigned int default_function_arg_round_boundary (machine_mode,
> const_tree);
> extern bool hook_bool_const_rtx_commutative_p (const_rtx, int);
> extern rtx default_function_value (const_tree, const_tree, bool);
> +extern HARD_REG_SET default_zero_call_used_regs (HARD_REG_SET);
> extern rtx default_libcall_value (machine_mode, const_rtx);
> extern bool default_function_value_regno_p (const unsigned int);
> extern rtx default_internal_arg_pointer (void);
> diff --git a/gcc/testsuite/c-c++-common/zero-scratch-regs-1.c b/gcc/testsuite/c-c++-common/zero-scratch-regs-1.c
> new file mode 100644
> index 0000000..2463353
> --- /dev/null
> +++ b/gcc/testsuite/c-c++-common/zero-scratch-regs-1.c
> @@ -0,0 +1,15 @@
> +/* { dg-do run } */
> +/* { dg-options "-O2 -fzero-call-used-regs=skip" } */
> +
> +volatile int result = 0;
> +int
> +__attribute__((noipa))
> +foo (int x)
> +{
> + return x;
> +}
> +int main()
> +{
> + result = foo (2);
> + return 0;
> +}
> diff --git a/gcc/testsuite/c-c++-common/zero-scratch-regs-10.c b/gcc/testsuite/c-c++-common/zero-scratch-regs-10.c
> new file mode 100644
> index 0000000..bdaf8e7
> --- /dev/null
> +++ b/gcc/testsuite/c-c++-common/zero-scratch-regs-10.c
> @@ -0,0 +1,92 @@
> +/* { dg-do run } */
> +/* { dg-options "-O2" } */
> +
> +#include <assert.h>
> +int result = 0;
> +
> +int
> +__attribute__((noipa))
> +__attribute__ ((zero_call_used_regs("skip")))
> +foo1 (int x)
> +{
> + return (x + 1);
> +}
> +
> +int
> +__attribute__((noipa))
> +__attribute__ ((zero_call_used_regs("used-gpr-arg")))
> +foo2 (int x)
> +{
> + return (x + 2);
> +}
> +
> +int
> +__attribute__((noipa))
> +__attribute__ ((zero_call_used_regs("used-gpr")))
> +foo3 (int x)
> +{
> + return (x + 3);
> +}
> +
> +int
> +__attribute__((noipa))
> +__attribute__ ((zero_call_used_regs("used-arg")))
> +foo4 (int x)
> +{
> + return (x + 4);
> +}
> +
> +int
> +__attribute__((noipa))
> +__attribute__ ((zero_call_used_regs("used")))
> +foo5 (int x)
> +{
> + return (x + 5);
> +}
> +
> +int
> +__attribute__((noipa))
> +__attribute__ ((zero_call_used_regs("all-gpr-arg")))
> +foo6 (int x)
> +{
> + return (x + 6);
> +}
> +
> +int
> +__attribute__((noipa))
> +__attribute__ ((zero_call_used_regs("all-gpr")))
> +foo7 (int x)
> +{
> + return (x + 7);
> +}
> +
> +int
> +__attribute__((noipa))
> +__attribute__ ((zero_call_used_regs("all-arg")))
> +foo8 (int x)
> +{
> + return (x + 8);
> +}
> +
> +int
> +__attribute__((noipa))
> +__attribute__ ((zero_call_used_regs("all")))
> +foo9 (int x)
> +{
> + return (x + 9);
> +}
> +
> +int main()
> +{
> + result = foo1 (1);
> + result += foo2 (1);
> + result += foo3 (1);
> + result += foo4 (1);
> + result += foo5 (1);
> + result += foo6 (1);
> + result += foo7 (1);
> + result += foo8 (1);
> + result += foo9 (1);
> + assert (result == 54);
> + return 0;
> +}
> diff --git a/gcc/testsuite/c-c++-common/zero-scratch-regs-11.c b/gcc/testsuite/c-c++-common/zero-scratch-regs-11.c
> new file mode 100644
> index 0000000..6756f57
> --- /dev/null
> +++ b/gcc/testsuite/c-c++-common/zero-scratch-regs-11.c
> @@ -0,0 +1,92 @@
> +/* { dg-do run } */
> +/* { dg-options "-O2 -fzero-call-used-regs=all" } */
> +
> +#include <assert.h>
> +int result = 0;
> …
I think this should just #include "zero-scratch-regs-10.c".
> diff --git a/gcc/testsuite/c-c++-common/zero-scratch-regs-2.c b/gcc/testsuite/c-c++-common/zero-scratch-regs-2.c
> new file mode 100644
> index 0000000..73c3794
> --- /dev/null
> +++ b/gcc/testsuite/c-c++-common/zero-scratch-regs-2.c
> @@ -0,0 +1,15 @@
> +/* { dg-do run } */
> +/* { dg-options "-O2 -fzero-call-used-regs=used-gpr-arg" } */
> +
> +volatile int result = 0;
> +int
> +__attribute__((noipa))
> +foo (int x)
> +{
> + return x;
> +}
> +int main()
> +{
> + result = foo (2);
> + return 0;
> +}
Similarly these can just #include "zero-scratch-regs-1.c". This makes
it easier to update the tests in a consistent way in future (if necessary).
> diff --git a/gcc/testsuite/c-c++-common/zero-scratch-regs-attr-usages.c b/gcc/testsuite/c-c++-common/zero-scratch-regs-attr-usages.c
> new file mode 100644
> index 0000000..c60e946
> --- /dev/null
> +++ b/gcc/testsuite/c-c++-common/zero-scratch-regs-attr-usages.c
> @@ -0,0 +1,10 @@
> +/* { dg-do compile} */
> +/* { dg-options "-O2" } */
> +
> +int result __attribute__ ((zero_call_used_regs("all"))); /* { dg-error "attribute applies only to functions" } */
> +int
> +__attribute__ ((zero_call_used_regs("gpr-arg-all")))
> +foo1 (int x) /* { dg-error "unrecognized zero_call_used_regs attribute" } */
> +{
> + return (x + 1);
> +}
Could you also add a test for the string constant check? E.g. with
__attribute__ ((zero_call_used_regs(1))).
Thanks,
Richard
More information about the Gcc-patches
mailing list