登录后台

页面导航

本文编写于 322 天前,最后修改于 322 天前,其中某些信息可能已经过时。

函数调用规范

函数调用规范(calling convention)用来描述父子函数时如何编译与链接的,特别是父函数和子函数之间调用关系的约定,例如栈的布局、参数的传递等等。每一个处理器体系结构都有不同的函数调用规范

  • 函数的前8个参数使用a0~a7(x10~x17)寄存器传递
  • 如果函数的参数多于8个,除前8个参数使用寄存器来传递之外,后面的参数使用栈来保存传递
  • 如果传递的参数宽度小于寄存器宽度(64位),那么先按照符号扩展到32位,再按照符号扩展到64位。如果传递的参数是128位,则使用一对寄存器来传递该参数。
  • 函数的返回参数保存到a0和a1寄存器中
  • 如果子函数需要使用s0~s11寄存器,那么在使用前需要将其保存到栈中,使用完之后再从栈中恢复这些寄存器的值
  • 栈是从高地址向低地址增长的,sp寄存器在进入函数时需要对齐到16字节。传递给栈的第一个参数位于sp offset=0处,后续offset依次偏高。
  • GCC使用-fno-omit-frame-pointer编译选项,那么编译器使用s0作为栈帧指针(frame pointer FP)

入栈与出栈

栈stack是一种后进先出的数据存储结构,其主要用于:

  • 保存临时存储的数据,如局部变量等
  • 在函数调用的过程中,如果传递的参数大于8个时,则需要用栈传递参数

通常栈是一种从高地址往低地址扩展的。栈的起始地址被称为栈底。栈从高地址往低地址延伸到的某个点称为栈顶。sp指针来指向栈最新分配的地址,即指向栈顶。fp指针(栈帧指针)用来指向栈底。

当数据入栈时,sp指向的地址减小,栈空间扩大;当数据出栈时,sp指向的地址增大,栈空间缩小。

栈在函数调用的过程中起到非常重要的作用,包括存储函数使用的局部变量、传递参数。在函数调用的过程,栈是逐步生成的。为单个函数分配的栈空间,即从栈底到栈顶的这段空间就称为栈帧。

RISC-V指令集中并没有提供专门的入栈和出栈指令

在RISC-V架构中,每个栈帧的大小至少为16字节。sp寄存器指向栈顶,并且必须按照16字节对齐。从栈底开始的16字节用于存储函数的返回地址ra和fp的值(gcc打开-fno-omit-frame-pointer选项)

RISC-V栈的布局

上文提到了gcc中的-fno-omit-frame-pointer选项用于开关是否使用FP指针。针对这两种不同的情况,栈的布局也有所区别

不使用FP

  • 所有的函数调用栈从高地址向低地址扩展
  • sp指向栈顶
  • 如果调用了子函数,函数的返回地址需要保存在栈里,即s_ra处
  • 栈的大小为16字节的倍数
  • 函数返回时需要先把返回地址从栈s_ra位置处恢复到ra寄存器,然后执行RET指令

Untitled

使用FP

  • 所有的函数调用栈都会组成一个单链表。
  • 每个栈由两个地址来构成这个链表,这两个地址都是64位宽的,并且它们都位于栈底。
  • s_fp的值指向上一个栈帧(父函数的栈帧)的栈底。
  • s_ra保存当前函数的返回地址,也就是父函数调用该函数时的地址。
  • 函数返回时,RISC-V处理器先把返回地址从栈的s_ra位置处载入到当前ra寄存器,然后 执行ret指令
  • 操作系统常用的输出栈信息等技术手段是通过栈帧指针FP来回溯整个栈

Untitled

博主已关闭本页面的评论功能