assembly 如果esp指向栈顶,ebp指向哪里?

pu3pd22g  于 2023-02-19  发布在  其他
关注(0)|答案(4)|浏览(189)

我在理解espebp寄存器的使用方法时遇到了一些困难。
我们为什么这样做:

pushl %ebp 
movl %esp, %ebp

在每个函数的开始?ebp在第一次被按下时持有什么?

v09wglhw

v09wglhw1#

在每个函数的开始ebp指向调用函数想要的地方,它与当前函数无关,直到当前函数的代码选择使用它. ebp只是一个堆栈帧指针,以防你选择使用堆栈帧.概念是你可以使用ebp来拥有一个非在您可以继续使用esp在堆栈上添加或移除项时,移动对函数堆栈的引用。如果你不使用堆栈指针,而是继续使用esp作为对堆栈的引用,那么在你的函数过程中,堆栈上的一个特定项的位置会相对于esp而变化。如果你在开始使用堆栈之前设置了ebp(而不是保存ebp),那么你就有了一个固定的相对地址,指向你的函数所关心的堆栈上的参数,比如传递的参数,局部变量等等。
您可以完全自由地使用eax或edx或任何其他寄存器作为函数中的堆栈帧指针,ebp是作为通用寄存器供您用于堆栈帧,因为x86历来具有堆栈依赖性(回邮地址,而旧的调用约定是基于堆栈的)。其他具有更多寄存器的指令集可能只是选择一个寄存器作为编译器实现的函数指针/堆栈帧指针。如果你有选择权,选择使用堆栈帧。它会消耗掉你本来可以用来做其他事情的寄存器,消耗掉更多的代码和执行时间。就像使用其他通用寄存器一样,ebp是非易失性的,按照今天使用的调用约定,你需要保留它,然后返回你找到它时的样子,所以它所指向的是特定于函数的,当你输入函数时,它所指向的是特定于调用函数的。
一个特定的编译器实现可以选择堆栈帧,也可以选择如何使用ebp。如果它在启用时总是以相同的方式使用,那么使用该工具链,您可能有一个调试器或其他工具可以利用它。例如,如果函数中的第一件事是将ebp推到堆栈上,那么在任何与ebp相关的函数中,调用函数的返回地址是固定的(除非有一些尾部优化,否则可能是调用者的调用者(调用者的调用者(调用者的调用者)))。您正在为这个特性消耗寄存器、堆栈空间和代码空间,但是,就像为调试而编译一样,您可以在开发期间使用堆栈帧来编译以使用这些特性。
为什么要从push开始呢?因为这是使用帧指针和定义一个一致位置的好方法。把它压入栈作为你做的第一件事1)保存ebp,这样你就不会使调用函数崩溃(s)2)定义一致的参考点EBP以下的地址是返回地址和在函数持续时间内固定偏移量处的调用参数。对于这样的方案,局部变量位于ebp之上的固定地址,编译器和人类一样,完全有能力不需要这样做,我的第一个参数可能在代码中的某个点位于esp-20,然后我可以在堆栈上再推8个字节,现在相同的参数位于esp-28,就这样编码吧。
但是为了调试的目的,调试所产生的代码,并且有时例如在固定偏移量处找到返回地址。烧掉另一个寄存器,是IMO懒但是,绝对可以帮助调试和提高编译器输出的质量,更快的找到编译器输出中的bug,并帮助尝试阅读代码的人更快、更轻松地理解代码。如果堆栈帧指针使用得当,则所有参数和局部变量在堆栈帧指针设置和清除点之间的函数持续时间内,都位于堆栈帧指针的固定偏移量处。将指针压入以保存它,将帧指针设置为堆栈指针,带或不带偏移量。到弹出帧指针前返回。

mi7gmzs6

mi7gmzs62#

我们为什么这样做:
这是有历史原因的。在16位代码中...

  • ... x86 CPU不允许所有寄存器用于内存寻址。
  • ......地址与"段"相关(例如16*ss16*ds)。

由于sp不能直接用于访问内存(例如10(%sp)-这在16位代码中是不可能的),您必须首先将sp复制到另一个寄存器,然后访问内存(例如将sp复制到bp,然后执行10(%bp))。
当然,也可以使用bxsidi来代替bp
然而,第二个问题是分段:使用这些寄存器中的一个将访问ds寄存器指定的段。要访问堆栈上的内存,我们必须执行ss:10(%bx)而不是10(%bx)。使用bp隐式访问包含堆栈的段(与显式指定段相比,这更快,指令短一个字节)。
在32位(或64位)代码中,所有这些都不再是必要的。我只是用现代C编译器编译了一个函数。结果是:

movl    12(%esp), %eax
imull   8(%esp), %eax
addl    4(%esp), %eax
ret

如您所见,ebp寄存器未使用。
但是,ebp仍然在现代代码中使用的原因有两个:

  • 创建函数更容易。您知道,即使函数包含pushpop指令,第一个参数也始终位于8(%ebp)。使用esp时,第一个参数的位置会随着每次pushpop操作而改变。
  • alloca函数的使用:这个函数将修改esp寄存器,修改的方式可能是编译器无法预料的!所以你需要一个原始esp寄存器的副本。

使用alloca的示例:

push %ebp
mov %esp, %ebp
call GetMemorySize  # This will set %eax

# ---- Start of alloca() ----
# The alloca "function" will reserve N bytes on the
# stack while the value N is calculated during
# the run-time of the program (here: by the function
# GetMemorySize)
or $3, %al
inc %eax

# This has the same effect as multiple "push"
# instructions. However, we don't know how many
# "push" instructions!
sub %eax, %esp

mov %esp, %eax
# From this moment on, we would not be able to "restore"
# the original stack any more if we didn't have a copy
# of the stack pointer!
# ---- End of alloca() ----

push %eax
mov 8(%ebp), %eax
push %eax
call ProcessSomeData
mov %ebp, %esp
pop %ebp

# Of course we need to restore the original value
# of %esp before we can do a "ret".
ret
i2loujxw

i2loujxw3#

在函数执行期间,可以将各种对象压入堆栈。压入递减%esp(如果使用的是64位硬件,则为%rsp)以指向堆栈上的下一个可用内存,而%ebp(或%rbp)维护指向函数堆栈帧起始的不变指针,因此,相对于%ebp,该函数能够找到其已经存储在堆栈上的各种对象。
早期的8位CPU(如20世纪70年代和80年代的老6502)没有%ebp。缺少%epb,请考虑以下C代码:

int a = 10;
++a;
{
    int b = 20;
    --b;
    a += b;
}

a存储在0(%esp)中,不同的是,当b被压入堆栈时,实际上没有移动的a现在位于4(%esp)中。
使用%ebp时,a始终位于-4(%ebp),并且b在范围内时位于-8(%ebp)

nzk0hqpo

nzk0hqpo4#

Ebp指向旧函数的Ebp。示例:如果从main函数调用某个函数,然后代码执行将开始执行此函数,则该函数的Ebp寄存器将指向main的Ebp寄存器

相关问题