Recursion x86程序集如何在递归中保持更新的变量?
伪代码如下:Recursion x86程序集如何在递归中保持更新的变量?,recursion,assembly,x86,stack,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
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
块之后执行此操作,那么我的初始化阶段将把垃圾数据放入这些寄存器中,因为堆栈不会有任何数据(实际上可能只是崩溃,因为在一开始就没有堆栈)
对我来说,这是一种鸡和蛋并存的局面。。。。有什么提示吗?解决方案很简单,但可能很难看到第一次:只需使用堆栈来保存调用方的寄存器
function(param1, param2, param3)
push eax
push ebx
push ecx
; Your function body here
pop ecx
pop ebx
pop eax
ret
当涉及递归时,堆栈是一种自然结构
该函数的一个简单实现将保存/恢复(也称为溢出/填充)所有使用的寄存器,但通常在调用方和被调用方之间有一个约定。此约定要求调用者不希望被调用者更改哪些寄存器(称为非易失性寄存器)——确切的设置是优化和方便的问题,因为它将保存特定寄存器的责任转移到调用者或从调用者转移到调用者 请注意,即使在调用函数实例之前和之后,也始终有一个堆栈。
对于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
中的某个内容执行递归调用,那么您需要像eax
一样保存/还原它如果呼叫约定要求
edx
是非易失性的,我们可以在呼叫之前将eax
移动到edx
中来保存它。这是因为在开场白/尾声中有一个
push/pop edx
——显示呼叫约定如何在呼叫者和被呼叫者之间转移线路 解决方案很简单,但可能很难第一次看到:只需使用堆栈来保存调用方的寄存器
function(param1, param2, param3)
push eax
push ebx
push ecx
; Your function body here
pop ecx
pop ebx
pop eax
ret
当涉及递归时,堆栈是一种自然结构
该函数的一个简单实现将保存/恢复(也称为溢出/填充)所有使用的寄存器,但通常在调用方和被调用方之间有一个约定。此约定要求调用者不希望被调用者更改哪些寄存器(称为非易失性寄存器)——确切的设置是优化和方便的问题,因为它将保存特定寄存器的责任转移到调用者或从调用者转移到调用者 请注意,即使在调用函数实例之前和之后,也始终有一个堆栈。
对于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
中的某个内容执行递归调用,那么您需要像eax
一样保存/还原它如果呼叫约定要求
edx
是非易失性的,我们可以在呼叫之前将eax
移动到edx
中来保存它。这是因为在开场白/尾声中有一个
push/pop edx
——显示呼叫约定如何在呼叫者和被呼叫者之间转移线路 好吧,在读了很多书之后,我发现我有一个基本的误解,它一响我就明白了。因此,对于任何在未来发现这一点的人来说
当调用函数时(在C/C++中),参数被放在堆栈上,因此,与其读取参数,如mov eax,param1
,不如从堆栈中读取它,以mov eax,[esp+8]
开始
这样,当您递归调用函数时,始终可以读取堆栈,包括第一次执行函数
我的误解是,当通过C调用时,我认为参数是以其他方式传递给函数的,堆栈方式仅用于汇编。但不,正如上面玛格丽特所说,一切都遵循惯例
因此,这也让我真正理解了公约的重要性。它妈