Проблемы реализации вызова / возврата функции стековой виртуальной машины

Сегодня я решил создать небольшую виртуальную машину на основе стека в C++11 - все шло довольно хорошо, пока я не начал вызывать функции и возвращаться из функций.

Я пытался следовать правилам вызова, аналогичным сборке x86, но я действительно запутался.

У меня проблемы с обработкой базовых смещений указателей и возвращаемых значений.

Кажется, очень трудно отслеживать регистры, используемые для возвращаемых значений и аргументов (для вызовов функций) в стеке.

Я создал простой язык, похожий на ассемблер, и компилятор. Вот прокомментированный пример (что моя виртуальная машина компилируется и выполняется). Я пытался объяснить, что происходит, и поделиться своими мыслями в комментариях.

//!ssvasm

$require_registers(3);

// C++ style preprocessor define directives to refer to registers
$define(R0, 0);
$define(R1, 1);
$define(R2, 2); 

// Load the 2.f float constant value into register R0
loadFloatCVToR(R0, 2.f);

// I want to pass 2.f as an argument to my next function call:
// I have to push it on top of the stack (as with x86 assembly)
pushRVToS(R0);

// I call the FN_QUAD function here: calling a function pushes both
// the current `stack base offset` and the `return instruction index`
// on the stack
callPI(FN_QUAD); 

// And get rid of the now-useless argument that still lies on top of the stack
// by dumping it into the unused R2 register 
popSVToR(R2);

halt(); // Halt virtual machine execution



$label(FN_DUP); // Function FN_DUP - returns its argument, duplicated

// I need the arg, but since it's beneath `old offset` and `return instruction`
// it has to copied into a register - I choose R0 - ...

// To avoid losing other data in R0, I "save" it by pushing it on the stack
// (Is this the correct way of saving a register's contents?)
pushRVToS(R0);

// To put the arg in R0, I need to copy the value under the top two stack values
// (Read as: "move stack value offset by 2 from base to R0")
// (Is this how I should deal with arguments? Or is there a better way?)
moveSBOVToR(R0, 2);

// Function logic: I duplicate the value by pushing it twice and adding
pushRVToS(R0); pushRVToS(R0); addFloat2SVs();

// The result is on top of the stack - I store it in R1, to get it from the caller
// (Is this how I should deal with return values? Or is there a better way?)
popSVToR(R1);

popSVToR(R0); // Restore R0 with its old value (it's now at the top of the stack)

// Return to the caller: this pops twice - it uses `old stack base offset` and
// unconditionally jumps to `return instruction index`
returnPI();



$label(FN_QUAD); // Function FN_QUAD

pushRVToS(R0);
moveSBOVToR(R0, 2);

// Call duplicate twice (using the first call's return value as the second
// call's argument)
pushRVToS(R0); callPI(FN_DUP); popSVToR(R2);
pushRVToS(R1); callPI(FN_DUP); popSVToR(R2);

popSVToR(R0);
returnPI();

Я никогда не программировал на ассемблере раньше, поэтому я не уверен, что используемые мной методы верны (или эффективны).

Правильный ли способ обработки аргументов / возвращаемых значений / регистров?

Должен ли вызывающий функции выдвинуть аргументы, затем вызвать, а затем выдвинуть аргументы? Кажется, что использовать регистр было бы проще, но я читал, что x86 использует стек для передачи аргументов. Я уверен, что метод, который я здесь использую, неверен.

Должен ли я подтолкнуть оба old stack offset а также return instruction index на вызов функции? Или я должен хранить old stack offset в реестре? (Или вообще не хранить?)

3 ответа

Решение

То, о чем вы говорите, называется соглашением о вызовах. Другими словами, определение того, кто и как создает стек, как вызывающий или вызываемый, и как должен выглядеть стек.

У них есть много способов сделать это, и никто не может быть лучше, чем другие, вы просто должны быть добросовестными.

Поскольку было бы слишком долго описывать различные соглашения о вызовах, вы должны просто проверить статью в Википедии, которая действительно завершена.

Но по-прежнему быстро соглашение о вызовах x86 C указывает, что вызывающая сторона должна сохранить свои регистры и построить стек и позволить вызываемому пользователю освободиться от использования регистров, чтобы возвратить значение или просто сделать что-то.

Для конкретных вопросов в конце вашего поста лучше всего иметь тот же стек, что и у C, хранить в последних EIP и EBP и оставлять регистры свободными для использования. Пространство стеков не так ограничено, как количество имеющихся у вас регистров.

Я решил эту проблему в своей машине стека, над которой я работал, следующим образом:

Инструкция вызова void (без параметров) делает что-то вроде этого:

Существует _stack[] (основной стек) и _cstack[] (стек вызовов, содержащий информацию о вызовах, такую ​​как размер возврата).

При вызове функции, (VCALL (void вызов функции) встречается следующее:

        u64& _next = _peeknext; //refer to next bytecode (which will be function address)
        AssertAbort((_next > -1) && (_next < _PROGRAM_SIZE), "Can't call function. Invalid address");
        cstack_push(ip + 2); //address to return to (current address +2, to account for function parameters next to function call)
        cstack_push(fp); //curr frame pointer
        cstack_push(_STACK_SIZE); //curr stack size
        cstack_push(0); //size of return value(would be 4 if int, 8 for long etc),in this case void
        ip = (_next)-1; //address to jump to (-1 to counter iteration incrementation of program counter(ip))

Затем, когда RET (возврат) инструкция встречается, сделано следующее:

        AssertAbort(cstackhas(3), "Can't return. No address to return to.");
        u64 return_size = cstack_pop(); // pop size of return value form call stack
        _STACK_SIZE = cstack_pop(); //set the stack size to what it was before the function call, not accounting for the return value size
        fp = cstack_pop(); //reset the frame pointer to the current value to where it was before the function call
        ip = cstack_pop() - 1; //set program counter to addres storedon call stack from last function call

        _cstack.resize(_STACK_SIZE + return_size); //leave the top of the stack intact (size of return value in bytes), but disregard the rest.

Это, вероятно, сейчас бесполезно для вас, так как этот вопрос довольно старый, но вы можете задать любые вопросы, если хотите:)

Лучшее решение зависит от машины.

Если push и pop в стеке работают так же быстро, как и при использовании регистров (в стеке микросхемы или в стеке запеченного чипа L1), и в то же время вы очень ограничены в количестве регистров, имеет смысл использовать стек.

Если у вас много регистров, вы можете использовать некоторые из них для хранения счетчиков (указателей) или переменных.

В общем, чтобы модули взаимодействовали друг с другом или переводили (или компилировали) другие языки в вашу сборку, вы должны указать двоичный интерфейс приложения.

Вы должны сравнить различные ABI для разных аппаратных средств (или виртуальных машин), чтобы найти методы, подходящие для вашей машины. Как только вы определите свой ABI, программы должны соответствовать бинарной совместимости.

Другие вопросы по тегам