jleahy
jleahy

Reputation: 16875

GCC computed goto and value of stack pointer

In GCC you can use a computed goto by taking the address of a label (as in void *addr = &&label) and then jumping to it (jump *addr). The GCC manual says you can jump to this address from any­where in the function, it's only that jumping to it from another function is undefined.

When you jump to the code it cannot assume anything about the values of registers, so presumably it reloads them from memory. However the value of the stack pointer is also not necessarily defined, for example you could be jumping from a nested scope which declares extra variables.

The question is how does GCC manage to set to value of the stack pointer to the correct value (it may be too high or too low)? And how does this interact with -fomit-frame-pointer (if it does)?

Finally, for extra points, what are the real constraints about where you can jump to a label from? For ex­am­ple, you could probably do it from an interrupt handler.

Upvotes: 10

Views: 3521

Answers (3)

Chris Dodd
Chris Dodd

Reputation: 126378

In general, when you have a function with labels whose address is taken, gcc needs to ensure that you can jump to that label from any indirect goto in the function -- so it needs to layout the stack so that the exact stack pointer doesn't matter (everything is indexed off the frame pointer), or that the stack pointer is consistent across all of them. Generally, this means it allocates a fixed amount of stack space when the function starts and never touches the stack pointer afterwards. So if you have inner scopes with variables, the space will be allocated at function start and freed at function end, not in the inner scope. Only the constructor and destructor (if any) need to be tied to the inner scope.

The only constraint on jumping to labels is the one you noted -- you can only do it from within the function that contains the labels. Not from any other stack frame of any other function or interrupt handler or anything.

edit

If you want to be able to jump from one stack frame to another, you need to use setjmp/longjmp or something similar to unwind the stack. You could combine that with an indirect goto -- something like:

if (target = (void *)setjmp(jmpbuf)) goto *target;

that way you could call longjmp(jmpbuf, label_address); from any called function to unwind the stack and then jump to the label. As long as setjmp/longjmp works from an interrupt handler, this will also work from an interrupt handler. Also depends on sizeof(int) == sizeof(void *), which is not always the case.

Upvotes: 12

Jens Gustedt
Jens Gustedt

Reputation: 78943

I don't think that the fact that the goto's are computed add to the effect that it has on local variables. The lifetime of local variable starts from entering their declaration at or beyond their declaration and ends when the scope of the variable cannot be reached in any way. This includes all different sorts of control flow, in particular goto and longjmp. So all such variables are always safe, until the return from the function in which they are declared.

Labels in C are visible to the whole englobing function, so it makes not much difference if this is a computed goto. You could always replace a computed goto with a more or less involved switch statement.

One notable exception from this rule on local variables are variable length arrays, VLA. Since they do necessarily change the stack pointer, they have different rules. There lifetime ends as soon as you quit their block of declaration and goto and longjmp are not allowed into scopes after a declaration of a variably modified type.

Upvotes: 2

James
James

Reputation: 1371

In the the function prologue the current position of the stack is saved in a callee saved register even with -fomit-frame-pointer.

In the below example the sp+4 is stored in r7 and then in the epilogue (LBB0_3) is restored (r7+4 -> r4; r4 -> sp). Because of this you can jump anywhere within the function, grow the stack at any point in the function and not screw up the stack. If you jump out of the function (via jump *addr) you will skip this epilogue and royally screw up the stack.

Short example which also uses alloca which dynamically allocates memory on the stack:

clang -arch armv7 -fomit-frame-pointer -c -S -O0 -o - stack.c

#include <alloca.h>

int foo(int sz, int jmp) {
    char *buf = alloca(sz);
    int rval = 0;

    if( jmp ) {
        rval = 1;
        goto done;
    }

    volatile int s = 2;

    rval = s * 5;

done:

    return rval;
}

and disassembly:

_foo:
@ BB#0:
    push    {r4, r7, lr}
    add r7, sp, #4
    sub sp, #20
    movs    r2, #0
    movt    r2, #0
    str r0, [r7, #-8]
    str r1, [r7, #-12]
    ldr r0, [r7, #-8]
    adds    r0, #3
    bic r0, r0, #3
    mov r1, sp
    subs    r0, r1, r0
    mov sp, r0
    str r0, [r7, #-16]
    str r2, [r7, #-20]
    ldr r0, [r7, #-12]
    cmp r0, #0
    beq LBB0_2
@ BB#1:
    movs    r0, #1
    movt    r0, #0
    str r0, [r7, #-20]
    b   LBB0_3
LBB0_2:
    movs    r0, #2
    movt    r0, #0
    str r0, [r7, #-24]
    ldr r0, [r7, #-24]
    movs    r1, #5
    movt    r1, #0
    muls    r0, r1, r0
    str r0, [r7, #-20]
LBB0_3:
    ldr r0, [r7, #-20]
    subs    r4, r7, #4
    mov sp, r4
    pop {r4, r7, pc}

Upvotes: 0

Related Questions