今天我决定在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
存储在注册表中? (或者完全避免存放它?)
答案 0 :(得分:1)
我在我一直在努力的堆栈机器中解决了这个问题,方法如下:
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.
这对你来说可能毫无用处,因为这个问题已经很老了,但如果你愿意,你可以提出任何问题:)
答案 1 :(得分:0)
你所说的是调用召唤约定。换句话说,定义谁构建堆栈以及如何,调用者或被调用者,以及堆栈应该如何。
他们有很多方法可以做到这一点,没有人比另一方好,你只需要保持一致。
由于长时间描述不同的呼叫对话,您应该查看真正完整的维基百科文章。
但是很快,x86 C调用约定指定调用者必须保存其寄存器并构建堆栈,让被调用者无需使用寄存器,返回值或只是为了做事。
对于帖子末尾的具体问题,最好的方法是使用与C相同的堆栈,存储在最后一个EIP和EBP中,并使寄存器可以自由使用。堆栈空间不限于您拥有的寄存器数量。
答案 2 :(得分:0)
最佳解决方案取决于机器。
如果堆栈中的push和pop与使用寄存器(芯片堆栈或芯片L1烘焙堆栈)一样快,同时您对寄存器数量的限制非常有限,使用堆栈是有意义的。
如果你有足够的寄存器,你可以使用它们中的一些来存储计数器(指针)或变量。
通常,为了使模块相互通信或将其他语言翻译(或编译)到程序集中,您应该指定一个应用程序二进制接口。
您应该比较不同硬件(或虚拟机)的不同ABI,以找到适合您机器的技术。定义ABI后,程序应符合二进制兼容性。