栈,栈帧Stack Frames和函数调用过程Control Flow

您所在的位置:网站首页 栈帧大小在什么时候确定 栈,栈帧Stack Frames和函数调用过程Control Flow

栈,栈帧Stack Frames和函数调用过程Control Flow

2024-01-31 15:21| 来源: 网络整理| 查看: 265

栈其实就是计算机系统内存中的一小块。栈是一块特殊的内存区域,栈在内存中的增长方向是向低地址扩展,%rsp寄存器存储栈的最低地址,即栈顶元素的地址。这种栈结构在程序中的应用有助于实现函数调用、局部变量的管理以及递归等功能。

 Push和Pop指令

内存中中的栈可以进行push和pop指令,如果你学过数据结构中的栈,基本就是类似的。 

Pushq

pushq Src操作包括以下步骤:

从Src获取操作数。将%rsp寄存器的值减8(因为x86-64架构中,一个二进制字长度为8字节)。将操作数写入%rsp寄存器指向的地址。

通过pushq操作,我们将数据压入栈顶,并更新了栈顶指针%rsp

popq

popq Dest操作包括以下步骤:

读取%rsp寄存器指向的地址处的值。将%rsp寄存器的值加8。(越往上地址越高,这里是向上缩小了)将读取到的值存储在Dest(通常是一个寄存器)。

关于pop要注意的是,我们仅仅是把原来的值copy了一份给了Dest, %rsp的值虽然加了8往上移动了,但是原来%rsp所指的值依然存在在内存的那个位置中。

程序控制流Control Flow

在程序执行过程中,栈结构被用于支持函数的调用和返回。

函数调用:call指令

当需要调用一个函数时,使用call label指令。它执行以下操作:

将返回地址(return address)压入栈中。(返回地址是紧跟在call指令后的下一条指令的地址。这一点很好理解,执行完call后就要回来继续往下执行嘛。)跳转(jump)至label指定的函数起始位置。 函数返回:ret指令

当函数执行完毕,需要返回到调用它的位置时,使用ret指令。它执行以下操作:

从栈中弹出地址(pop address from stack)。跳转(jump)至弹出的地址。

下面放一个例子,我配上了注释说明,应该很好理解。图中%rip存的是当前执行指令地址,即instruction pointer。

 栈存储额外参数

在我们调用函数时,如果函数的参数少于等于6个,我们可以用%rdi %rsi %rdx %rcx %r8 %r9这几个寄存器分别存储这六个参数,但如果参数多于6个,从第七个开始,我们就会把他们放到栈里。

Stack Frames栈帧

啥是栈帧?当调用一个函数时,会使用 call 指令。call 指令会将返回地址压入栈中,然后跳转到目标函数的起始位置。此时,新的栈帧会被分配和设置,用于存储目标函数的参数、局部变量等。

返回地址属于调用者(上一个函数)的栈帧。当一个函数被调用时,它的返回地址会被call指令压入到当前栈帧的顶部,紧接着就是下一个(被调用)函数的栈帧。

基于栈的编程语言通常支持递归,例如C、Pascal和Java等。这些编程语言的代码必须是"可重入的"(Reentrant),这意味着单个函数可以拥有多个同时进行的实例。为了实现这种功能,我们需要某种方式来存储每个函数实例的状态,包括参数、局部变量和返回指针等。栈规则(Stack Discipline)是为了实现上述需求而采用的策略。对于给定的函数实例,我们只需要在有限的时间内维护其状态,即从函数被调用到函数返回的这段时间。此外,被调用函数(callee)在调用函数(caller)返回之前就需要返回。

为了实现栈规则,我们将栈分配为栈帧。每个栈帧代表单个函数实例的状态。当一个函数被调用时,将为其分配一个新的栈帧,用于存储其参数、局部变量和返回指针等。当函数返回时,将释放其栈帧,从而为其他函数实例提供空间。

栈帧是用于存储函数实例状态的数据结构。每个栈帧包含以下内容:

返回信息:包括返回地址(return address)和可能的保存的寄存器值。局部存储(如果需要的话):用于存储函数内的局部变量。临时空间(如果需要的话):用于存储函数执行期间可能需要的临时数据。

栈帧的分配和回收在函数的调用和返回过程中完成:

分配空间:当进入函数时,先将返回地址压入栈中,再会为其分配一个栈帧。这个过程包括pushq指令(由call指令执行)。回收空间:当函数返回时,会释放其栈帧。这个过程包括popq指令(由ret指令执行)将返回地址从栈中弹出。

上面的caller frame就是调用者的栈帧,caller又调用了另一个函数,于是有了新的栈帧。

Caller Saved 和Callee Saved 寄存器

最后再补充两个概念:Caller Saved 和Callee Saved。

当一个函数掉用另一个函数时,另一个函数进行计算时同样会用到寄存器来存储数据,那么难免会影响到前一个函数在寄存器上面的值,因此有这么一种规范或者约定,来帮助程序合理地使用寄存器而不会彼此冲突。

调用方保存寄存器(caller-saved register)也叫( “Call-Clobbered”)

这个术语的意思是,在调用过程(函数或子程序)之前,调用方有责任保存这些寄存器的值。这是因为被调用方可能会修改这些寄存器的值,而不需要负责恢复它们。换句话说,如果调用方关心这些寄存器的值,它需要在调用过程之前将这些值保存到内存中,在调用过程完成并返回后再将它们恢复到寄存器中。

属于caller-saved register的寄存器有:%rdi %rsi %rdx %rcx %r8 %r9 %r10 %r11

被调用方保存寄存器(callee-saved register)也叫(“Call-Preserved”)

当一个函数或子程序(被调用方)计划使用这些寄存器时,它负责在执行过程中保留这些寄存器的初始值,并在过程结束前恢复它们。这样,调用方可以确信在调用过程返回后,这些寄存器的值保持不变。被调用方保存寄存器的目的是减轻调用方的负担,因为它无需为这些寄存器执行保存和恢复操作。因此被调用方保存寄存器和调用方保存寄存器是互补的。

属于callee-saved register的寄存器有:%rbx, %r12, %r13, %r14

简单来说,caller-saved register是会被子函数改变的,调用者要自己通过提前存储的方式保存下来;而callee-saved register是可以保证调用结束后值不变的,调用者不用操心它们~



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3