10

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.

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
jleahy
  • 16,149
  • 6
  • 47
  • 66

3 Answers3

12

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.

Chris Dodd
  • 119,907
  • 13
  • 134
  • 226
  • The interesting thing about interrupt handlers is that the stack will be unharmed and you can restore all registers (including the sp). In this case provided that gcc does what you've described then you would actually be able to jump to a label from an interrupt handler. The only case it wouldn't work is if gcc puts special per-function stack handling code around the computed goto. – jleahy Sep 12 '12 at 16:43
  • @jleahy: which it certainly could (don't know if it does). The minimal requirement is that the function has a defined "clean" state: all labels whose address is taken start at this clean state, and all computed jumps establish the clean state before they jump. In fact it might even be more minimal than that: the "address of a label" could be the address of some code that goes from the clean state to the state expected at the label, then `goto` the "real" label. It would work, but it might contradict something else that gcc says about the addresses of labels. – Steve Jessop Sep 12 '12 at 17:28
  • ... and as long as the value of some registers is part of the "clean state" for the function, then the code *could* assume something about the value of registers, if that was beneficial to GCC. `r14` on ARM would be a strong candidate, for example. I realise all of this is complete speculation about how this feature could be implemented in general, not how GCC actually does it. – Steve Jessop Sep 12 '12 at 17:32
  • @SteveJessop Speculation may be all that's available, but what you've said sounds perfectly sensible to me. I may have to dive into the gcc source code at some point. – jleahy Sep 12 '12 at 18:00
  • I'm not following the interrupt handler discussion at all... what exactly would "restore all registers" if a jump were made from an interrupt handler? – Michael Burr Sep 13 '12 at 05:45
  • @jleahy: An interrupt handler generally has its own stack frame pushed on top of the current function's stack frame. You can think of an interrupt sort of like an involuntary function call the got stuck into the code stream -- when the handler returns, the return-from-interrupt process will pop that stack frame, much the same way a normal return pops a normal function's stack frame. So trying to do an indirect goto from an interrupt handler will work exactly as well as doing an indirect goto from a called function -- not at all. – Chris Dodd Sep 13 '12 at 15:52
  • @ChrisDodd You make a good point, which is the part I was missing. I was imagining an interrupt handler written in assembler, with no stack frame, but of course if that's the case then you can't do a goto. As soon as you're in C then you get the stack frame and esp has moved. Thanks for setting me straight. – jleahy Sep 14 '12 at 08:17
2

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.

Jens Gustedt
  • 76,821
  • 6
  • 102
  • 177
0

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}
James
  • 1,341
  • 7
  • 13
  • This is true for a normal goto, because the compiler knows the destination at compile time, but I'm not sure if it's true for a computed goto. – jleahy Sep 12 '12 at 16:46
  • It is still true. The SP is saved in a register that is not manipulated elsewhere in the function and is save/restored by by any functions that are called. Therefore the code flow through the function (whether straight thru or dynamically jumping around) does not matter as long as you go through the function epilogue. – James Sep 12 '12 at 17:05
  • 2
    `r7` is your frame pointer here. Using alloca disables -fomit-frame-pointer for the function as a frame pointer is required in order to implement alloca. – Chris Dodd Sep 13 '12 at 16:04