【问题标题】:What is stack frame in assembly?什么是组装中的堆栈框架?
【发布时间】:2011-04-11 14:05:51
【问题描述】:

堆栈帧的结构是什么?在汇编中调用函数时如何使用它?

【问题讨论】:

标签: assembly x86


【解决方案1】:

每个例程都使用堆栈的一部分,我们称之为堆栈帧。尽管汇编程序员不必遵循以下风格,但强烈建议将其作为良好做法。

每个例程的堆栈帧分为三部分:函数参数、指向前一个堆栈帧的反向指针和局部变量。

第 1 部分:函数参数

例程堆栈帧的这一部分是由调用者设置的。使用“push”指令,调用者将参数压入堆栈。不同的语言可能会以不同的顺序推送参数。 C,如果我没记错的话,把它们从右到左推。也就是说,如果你打电话...

foo (a, b, c);

调用者会将其转换为 ...

push c
push b
push a
call foo

随着每个项目被压入堆栈,堆栈会向下增长。也就是说,堆栈指针寄存器递减四 (4) 个字节(在 32 位模式下),并将该项复制到堆栈指针寄存器指向的内存位置。请注意,“调用”指令将隐式地将返回地址压入堆栈。参数的清理将在第 5 部分中讨论。

第 2 部分:堆栈帧返回指针

此时,“调用”指令已发出,我们现在处于被调用例程的开头。如果我们想访问我们的参数,我们可以像...一样访问它们

[esp + 0]   - return address
[esp + 4]   - parameter 'a'
[esp + 8]   - parameter 'b'
[esp + 12]  - parameter 'c'

但是,在我们为局部变量和其他东西腾出空间后,这可能会变得很笨拙。因此,除了堆栈指针寄存器之外,我们还使用堆栈基指针寄存器。但是,我们希望将 stackbase-pointer 寄存器设置为当前帧,而不是前一个函数。因此,我们将旧的保存在堆栈中(这会修改堆栈上参数的偏移量),然后将当前堆栈指针寄存器复制到堆栈基指针寄存器。

push ebp        ; save previous stackbase-pointer register
mov  ebp, esp   ; ebp = esp

有时您可能会看到仅使用“ENTER”指令即可完成此操作。

第 3 部分:为局部变量雕刻空间

局部变量存储在堆栈中。由于堆栈向下增长,我们减去一些字节数(足以存储我们的局部变量):

sub esp, n_bytes ; n_bytes = number of bytes required for local variables

第 4 部分:将所有内容放在一起。 使用 stackbase-pointer 寄存器访问参数...

[ebp + 16]  - parameter 'c'
[ebp + 12]  - parameter 'b'
[ebp + 8]   - parameter 'a'
[ebp + 4]   - return address
[ebp + 0]   - saved stackbase-pointer register

使用堆栈指针寄存器访问局部变量...

[esp + (# - 4)] - top of local variables section
[esp + 0]       - bottom of local variables section

第 5 部分:Stackframe 清理

当我们离开例程时,堆栈帧必须被清理。

mov esp, ebp   ; undo the carving of space for the local variables
pop ebp        ; restore the previous stackbase-pointer register

有时您可能会看到“LEAVE”指令取代了这两条指令。

根据您使用的语言,您可能会看到“RET”指令的两种形式之一。

ret
ret <some #>

选择哪一个将取决于语言的选择(或使用汇编程序编写时您希望遵循的风格)。第一种情况表明调用者负责从堆栈中删除参数(对于 foo(a,b,c) 示例,它将通过 ... add esp, 12 执行此操作),这就是“C”的方式它。第二种情况表明,return 指令在返回时会从堆栈中弹出 # 个字(或 # 个字节,我不记得是哪个),从而从堆栈中删除参数。如果我没记错的话,这是 Pascal 使用的风格。

这很长,但我希望这可以帮助您更好地理解堆栈帧。

【讨论】:

  • +1 - 非常好的答案。你能推荐一些关于这些部分的好书吗?
  • 由于我很少阅读编程书籍,我没有任何建议。上面的信息只是我几十年前在学校记得的一些东西,也是我多年来从实验中学到的东西。
  • @AbidRahmanK:我在 Coursera 推荐这门课程 coursera.org/course/hwswinterface 这是一门非常不错的课程,应该认真对待。
  • @Sparky 你给出的解释非常好,我对此表示赞同,但唯一怀疑的是使用 [ebp+16] 访问参数“C”,我这边的计算表明它应该是 [ebp +20],将旧的 ebp 推入堆栈将强制 esp 减四,然后 ebp=esp 并访问旧的 ebp ,它应该是 [ebp+4] ,如果我在这里错了,请纠正我。跨度>
  • @naxa:堆栈帧是一个在逻辑上分离存储在堆栈上的变量的概念。它非常有用,以至于您不想使用它的情况相对较少。 x86 处理器不强制使用它,但强烈鼓励使用它。例如:在 16 位模式下,SP 和 BP 寄存器是少数接受位移的寄存器之一。在所有模式下,CALL 指令自动将返回地址压入堆栈,而 RET 指令自动将返回地址从堆栈中弹出。我希望这会有所帮助。
【解决方案2】:

x86-32栈帧是通过执行创建的

function_start:
    push ebp
    mov ebp, esp

所以它可以通过 ebp 访问,看起来像

ebp+00 (current_frame) : prev_frame
ebp+04                 : return_address
                         ....
prev_frame             : prev_prev_frame
prev_frame+04          : prev_return_address

通过汇编指令设计将 ebp 用于堆栈帧有一些优点,因此通常使用 ebp 寄存器访问参数和局部变量。

【讨论】:

  • 这对我来说似乎是错误的 - 不是在 ebp+04 找到返回地址(参见最受好评的答案)?
【解决方案3】:

这取决于所使用的操作系统和语言。因为 ASM 中的堆栈没有通用格式,堆栈在 ASM 中所做的唯一事情就是在执行跳转子程序时存储返回地址。当执行从子程序返回时,地址从堆栈中取出并放入程序计数器(下一个 CPU 执行指令的内存位置)

您需要查阅您正在使用的编译器的文档。

【讨论】:

    【解决方案4】:

    编译器(取决于编译器)可以使用 x86 堆栈帧来传递参数(或指向参数的指针)和返回值。见this

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-06-18
      • 1970-01-01
      • 2014-07-13
      • 2012-12-20
      • 2019-04-13
      • 2016-08-06
      • 2012-04-26
      相关资源
      最近更新 更多