x86汇编如何在递归中保持更新的变量?

时间:2017-05-25 08:01:53

标签: recursion assembly x86 stack

伪代码如下:

function(param1, param2, param3)

mov eax, param1
mov ebx, param2
mov ecx, param3

//a bunch of stuff is done here
//I need to call recursevely so I can do

dec ebx //ebx--
inc ecx //ecx++

push ecx
push ebx
push eax

call function //Needs to call itself with decremented/incremented values

mov edi, eax

pop eax
pop ebx
pop ecx

我的问题是,当我以递归方式调用函数时,如果我在mov块之前从堆栈中复制它们,那么在开始时初始化所需的mov指令将覆盖修改后的值。相反,如果我在mov块后执行此操作,那么我的初始化阶段会将垃圾数据放入这些寄存器,因为堆栈不会有任何数据(实际上它可能会崩溃,因为一开始就没有堆栈)....

对我来说,这是一种鸡肉和鸡蛋的情况....任何提示?

2 个答案:

答案 0 :(得分:5)

解决方案很简单,但第一次可能很难看到:只需使用堆栈来保存调用者的寄存器。

imageView

在递归时,堆栈是一种自然结构。

该函数的一个简单实现将保存/恢复(a.k.a.溢出/填充)所有使用的寄存器,但通常在调用者和名为the calling convention的被调用者之间存在契约。
这个约定要求调用者期望被调用者不改变哪些寄存器(称为非易失性) - 确切的集合是优化和便利的问题,因为它将特定寄存器保存到调用者或从调用者保存的责任。

请注意,即使在调用函数实例之前和之后,您总是有一个堆栈 对于32位程序,标准序言和结语是

function(param1, param2, param3)

push eax
push ebx
push ecx

; Your function body here

pop ecx
pop ebx
pop eax

ret

此代码将参数和局部变量保持在相对于帧指针(由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 指向)的固定地址,而与局部变量的大小无关。

如果您的功能足够简单或者您对数学有信心,则可以省略帧指针的创建。
在这种情况下,访问参数或局部变量是在函数体的不同部分使用不同的偏移量完成的 - 这取决于此刻堆栈的堆栈。

实施例

让我们假设调用约定要求ebpebx是非易失性的,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完全一样 如果调用约定要求eax为非易失性,我们可以通过在调用前将其移至edx来保存eax。 这是在序言/结语中有edx的费用 - 显示调用约定如何在调用者和被调用者责任之间转换线。

答案 1 :(得分:0)

好的,经过大量的阅读后,我发现我有一个根本的误解,一旦点击我就明白了。所以对于将来发现这一点的人来说。

当调用函数时(在C / C ++中),参数被放在堆栈上,所以你不应该读取像mov eax, param1这样的参数,而是应该从堆栈中读取它以{{{}开始。 1}}。

这样当你以递归方式调用函数时,你总是可以读取堆栈,包括你第一次执行函数。

我的误解是我认为参数在通过C调用时以其他方式传递给函数,而堆栈方式仅用于汇编。但不,正如上面的玛格丽特所说,这一切都遵循惯例。

所以这也让我非常欣赏这个会议并真正理解它的重要性。这让事情变得更加轻松!

非常感谢你的帮助!