解决方案很简单,但第一次可能很难看到:只需使用堆栈来保存调用方的寄存器。
function(param1, param2, param3)
push eax
push ebx
push ecx
; Your function body here
pop ecx
pop ebx
pop eax
ret
就递归而言,堆栈是一种自然结构。
该函数的一个简单实现将保存/恢复(也称为溢出/填充)所有使用的寄存器,但通常调用者和被调用者之间有一个名为 the calling convention 的合约。
该约定要求调用者期望哪些寄存器(称为非易失性)不会被被调用者更改 - 确切的设置是优化和方便的问题,因为它将保存特定寄存器的责任转移到调用者或从调用者转移。
请注意,您始终有一个堆栈,即使在调用函数实例之前和之后也是如此。
对于 32 位程序,标准的序言和尾声是
push ebp ;Save caller's frame pointer
mov ebp, esp ;Make OUR frame pointer
sub esp, ... ;Allocate space for local vars
;Save non-volatile registers
push ...
push ...
push ...
;Function body
;
;[ebp+0ch] = Parameter 2 (or N - 1)
;[ebp+08h] = Parameter 1 (or N)
;[ebp+04h] = Return address
;[ebp] = Caller frame pointer
;[ebp-04h] = First local var
;[ebp-08h] = Second local var
;...
;Restore block
pop ...
pop ...
pop ...
;Restore ESP (Free local vars)
mov esp, ebp
pop ebp ;Restore caller's frame pointer
ret ;If a callee cleanup calling convention put the number of bytes here
此代码将参数和局部变量保持在相对于帧指针(由ebp 指向)的固定地址,与局部变量的大小无关。
如果您的函数足够简单,或者如果您对数学有信心,则可以省略创建帧指针。
在这种情况下,访问参数或局部变量是在函数体的不同部分使用不同的偏移量完成的 - 取决于此时堆栈的堆栈。
示例
假设调用约定要求ebx 和ecx 是非易失性的,eax 是保存返回值的寄存器,参数从右到左推入,并且被调用者清理堆栈.
如果我们省略帧指针,函数可以写成
function(param1, param2, param3)
;EBX and ECX are non-volatile, so we save them on the stack since we
;now we are going to use them
push ebx
push ecx
;Move the arguments into the registers
;You need to adjust the offset to reach to the parameters
mov eax, param1 ;eg: mov eax, [esp + 0ch]
mov ebx, param2 ;eg: mov ebx, [esp + 10h]
mov ecx, param3 ;eg: mov ecx, [esp + 14h]
;The logic of the function
dec ebx
inc ecx
;---Recursive call---
;The function won't save eax, so we save it instead
push eax
;Do the call
push ecx
push ebx
push eax
call function
;Here eax is the return value, but ebx and ecx are the same as before the call
;Save the return value in EDI
mov edi, eax
;Restore eax (Now every register is as before but for EDI)
pop eax
;Other logic ...
;Epilogue
;Restore non volatile registers
pop ecx
pop ebx
ret 0ch
注意edi 是易变的,因此我们不需要为调用者保存它,如果你碰巧用edi 中的某些东西进行递归调用,那么你需要像@987654331 一样保存/恢复它@.
如果调用约定要求edx 是非易失性的,我们可以通过在调用之前将eax 移动到edx 来保存它。
这是在序言/尾声中使用push / pop edx 的代价 - 显示调用约定如何改变调用者和被调用者责任之间的界限。