堆栈帧的结构是什么?在程序集中调用函数时如何使用堆栈帧?
rn0zuynd1#
每个例程都使用堆栈的一部分,我们称之为堆栈框架。虽然汇编程序员不一定要遵循下面的风格,但强烈推荐这种做法。每个例程的堆栈帧分为三个部分:函数参数、指向上一个堆栈帧的反向指针和局部变量。
第1部分:函数参数
例程堆栈帧的这一部分是由调用者设置的。调用者使用“push”指令将参数推入堆栈。不同的语言可能以不同的顺序推入参数。如果我没记错的话,C是从右到左推入参数的。也就是说,如果你调用...
foo (a, b, c);
调用者将把它转换为...
push c push b push a call foo
当每一个项目被压入堆栈时,堆栈会向下增长。也就是说,堆栈指针寄存器会减少四(4)个字节(在32位模式下),并且项目会被复制到堆栈指针寄存器所指向的内存位置。注意,“call”指令会隐式地将返回地址压入堆栈。参数的清理将在第5部分中讨论。
第2部分:堆栈帧返回指针
此时,“call”指令已经发出,我们现在正处于被调用例程的开始。如果我们想访问参数,我们可以像这样访问它们...
[esp + 0] - return address [esp + 4] - parameter 'a' [esp + 8] - parameter 'b' [esp + 12] - parameter 'c'
然而,在我们为局部变量和其他东西留出空间后,这可能会变得笨拙。因此,除了堆栈指针寄存器之外,我们还使用了堆栈基指针寄存器。然而,我们希望堆栈基指针寄存器被设置为当前帧,而不是前一个函数。因此,我们将旧的堆栈指针寄存器保存在堆栈上(这修改了堆栈上参数的偏移量),然后将当前堆栈指针寄存器复制到堆栈基指针寄存器。
push ebp ; save previous stackbase-pointer register mov ebp, esp ; ebp = esp
有时您可能看到仅使用“ENTER”指令即可完成此操作。
第3部分:为局部变量雕刻空间
局部变量存储在堆栈中,由于堆栈变小,我们减去一些字节数(足够存储局部变量):第一个月
第4部分:总结一下。使用堆栈基指针寄存器访问参数...
[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部分:堆栈框架清理
当我们离开例程时,堆栈帧必须被清除。
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)示例,它将通过... addesp来这样做,12),这是“C”执行此操作的方式。第二种情况表示return指令将弹出# words(或者# bytes,我记不清是哪一个了),这样就把参数从堆栈中移除了。如果我没记错的话,这是Pascal使用的风格。
eoigrqb62#
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寄存器访问参数和局部变量。
xn1cxnb43#
这取决于操作系统和使用的语言。由于ASM中的堆栈没有通用格式,堆栈在ASM中所做的唯一事情是在执行跳转子例程时存储返回地址。在执行从子例程返回时,从堆栈中获取地址并将其放入程序计数器(下一个CPU执行指令将从其中读取的存储器位置)您需要查阅文档以了解所使用的编译器。
cidc1ykv4#
编译器可以使用x86堆栈帧(取决于编译器)来传递参数(或指向参数的指针)和返回值。
4条答案
按热度按时间rn0zuynd1#
每个例程都使用堆栈的一部分,我们称之为堆栈框架。虽然汇编程序员不一定要遵循下面的风格,但强烈推荐这种做法。
每个例程的堆栈帧分为三个部分:函数参数、指向上一个堆栈帧的反向指针和局部变量。
第1部分:函数参数
例程堆栈帧的这一部分是由调用者设置的。调用者使用“push”指令将参数推入堆栈。不同的语言可能以不同的顺序推入参数。如果我没记错的话,C是从右到左推入参数的。也就是说,如果你调用...
调用者将把它转换为...
当每一个项目被压入堆栈时,堆栈会向下增长。也就是说,堆栈指针寄存器会减少四(4)个字节(在32位模式下),并且项目会被复制到堆栈指针寄存器所指向的内存位置。注意,“call”指令会隐式地将返回地址压入堆栈。参数的清理将在第5部分中讨论。
第2部分:堆栈帧返回指针
此时,“call”指令已经发出,我们现在正处于被调用例程的开始。如果我们想访问参数,我们可以像这样访问它们...
然而,在我们为局部变量和其他东西留出空间后,这可能会变得笨拙。因此,除了堆栈指针寄存器之外,我们还使用了堆栈基指针寄存器。然而,我们希望堆栈基指针寄存器被设置为当前帧,而不是前一个函数。因此,我们将旧的堆栈指针寄存器保存在堆栈上(这修改了堆栈上参数的偏移量),然后将当前堆栈指针寄存器复制到堆栈基指针寄存器。
有时您可能看到仅使用“ENTER”指令即可完成此操作。
第3部分:为局部变量雕刻空间
局部变量存储在堆栈中,由于堆栈变小,我们减去一些字节数(足够存储局部变量):
第一个月
第4部分:总结一下。使用堆栈基指针寄存器访问参数...
使用堆栈指针寄存器访问局部变量...
第5部分:堆栈框架清理
当我们离开例程时,堆栈帧必须被清除。
有时您可能会看到“LEAVE”指令替换这两条指令。
根据您使用的语言,您可能会看到两种形式的“RET”指令之一。
至于选择哪一种,则视乎所选择的语文而定(或者如果用汇编语言编写,则为您希望遵循的样式)。第一种情况表示调用方负责从堆栈中移除参数(对于foo(a,B,c)示例,它将通过... addesp来这样做,12),这是“C”执行此操作的方式。第二种情况表示return指令将弹出# words(或者# bytes,我记不清是哪一个了),这样就把参数从堆栈中移除了。如果我没记错的话,这是Pascal使用的风格。
eoigrqb62#
x86-32堆栈帧通过执行以下命令创建
所以它可以通过ebp访问
汇编指令设计对堆栈帧使用ebp有一些优点,因此通常使用ebp寄存器访问参数和局部变量。
xn1cxnb43#
这取决于操作系统和使用的语言。由于ASM中的堆栈没有通用格式,堆栈在ASM中所做的唯一事情是在执行跳转子例程时存储返回地址。在执行从子例程返回时,从堆栈中获取地址并将其放入程序计数器(下一个CPU执行指令将从其中读取的存储器位置)
您需要查阅文档以了解所使用的编译器。
cidc1ykv4#
编译器可以使用x86堆栈帧(取决于编译器)来传递参数(或指向参数的指针)和返回值。