栈与调用惯例

进程内存布局

每个进程分配的内存由很多部分组成,通常称为“段”。
1.文本段:包含了进程运行的程序二进制机器语言指令,只读,可共享,因为多个进程可同时运行同一程序;
2.初始化数据段:包含显式初始化的全局变量和静态变量;
3.未初始化数据段:也称为BSS段,包含未进行显示初始化的全局变量和静态变量。为什么分开放呢?主要原因在于程序在磁盘上存储时,没有必要为未经初始化的变量分配存储空间,相反可执行文件只需记录未初始化数据段的位置及所需大小,直到运行时再由程序加载器来分配这一空间。
4.栈段:动态增长和收缩的段,由栈帧组成,系统会为每个当前调用的函数分配一个栈帧,存储函数的局部变量、实参和返回值。动态存储函数之间的关系,以保证被调用函数在返回时恢复到母函数继续执行
5.堆:可在运行时为变量动态进行内存分配的一块区域,并在用完之后归还给堆区。
32位计算机限制了虚拟地址空间为4GB的大小。

这里写图片描述

栈与系统栈

栈在内存中的存放是高地址是栈底,低地址是栈顶。
内存的栈区实际上指的就是系统栈,系统栈由系统自动维护,它用于实现高级语言中函数的调用。对于类似C语言这样的高级语言,系统栈的PUSH、POP等栈平衡细节是透明的。一般只有在使用汇编语言开发程序的时候,才需要和它直接打交道。

寄存器与函数栈帧

每一个函数都独占自己的栈帧空间。当前正在运行的函数的栈帧总是在栈顶。栈驻留在内存的高端并向下增长(朝堆的方向)。

(1)ESP

栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶

(2)EBP

基址指针寄存器,ESP就是一直指向栈顶的指针,而EBP只是存放某时刻的栈顶指针,以方便对栈的操作,如取局部变量、函数参数等。

这里写图片描述

函数栈帧

ESP和EBP之间的内存空间为当前栈帧,栈帧中一般包含以下几类信息:
(1)函数实参和局部变量:为函数局部变量开辟的内存空间。
(2)栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部可以通过栈帧平衡计算得到),用于在本栈被弹出后恢复出上一个栈帧。
(3)函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置,以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。
ps:函数栈帧的大小并不固定,一般与其对应的局部变量多少有关。函数运行中,其栈帧大小也是不断变化的。
还有一个至关重要的寄存器是EIP,指令寄存器,其内存放着一个指针,该指针永远指向下一条等待执行的指令地址。

函数调用约定与相关指令

函数调用约定描述了函数传递参数方式和栈帧工作方式的技术细节。不同操作系统、不同语言、不同编译器在实现函数调用原理虽然基本相同,但具体的调用约定还是有差别的,包括参数传递方式、参数入栈顺序是从右向左还是从左向右、函数返回时恢复堆栈平衡的操作在子函数中进行还是在母函数中进行。

函数调用的步骤:

1.参数入栈:将参数从右向左依次压入系统栈中。
2.返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。
3.代码区调转:处理器从当前代码区调转到被调用函数的入口处。
4.栈帧调整:
<1>保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)。
  <2>将当前栈帧切换到新栈帧(将ESP值装入EBP,更新栈帧底部)。
  <3>给新栈帧分配空间(把ESP减去所需空间的大小,抬高栈顶)。
  <4>对于_stdcall调用约定,函数调用时用到的指令序列大致如下:
     push 参数3 ;假设该函数有3个参数,将从右向做依次入栈
     push 参数2
     push 参数1
     call 函数地址 ;call指令将同时完成两项工作:a)向栈中压入当前指令地址的下一个指令地址,即保存返回地址。 b)跳转到所调用函数的入口处。
     push ebp ;保存旧栈帧的底部
     mov ebp,esp ;设置新栈帧的底部 (栈帧切换)
     sub esp,xxx ;设置新栈帧的顶部 (抬高栈顶,为新栈帧开辟空间)

函数返回的步骤如下:

  <1>保存返回值,通常将函数的返回值保存在寄存器EAX中。
  <2>弹出当前帧,恢复上一个栈帧。具体包括:
  (1)在堆栈平衡的基础上,给ESP加上栈帧的大小,降低栈顶,回收当前栈帧的空间。
  (2)将当前栈帧底部保存的前栈帧EBP值弹入EBP寄存器,恢复出上一个栈帧。
  (3)将函数返回地址弹给EIP寄存器。
  <3>跳转:按照函数返回地址跳回母函数中继续执行。
  还是以C语言和Win32平台为例,函数返回时的相关的指令序列如下:
  add esp,xxx ;降低栈顶,回收当前的栈帧
  pop ebp ;将上一个栈帧底部位置恢复到ebp
  ret ;a)弹出当前栈顶元素,即弹出栈帧中的返回地址,至此,栈帧恢复到上一个栈帧工作完成。b)让处理器跳转到弹出的返回地址,恢复调用前代码区

一个很常见的堆栈帧如下:

这里写图片描述

i386 下的函数是这样调用的:

  • 把所有或者一部分参数压入栈中,如果有其他参数没有入栈,则使用某些特定寄存器传递
  • 把当前指令的下一条指令的地址压入栈中
  • 跳转到函数体执行
  • push ebp ;把 ebp 压入栈中
  • mov ebp,esp ;ebp = esp (这是 ebp 指向栈顶,而此时栈顶就是 old ebp)
  • [可选]sub esp, XXX ;在栈上分配XXX字节的临时空间
  • [可选]push XXX ;保存名为 XXX寄存器(可重复多个)

下面是返回

  • [可选]pop XXX ;如有必要,恢复保存过的寄存器(可重复多个)
  • mov esp, ebp ;回复 ESP 同时回收局部变量空间
  • pop ebp ;从栈中恢复保存的 ebp 的值
  • ret ;从栈中取得返回地址,并跳转到该位置

下面是一个多级调用形成的堆栈格局:

这里写图片描述

### RISC-V 架构下的函数调用约定布局 #### 布局概述 在 RISC-V 的 ABI 中,帧的设计是为了支持函数调用过程中的局部变量存储、参数传递以及上下文保存等功能。帧通常由指针(SP)管理,其布局遵循一定的规则以确保跨平台兼容性和效率[^5]。 - **的增长方向**:RISC-V 的是从高地址向低地址增长的。 - **帧结构**: - 底通常是当前函数的返回地址(RA 寄存器的内容会被压入中)。 - 随后是上一层函数的帧边界。 - 局部变量和临时数据会分配在帧的较低地址区域。 #### 参数传递机制 RISC-V 的函数调用约定明确规定了参数如何传递: - 如果参数数量较少,则优先使用寄存器 `a0` 至 `a7` 来传递参数[^1]。 - 当参数超过 8 个或者某些复杂类型的参数无法完全放入寄存器时,剩余的部分将通过进行传递。 - 对于浮点数类型的数据,如果目标硬件支持 FPU,则可能利用专门的浮点寄存器来进行传输[^3]。 #### 堆清理责任 关于堆清理的责任划分,在不同的调用惯例中有差异: - 在 CDECL 类型的标准下,一般是由调用方负责调整指针回到原始位置之后再继续执行后续操作。 - 而对于一些特定优化场景或是中断服务例程等情况,也可能存在被叫函数自行处理退出前恢复现场的情形。 #### 返回值传递方式 针对简单数值类别的结果反馈,常规做法是指定首个可用的结果接收容器即 `a0` 或者组合形式如双精度实数需占用连续两个槽位(`a0`, `a1`)完成交付动作;而对于较大尺寸的对象拷贝任务,则往往预先安排好一块内存缓冲区作为暂存场所并通过额外指示告知确切方位信息以便取回最终产物[^4]。 ```python # 示例代码展示了一个简单的函数调用流程模拟 def function_call_simulation(): # 初始化寄存器 a0-a7 和 sp (假设为简化模型) registers = {'sp': 0x7ffffffc, 'ra': None} def caller_function(): nonlocal registers # 准备参数并设置到相应寄存器中 registers['a0'] = 10 registers['a1'] = 20 # 更新指针预留空间用于保存 ra 及其他必要项 registers['sp'] -= 8 # 模拟跳转至 callee 并记录返回地址 registers['ra'] = addresses['callee'] # 执行实际转移逻辑此处省略具体实现细节 def callee_function(): nonlocal registers # 处理传入参数... result_value = perform_computation(registers['a0'], registers['a1']) # 将结果写入默认返回值寄存器 registers['a0'] = result_value # 清理本地使用的资源并将控制权交还给调用者 restore_context() jump_back_to_address(registers['ra']) caller_function() # 启动整个链条运作 ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值