2
votes

In an attempt to mitigate the possibility of stack overflow I would like to reset the stack pointer after entering a function that will never return. There are two cases in my code where this occurs, main() and a shutdown_immediate() ISR that saves data to flash and enters deep sleep. I use LTO to make the code fit, so main() ends up being quite a large function that requires allocating part of the stack for local variables.

My first attempt was to use __attribute__ ((noreturn)) combined with a call to __builtin_unreachable(), but that does not change the generated assembly in any way. I then created an inline assembly function to reset the stack pointer to the last SRAM address which is called first in main() and the shutdown_immediate() handler.

inline __attribute__((always_inline)) void NO_RETURN (void)
{
        extern const uint32_t __stack_top__; // Defined in .ld file
        asm volatile ("ldr r3, %[stack_top]\n"
                      "mov sp, r3\n"
                      : /* no outputs */
                      : [stack_top] "m" (__stack_top__)
                      : /* no clobbers */
        );
}

int main (void)
{
    NO_RETURN();

    /* rest of the code here... */
}

void shutdown_immediate (void)
{
    NO_RETURN();
}

This generates seemingly correct code for the ISR. For main() however the mov sp, r3 happens after stack is allocated for local variables etc.. This will fail once main starts branching.

Generated assembly code:

00007f60 <shutdown_immediate>:
    7f60:   b570        push    {r4, r5, r6, lr}
    7f62:   4b21        ldr r3, [pc, #132]  ; (7fe8 <shutdown_immediate+0x88>)
    7f64:   681b        ldr r3, [r3, #0]
    7f66:   469d        mov sp, r3
; ...
    7fe8:   00202000    eoreq   r2, r0, r0 ; last SRAM address


00001180 <main>:
    1180:   b5f0        push    {r4, r5, r6, r7, lr}
    1182:   4be7        ldr r3, [pc, #924]  ; (1520 <main+0x3a0>)
    1184:   b097        sub sp, #92 ; 0x5c ; This SUB must be _after_ 0x1188!
    1186:   681b        ldr r3, [r3, #0]
    1188:   469d        mov sp, r3
; ...
    1520:   00202000    eoreq   r2, r0, r0 ; Last SRAM address

Does anyone have any tricks for how this can be done correctly? I could always create a second variant of the NO_RETURN() function which takes a stack allocation value as an argument, compile, disassemble, compile again and insert the required sub sp, #nn after the mov sp, r3, but that is a messy solution.

Architecture: Cortex-M0

Toolchains tested:

  • gcc-arm-none-eabi-6-2017-q2-update
  • gcc-arm-none-eabi-8-2018-q4-major
1
Subtracting from the stackpointer right after entry is part of the compiler generated function prologue. If you need to reset the stackepointer before that you can either prevent GCC from generating one by using the "naked" attribute (gcc.gnu.org/onlinedocs/gcc/ARM-Function-Attributes.html) or, the probably saner and safer way, you reset the stackpointer before entering main.Vinci
Yes, naked for main is definitely out of the question since only asm code is considered safe in such functions according to gcc's docs. I wanted to reclaim the 20 bytes pushed to the stack in the initial instruction at 0x1180. Since main() is called from the reset handler and never returns, there is no point in pushing these registers to the stack. The reset handler already initialises the stack. My initial hope was that adding the noreturn attribute would prevent the compiler from injecting the push into the prologue, but no such luck.tarjeik
There is likely no way to "correctly" meddle with the stack pointer from C code. Have you tried using main() as an ISR for Reset?A.K.

1 Answers

0
votes

The most robust solution would be to implement main in assembly and jump to user's main from there (renamed as appropriate) once stack has been reset. Hacking around compiler logic would be too fragile in the long term.