提问者:小点点

为什么x86架构使用两个堆栈寄存器(esp; ebp)?


我的问题很简单,为什么x86架构使用两个堆栈寄存器(esp; ebp)?

堆栈帧的长度在编译期间已经确定,那么我认为我们可以只使用一个寄存器(例如esp)来访问堆栈,并维护堆栈上的ebp寄存器中的基地址(或其他内存区域,但这会导致更多的性能损失)

有可能吗?


共2个答案

匿名用户

当然,这一切都取决于调用约定,但它通常是这样的。

堆栈指针可以根据您在任何给定时间点需要压入或弹出堆栈的任何内容任意移动。这可以在函数中的任何时间发生,因为您需要在堆栈上临时保存一些数据。

对于任何给定的堆栈深度,基指针通常设置为相同的值,用于访问传递的参数(一侧)和局部变量(另一侧)。它还用于在退出函数时快速移动堆栈指针。

这样做的原因是为了简化代码,这样您就不必根据可能更改的堆栈指针来引用堆栈内容。使用基指针大大简化了代码生成的任务(您不必在任何给定时间知道堆栈指针是什么,只需使用基指针,它在函数期间保持不变)。

否则,想要推送局部变量的两个副本以调用下一个函数的代码将如下所示:

    mov    eax, [esp+16]    ; get var1
    push   eax              ; push it
    mov    eax, [esp+20]    ; get var1 again
    push   eax
    call   _somethingElse

抛开在这种情况下不会重新加载eax的事实不谈,我想说的是,移动堆栈指针中项目的相对位置会不必要地使事情复杂化。

例如,这是一个遵循通用调用约定的汇编函数:

_doSomething:
    push   ebp              ; stack current base pointer
    mov    ebp, esp         ; save previous stack pointer
    sub    esp, 48          ; incl 48 bytes local storage

    ; do whatever you want here, including changing
    ;  esp, as long as it ends where it started.

    mov    esp, ebp         ; restore previous stack pointer
    pop    ebp              ; and previous base pointer

    ret                     ; then return

_callIt:
    mov    eax, 7
    push   eax              ; push parameter for function
    call   _doSomething
    add    esp, 4           ; get rid of pushed value
    :

如果按照代码,可以看到函数体内部的ebp是一个固定的参考点,其中[ebp]ebp的内容)是返回地址,[ebp 4]7的推送值,[ebp-N]_doSomething的本地存储,其中N从148不等。

无论在函数主体内推入或弹出多少项,都是这种情况。

匿名用户

为什么x86架构使用两个堆栈寄存器(esp; ebp)?

根据@gsg对相关问题的回答“是否有任何语言/编译器使用具有非零嵌套级别的x86 ENTER指令?”x86架构在30年前被设计为“Pascal机器”。

有关其中一位芯片设计人员的基本原理,请参阅Stephen P. Morse的8086/8088 Primer,第8章:高级语言编程(Pascal)。

因此,它包含了对嵌套和递归子程序(过程、函数)的硬件支持,就像在Pascal编程语言中一样,这是结构化编程范式的一个重要方面,旨在生成更易于读/写/维护的代码。

特殊CPU指令形式的硬件支持能够使用更少的指令生成代码。更少的指令通常也意味着更快的代码。

是否可以在另一台图灵整机上实现堆栈帧变量访问,而无需使用ebp寄存器?

是的但执行时间不同