【发布时间】:2011-04-11 14:05:51
【问题描述】:
堆栈帧的结构是什么?在汇编中调用函数时如何使用它?
【问题讨论】:
堆栈帧的结构是什么?在汇编中调用函数时如何使用它?
【问题讨论】:
每个例程都使用堆栈的一部分,我们称之为堆栈帧。尽管汇编程序员不必遵循以下风格,但强烈建议将其作为良好做法。
每个例程的堆栈帧分为三部分:函数参数、指向前一个堆栈帧的反向指针和局部变量。
第 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 使用的风格。
这很长,但我希望这可以帮助您更好地理解堆栈帧。
【讨论】:
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 寄存器访问参数和局部变量。
【讨论】:
这取决于所使用的操作系统和语言。因为 ASM 中的堆栈没有通用格式,堆栈在 ASM 中所做的唯一事情就是在执行跳转子程序时存储返回地址。当执行从子程序返回时,地址从堆栈中取出并放入程序计数器(下一个 CPU 执行指令的内存位置)
您需要查阅您正在使用的编译器的文档。
【讨论】:
编译器(取决于编译器)可以使用 x86 堆栈帧来传递参数(或指向参数的指针)和返回值。见this
【讨论】: