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}