Why does a noreturn func push LR?

Asked by Gary Fuehrer

My entry point function:
void __attribute__((noreturn)) Reset_Handler() {
L0: goto L0;

compiled (also -O2):
arm-none-eabi-gcc -c -std=gnu11 -mcpu=cortex-m4 -mthumb -Os startup.c

00000204 <Reset_Handler>:
 204: b508 push {r3, lr}
 206: f000 f817 bl 238 <main>
 20a: e7fe b.n 20a <Reset_Handler+0x6>

If I remove the goto, I get the compiler warning about returning from noreturn, of course, but now it emits what I would expect:
00000204 <Reset_Handler>:
 204: f000 b816 b.w 234 <main>


Thank you,

Question information

English Edit question
GNU Arm Embedded Toolchain Edit question
No assignee Edit question
Last query:
Last reply:
Revision history for this message
Thomas Preud'homme (thomas-preudhomme) said :

Hi Gary,

Disclaimer: I haven't looked at the code and am just guessing at this point

I suppose the compilers simply ignores the noreturn to determine whether LR needs to be pushed. All it sees is that main does return and thus generates the push of lr. Try declaring main as nonreturn.

I am surprised that you don't call a startup code though.

Revision history for this message
Gary Fuehrer (gfuehrer) said :

Thank you, Thomas, for taking your time to respond. And for watching my back, but yes I know about the need for crt_init stuff that I left out of my example which is minimized as a mater of courtesy.

My purpose here is to find out definitively, is the ARM embedded gcc compiler expected to optimize out code that preserves LR in a function attributed with noreturn? If so, then I'll submit a bug report. Otherwise I'll submit a feature request.

Now, about attributing 'main' with noreturn. Without knowing the ANSI C11 spec I'd bet there's not much (any?) freedom in the conformant declarations for 'main'. But I took your point, pretended it was some other function, and tired that.

Consequently, I discovered that this breaks the tail-call-or-whatever optimization that's illustrated by my second code example -- the one that replaces the 'bl' instruction with a 'b.w' and, since LR is nowhere overwritten within the calling function, additionally drops the 'push'. Concretely, this:
void __attribute__((noreturn)) derp(void);
void Reset_Handler() {
 <Stuff not overwriting LR>;

Instead yields:
00000204 <Reset_Handler>:
 204: b508 push {r3, lr}
   : <Stuff not overwriting LR>
 224: f000 f838 bl 298 <derp>

Would someone please confirm that I'm not missing something here before I report this bug?


Revision history for this message
Andre Vieira (andre-simoesdiasvieira) said :

Hi Gary,

I don't think this constitutes an actual bug. I have read through https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes and the only two parts that come close to relate to this are:

"The noreturn keyword tells the compiler to assume that fatal cannot return. It can then optimize without regard to what would happen if fatal ever did return. This makes slightly better code."
>> I think we sort of comply with this one, since we don't emit code to restore the pushed registers or return, reducing code size so we are producing slightly better code. And it also says "can optimize" not "will".

"Do not assume that registers saved by the calling function are restored before calling the noreturn function. "
>> Here it speaks of the 'restoring' part and not about the 'saving' part.

Having said that I do agree the push is not needed in your case and can't actually think of a way C would generate code in which it was needed. Given we do not return, there is no way to jump back and thus the value in LR will never be needed.

I don't know how much work it would be to get this working the way you would like it to. So to help you make your case, you might want to justify the effort. Why do you want to get rid of that push so badly. In the end it is merely one extra instruction which by definition must be outside a loop since the function doesn't return, so it is executed only once!

Oh and another "solution" for your case would be to use:

void __attribute__ ((naked)) ResetHandler (void)
  asm ("b.w main");

Probably not an ideal solution though ;)


Revision history for this message
Gary Fuehrer (gfuehrer) said :

Okay, it doesn't need to push LR, thank you for the confirmation.

It was interesting to hear if the C spec has anything to say about the validity of dropping the push as an optimization when a function is attributed with noreturn.

However, I've asked this question:
> ... is the ARM embedded gcc compiler expected to <some behavior>?

And if the answer is yes, then it's something to report as a 'bug', however unimportant to anyone, because developers generally want to know when something in the code they wrote *should* work when in fact doesn't.

And now, because of trying Thomas' idea, I've got two behaviors.

In the case of the first, original one, a noreturn function calls some normal ones. Because it is noreturn, it doesn't need to preserve LR *because* of making function calls with the 'bl' instruction, only for other uses of LR. FWIW I'm betting that this optimization is not yet implemented (ie not a bug, but maybe a feature request).

In the second case, a function calls some function as its last statement and, other than the last instruction which is 'bl', it doesn't otherwise touch LR. There does exist an optimization in this compiler whereby the 'bl' is changed to a 'b.w' and consequently LR is now nowhere touched, so it omits its preservation. But when the called function is attributed noreturn, then this optimization no longer works. FWIW I'm betting that this is unexpected (ie a bug).

Can anyone answer my question in either case?

Thank you,

Revision history for this message
Andre Vieira (andre-simoesdiasvieira) said :

Hi Gary,

Your first question, yes, I think it might be worth looking into making all noreturn functions not push LR.

As for your second case, I will admit the behavior is rather suspect.

If you have:

void foo (void);

void __attribute__ ((noreturn)) bar (void)
  foo ();

You get a warning about foo not being a noreturn function, but you do end up with:

  b foo

Whereas if you add the noreturn attribute to foo, you end up with no warning but:

  push {r4, lr}
  bl bar

That doesn't make any sense to me. Feel free to report it.

Revision history for this message
David Brown (davidbrown) said :

Also if you write:

extern void foo(void);
void bar(void)

Then the __builtin_unreachable() causes the "push {r4, lr}" instruction.

Can you help with this problem?

Provide an answer of your own, or ask Gary Fuehrer for more information if necessary.

To post a message you must log in.