Переместить текущий кадр стека в C
Мне было интересно, есть ли удобный способ скопировать текущий кадр стека, переместить его в другое место, а затем "вернуться" из функции из нового места?
Я играл с setjmp
а также longjmp
при выделении больших массивов в стеке, чтобы отодвинуть указатель стека. Я знаком с соглашениями о вызовах и с тем, где заканчиваются аргументы функций и т. Д., Но я не очень разбираюсь в арифметике указателей.
В общих чертах описать конечную цель; Цель состоит в том, чтобы иметь возможность выделять кадры стека и переходить к другому кадру стека, когда я вызываю функцию (мы можем вызвать эту функциюswitch
). Однако, прежде чем я перейду к новому фрейму стека, я хотел бы получить адрес возврата изswitch
поэтому, когда я (предположительно) longjmp
d в новый фрейм, я смогу вернуться в позицию, которая инициировала переключение контекста.
Я уже получил некоторое вдохновение, как имитировать сопрограммы с помощью longjmp
ан setjmp
из этого поста.
Если это возможно, это будет составной частью моего текущего исследования, в котором я пытаюсь реализовать (очень грубое) доказательство расширения концепции в компиляторе. Буду признателен только за ответы и комментарии, касающиеся вопроса, поставленного в моем первом абзаце.
Обновить
Чтобы попытаться прояснить мои намерения, я написал этот пример на C. Он должен быть скомпилирован с помощью -fno-stack-protector
. Я хочу, чтобы локальные переменныеa
а также b
в main
чтобы не быть рядом друг с другом в стеке (1), а скорее быть отделены друг от друга расстоянием, указанного в буфереcall
. Кроме того, в настоящее время этот код будетreturn
к main
дважды, в то время как я хочу сделать это только один раз (2). Предлагаю вам прочитать процедуры в таком порядке:main
, call
а также change
.
Если бы кто-нибудь мог ответить на любой из двух вопросов, поставленных в предыдущем абзаце, я был бы безмерно благодарен. Он не обязательно должен быть красивым или портативным.
Опять же, я бы предпочел ответы на мои вопросы, а не предложения лучших способов решения проблем.
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
jmp_buf* buf;
long* retaddr;
int change(void) {
// local variable to use when computing offsets
long a[0];
for(int i = 0; i < 5; i++) a[i]; // same as below, not sure why I need to read this
// save this context
if(setjmp(*buf) == 0) {
return 1;
}
// the following code runs when longjmp was called with *buf
// overwrite this contexts return address with the one used by call
a[2] = *retaddr;
// return, hopefully now to main
return 1;
}
static void* retain;
int call() {
buf = (jmp_buf*)malloc(sizeof(jmp_buf));
retaddr = (long*) malloc(sizeof(long));
long a[0];
for(int i = 0; i < 5; i++) a[i]; // not sure why I need to do this. a[2] reads (nil) otherwise
// store return address
*retaddr = a[2];
// allocate local variables to move the stackpointer
char n[1024];
retain = n; // maybe cheat the optimiser?
// get a jmp_buf from another context
change();
// jump there
longjmp(*buf, 1);
}
// It returns to main twice, I am not sure why
int main(void) {
char a;
call(); // this function should move stackpointer (in this case, 1024 bytes)
char b;
printf("address of a: %p\n", &a);
printf("address of b: %p\n", &b);
return 1;
}
2 ответа
Tl ;dr - нет.
(На каждом компиляторе, заслуживающем внимания): компилятор знает адрес локальных переменных по их смещению либо от sp, либо от назначенного сохраненного указателя стека, фрейма или базового указателя. a может иметь адрес (sp+1), а b может иметь адрес (sp+0). Если вам удастся успешно вернуться в главное меню с уменьшенным на 1024 указателем стека; они по-прежнему будут называться (sp+1), (sp+0); хотя технически они сейчас (sp+1-1024), (sp+0-1024), что означает, что они больше не a & b.
Вы могли бы разработать язык, который фиксировал бы локальное распределение так, как вы считаете, и это могло бы иметь некоторую интересную выразительность, но это не C. Я сомневаюсь, что какой-либо существующий компилятор мог бы придумать последовательную обработку этого. Для этого при обнаружении:
char a;
ему нужно было бы создать псевдоним этого адреса в том месте, где он его встретил; сказать:
add %sp, $0, %r1
sub %sp, $1, %sp
и когда он столкнулся
char b;
add %sp, $0, %r2
sub %sp, $1, %sp
и так далее, но если у него заканчиваются свободные регистры, он должен вылить их в стек; и поскольку он считает, что стек изменяется без предупреждения, ему необходимо выделить указатель на эту область сброса и сохранить его в регистре.
Кстати, это недалеко от концепции расширенного стека (golang использует их), но, как правило, детализация находится на границе функции или метода, а не между двумя определениями переменных.
Хотя идея интересная.
Это возможно, это то, что делают многозадачные планировщики, например, во встроенных средах.
Однако он чрезвычайно зависит от среды и должен вникать в специфику процессора, на котором он работает.
В основном возможные шаги:
- Определите регистры, которые содержат необходимую информацию. Выбирайте их по тому, что вам нужно, они, вероятно, отличаются от того, что компилятор использует в стеке для реализации вызовов функций.
- Узнайте, как их содержимое может быть сохранено (скорее всего, конкретные инструкции ассемблера для каждого регистра).
- Используйте их для непрерывного хранения всего содержимого.
- Место для этого, вероятно, уже выделено внутри объекта, описывающего и администрирующего текущую задачу.
- Не используйте обратный адрес. Вместо этого, когда закончите со "вставленной" задачей, выберите среди множества наборов данных задачи, которые описывают потенциальные задачи, к которым нужно вернуться. Это суть планирования. Если адрес возврата известен заранее, то он очень похож на обычный вызов функции. Т.е. идея в том, чтобы потенциально вернуться к другой задаче, нежели последняя оставшаяся. Это также причина, по которой во многих случаях задачам нужен собственный стек.
Кстати, я не думаю, что арифметика с указателями является здесь наиболее актуальным инструментом.
Содержимое регистров, составляющих кадр стека, находится в регистрах, а не в любом месте памяти, на которое может указывать указатель. (По крайней мере, в большинстве современных систем C64 не участвует в этом....).