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

Dux*_*uxa 1 recursion x86 assembly 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
Run Code Online (Sandbox Code Playgroud)

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

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

Mar*_*oom 5

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

function(param1, param2, param3)

push eax
push ebx
push ecx

; Your function body here

pop ecx
pop ebx
pop eax

ret
Run Code Online (Sandbox Code Playgroud)

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

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

请注意,即使在调用函数实例之前和之后,也总是有堆栈.
对于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
Run Code Online (Sandbox Code Playgroud)

此代码将参数和局部变量保持在相对于帧指针(由其指向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
Run Code Online (Sandbox Code Playgroud)

注意如何edi是volatile,因此我们不需要为调用者保存它,如果你碰巧用某些东西进行递归调用,edi那么你需要保存/恢复它eax.
如果强制执行的约定edx是非易失性的,我们可以eax通过edx在调用之前将其移动来保存.
这是push / pop edx在序言/结语中有一个代价- 显示调用约定如何改变调用者和被调用者责任之间的界限.