GCC вычисляет goto и значение указателя стека

В GCC вы можете использовать вычисленный goto, беря адрес метки (как в void *addr = &&label), а затем прыгаете на нее (jump *addr). руководство GCC говорит, что вы можете перейти на этот адрес от любого & shy, где в функции это только то, что перескакивает к нему из другой функции, это undefined.

Когда вы переходите к коду, он ничего не может предположить о значениях регистров, поэтому, по-видимому, он перезагружает их из памяти. Однако значение указателя стека также необязательно определяется, например, вы можете перескакивать из вложенной области, которая объявляет дополнительные переменные.

Вопрос в том, как GCC может установить значение указателя стека на правильное значение (оно может быть слишком высоким или слишком низким)? И как это взаимодействует с -fomit-frame-pointer (если это так)?

Наконец, для дополнительных очков, каковы реальные ограничения на то, где вы можете перейти на ярлык? Для ex & shy: am & shy, вы могли бы сделать это с помощью обработчика прерываний.

Ответ 1

В общем случае, когда у вас есть функция с ярлыками, чей адрес взят, gcc должен убедиться, что вы можете перейти к этой метке из любого косвенного goto в функции - так что ему нужно разложить стек так, чтобы точный стек указатель не имеет значения (все индексируется с указателем фрейма) или что указатель стека согласован во всех из них. Как правило, это означает, что он выделяет фиксированное количество пространства стека при запуске функции и никогда не прикасается к указателю стека. Поэтому, если у вас есть внутренние области с переменными, пространство будет выделено при запуске функции и освобождено в конце функции, а не во внутренней области. Только конструктор и деструктор (если есть) должны быть привязаны к внутренней области.

Единственное ограничение при переходе на метки - это тот, который вы отметили - вы можете делать это только из функции, содержащей метки. Не из любого другого кадра стека любой другой функции или обработчика прерываний или чего-то еще.

изменить

Если вы хотите иметь возможность переходить из одного стекового кадра в другой, вам нужно использовать setjmp/longjmp или что-то похожее на разматывание стека. Вы можете комбинировать это с косвенным goto - что-то вроде:

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

таким образом вы можете вызвать longjmp(jmpbuf, label_address); из любой вызываемой функции, чтобы развернуть стек, а затем перейти к метке. Пока setjmp/longjmp работает с обработчиком прерываний, это также будет работать с обработчиком прерываний. Также зависит от sizeof(int) == sizeof(void *), что не всегда так.

Ответ 2

Я не думаю, что тот факт, что goto вычисляется, добавляет, что он имеет локальные переменные. Время жизни локальной переменной начинается с ввода своего объявления в объявлении или за его пределами и заканчивается, когда область действия переменной не может быть достигнута каким-либо образом. Это включает в себя все различные виды потока управления, в частности goto и longjmp. Таким образом, все такие переменные всегда безопасны, пока не будут возвращены функции, в которых они объявлены.

Ярлыки на C видны для всей функции englobing, поэтому это не имеет большого значения, если это вычисленный goto. Вы всегда можете заменить вычисленный goto более или менее вовлеченным оператором switch.

Одним из примечательных исключений из этого правила для локальных переменных являются массивы переменной длины, VLA. Поскольку они обязательно меняют указатель стека, у них разные правила. Там срок жизни заканчивается, как только вы выходите из своего блока декларации, а goto и longjmp не допускаются в области видимости после объявления измененного типа.

Ответ 3

В прологе функции текущая позиция стека сохраняется в сохраненном регистре вызываемого абонента даже с -fomit-frame-pointer.

В приведенном ниже примере sp + 4 сохраняется в r7, а затем в эпилоге (LBB0_3) восстанавливается (r7 + 4 → r4; r4 → sp). Из-за этого вы можете прыгать в любом месте функции, расти стек в любой момент в функции, а не прикручивать стек. Если вы выпрыгнете из функции (с помощью jump * 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}