【问题标题】:GCC computed goto and value of stack pointerGCC 计算 goto 和堆栈指针的值
【发布时间】:2012-09-05 17:25:23
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: c gcc stack goto


    【解决方案1】:

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

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

    编辑

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

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

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

    【讨论】:

    • 中断处理程序的有趣之处在于堆栈不会受到伤害,您可以恢复所有寄存器(包括 sp)。在这种情况下,如果 gcc 执行您所描述的操作,那么您实际上可以从中断处理程序跳转到标签。唯一不起作用的情况是 gcc 在计算的 goto 周围放置了特殊的每个函数堆栈处理代码。
    • @jleahy:当然可以(不知道是否可以)。最低要求是函数具有定义的“干净”状态:所有地址被获取的标签都以这个干净状态开始,并且所有计算的跳转在它们跳转之前建立干净状态。事实上,它甚至可能比这更小:“标签地址”可能是从干净状态到标签预期状态的某些代码的地址,然后是goto“真实”标签。它会起作用,但它可能与 gcc 关于标签地址的其他内容相矛盾。
    • ...只要某些寄存器的值是函数“干净状态”的一部分,那么代码可以假设寄存器的值,如果这对 GCC 有利的话。例如,ARM 上的r14 将是一个强有力的候选者。我意识到所有这些都是关于如何实现这个功能的完整猜测,而不是 GCC 实际是如何实现的。
    • @SteveJessop 推测可能是所有可用的,但你所说的对我来说听起来非常明智。在某些时候我可能不得不深入研究 gcc 源代码。
    • 我根本没有关注中断处理程序的讨论......如果从中断处理程序进行跳转,究竟什么会“恢复所有寄存器”?
    【解决方案2】:

    在函数序言中,即使使用 -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}
    

    【讨论】:

    • 这对于普通的 goto 是正确的,因为编译器在编译时就知道目的地,但我不确定它是否适用于计算的 goto。
    • 仍然如此。 SP 保存在一个寄存器中,该寄存器不会在函数的其他地方进行操作,并由任何调用的函数保存/恢复。因此,只要您通过函数尾声,通过函数的代码流(无论是直通还是动态跳转)都无关紧要。
    • r7 是您的帧指针。使用 alloca 会禁用函数的 -fomit-frame-pointer 作为实现 alloca 所需的帧指针。
    【解决方案3】:

    我不认为计算 goto 的事实会增加它对局部变量的影响。局部变量的生命周期从在其声明处或之后进入其声明开始,并在无法以任何方式达到变量的范围时结束。这包括所有不同类型的控制流,特别是gotolongjmp。所以所有这些变量总是安全的,直到从声明它们的函数返回。

    C 中的标签对整个 englobing 函数都是可见的,因此如果这是一个计算出的 goto 并没有太大区别。您总是可以用或多或少涉及的 switch 语句替换计算出的 goto

    这个关于局部变量的规则的一个值得注意的例外是可变长度数组,VLA。由于它们确实必须更改堆栈指针,因此它们具有不同的规则。一旦您退出他们的声明块,生命周期就会结束,并且在声明可变修改类型后,gotolongjmp 不允许进入作用域。

    【讨论】:

      猜你喜欢
      • 2015-08-05
      • 2021-09-17
      • 2012-01-18
      • 2013-12-02
      • 2015-11-16
      • 2013-01-20
      • 2015-04-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多