【问题标题】:x86 assembly how to keep updated variables in recursion?x86 程序集如何在递归中保持更新的变量?
【发布时间】:2017-10-25 19:14:06
【问题描述】:

伪代码如下:

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 块之后执行此操作,那么我的初始化阶段会将垃圾数据放入这些寄存器,因为堆栈不会有任何数据(实际上它可能会崩溃,因为一开始没有堆栈)......

对我来说这是先有鸡还是先有蛋的情况……有什么建议吗?

【问题讨论】:

    标签: recursion assembly x86 stack


    【解决方案1】:

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

    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 指向)的固定地址,与局部变量的大小无关。

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

    示例

    假设调用约定要求ebxecx 是非易失性的,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 的代价 - 显示调用约定如何改变调用者和被调用者责任之间的界限。

    【讨论】:

    • 嗨,玛格丽特,感谢您非常详细的回复。我了解调用约定和保存寄存器等。我在编写非递归汇编时没有任何问题(当一个函数调用另一个函数并传递参数等时)。但是,当函数调用自身时,我的主要问题是传递更新的参数而不用将初始参数移动到寄存器的代码块覆盖它们。据我了解,传递的那些初始参数保持静态。我可以想到一种方法,做一个 if 语句等效于查看它是否是初始运行?但这似乎很老套?
    • 通过mov param1, eax更新参数然后调用函数是否有效?这样初始化块会获取新值吗?
    • @Duxa,抱歉我的回复晚了。我很高兴看到你已经想通了 :)
    【解决方案2】:

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

    当函数被调用(在 C/C++ 中)时,参数被放入堆栈,所以不要像 mov eax, param1 那样读取参数,你应该做的实际上是从堆栈中读取它以从 mov eax, [esp+8] 开始。

    这样,当您递归调用函数时,您始终可以读取堆栈,包括您第一次执行该函数。

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

    所以这也让我非常欣赏这个约定,并真正理解它的重要性。它让事情变得容易多了!

    非常感谢你们的帮助!

    【讨论】:

    • 请记住,许多调用约定传递(大部分)堆栈上的参数,例如首选寄存器的 x86-64 调用约定。然而,这并不能阻止递归 - 它只是意味着递归函数应该在进行递归调用之前将其参数(在寄存器中传递)存储在堆栈上。代码看起来几乎一样,但它是当前被压入堆栈的参数值,而不是下一次递归调用的参数。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-13
    • 2017-01-16
    相关资源
    最近更新 更多