堆栈帧的结构是什么?在汇编中调用函数时如何使用它?
答案 0 :(得分:130)
每个例程使用堆栈的一部分,我们将其称为堆栈帧。虽然汇编程序员不必遵循以下风格,但强烈建议将其作为良好实践。
每个例程的堆栈帧分为三个部分:函数参数,指向前一个堆栈帧的后向指针和局部变量。
第1部分:功能参数
例程的堆栈帧的这一部分由调用者设置。使用'push'指令,调用者将参数压入堆栈。不同的语言可能会以不同的顺序推动参数。 C,如果我没记错的话,将它们从右向左推。也就是说,如果你打电话......
foo (a, b, c);
来电者会将此转换为...
push c
push b
push a
call foo
当每个项目被推入堆栈时,堆栈会逐渐减少。也就是说,堆栈指针寄存器递减四(4)个字节(在32位模式下),并且该项被复制到堆栈指针寄存器指向的存储器位置。请注意,'call'指令将隐式地推送堆栈上的返回地址。第5部分将讨论参数的清理。
第2部分:Stackframe后退指针
此时,'call'指令已经发出,我们现在处于被调用例程的开始处。如果我们想要访问我们的参数,我们可以像...一样访问它们。
[esp + 0] - return address
[esp + 4] - parameter 'a'
[esp + 8] - parameter 'b'
[esp + 12] - parameter 'c'
然而,在我们为局部变量和东西划分空间之后,这可能会变得笨拙。因此,除堆栈指针寄存器外,我们还使用stackbase-pointer寄存器。但是,我们希望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)示例,它将通过...添加esp,12),这就是'C'的做法它。第二种情况表明返回指令会在返回时从堆栈弹出#words(或#字节,我记不清哪些),从而从堆栈中删除参数。如果我没记错的话,这就是Pascal使用的风格。
很长,但我希望这有助于您更好地理解堆栈框架。
答案 1 :(得分:13)
通过执行
创建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寄存器访问参数和本地。
答案 2 :(得分:2)
这取决于所使用的操作系统和语言。因为ASM中没有堆栈的通用格式,堆栈在ASM中唯一要做的就是在执行跳转子例程时存储返回地址。当执行从子程序返回时,地址从堆栈中被拾取并放入程序计数器(存储器位置,其中下一个CPU执行指令将来自其中)
您需要查阅您正在使用的编译器的文档。
答案 3 :(得分:0)
编译器(取决于编译器)可以使用x86堆栈帧来传递参数(或指向参数的指针)并返回值。见this