10

在 GCC 中,您可以通过获取标签的地址(如)然后跳转到它( )来使用计算的 gotoGCC手册说你可以从函数的任何地方跳转到这个地址,只是从另一个函数跳转到它是未定义的。void *addr = &&labeljump *addr

当你跳转到代码时,它不能假设任何关于寄​​存器的值,所以大概它会从内存中重新加载它们。然而,堆栈指针的值也不一定是定义的,例如,您可以从声明额外变量的嵌套范围跳转。

问题是 GCC 如何设法将堆栈指针的值设置为正确的值(它可能太高或太低)?这与-fomit-frame-pointer(如果有的话)如何相互作用?

最后,关于从哪里跳转到标签的真正限制是什么?例如,您可能可以从中断处理程序中执行此操作。

4

3 回答 3

12

通常,当您有一个带有标签的函数时,gcc 需要确保您可以从函数中的任何间接 goto 跳转到该标签——因此它需要布局堆栈,以便确切的堆栈指针不会t 重要(所有内容都从帧指针索引),或者堆栈指针在所有这些上都是一致的。通常,这意味着它在函数启动时分配固定数量的堆栈空间,之后不再接触堆栈指针。因此,如果您有带有变量的内部范围,则空间将在函数开始时分配并在函数结束时释放,而不是在内部范围内。只有构造函数和析构函数(如果有)需要绑定到内部范围。

跳转到标签的唯一限制是您注意到的那个 - 您只能在包含标签的函数中执行此操作。不是来自任何其他函数或中断处理程序或任何其他东西的任何其他堆栈帧。

编辑

如果您希望能够从一个堆栈帧跳转到另一个堆栈帧,则需要使用 setjmp/longjmp 或类似的东西来展开堆栈。您可以将其与间接 goto 结合起来——例如:

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

这样你就可以longjmp(jmpbuf, label_address);从任何被调用的函数中调用来展开堆栈,然后跳转到标签。只要在setjmp/longjmp中断处理程序中工作,这也将在中断处理程序中工作。也取决于sizeof(int) == sizeof(void *),但情况并非总是如此。

于 2012-09-12T16:39:32.633 回答
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.

于 2012-09-12T19:26:36.777 回答
0

在函数序言中,即使使用 -fomit-frame-pointer,堆栈的当前位置也会保存在被调用者保存的寄存器中。

在下面的示例中,sp+4 存储在 r7 中,然后在结尾 (LBB0_3) 中恢复 (r7+4 -> r4; r4 -> sp)。正因为如此,您可以在函数中的任何位置跳转,在函数中的任何点增加堆栈,而不是搞砸堆栈。如果您跳出函数(通过跳转 *addr),您将跳过此结语并彻底搞砸堆栈。

也使用 alloca 的简短示例,它在堆栈上动态分配内存:

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;
}

和拆卸:

_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}
于 2012-09-12T16:43:38.173 回答