寄存器和运行时栈(LoongArch体系)

一、寄存器

1.1 通用寄存器

在LoongArch体系中,有32个通用寄存器,除了0号寄存器始终为0外,其他31个寄存器物理上没有区别。但系统人为添加了一些约定,给了它们特定的名字和使用方式。
在这里插入图片描述

对以上通用寄存器详细说明:

  • $zero寄存器中存放的值永远是零,且不能改变,这个寄存器的用途主要是方便编码。
  • $ra 是一个临时寄存器,控制流到达callee瞬间的 $ra 的值是callee的返回地址,且这个值只会被备份在栈帧中,callee返回指令是 jirl $r0,$ra,0
  • $tp 用于支持 TLS ( thread-local storage ),是用户程序中一段线程独有的空间( TLS block )。TLS block容纳可执行文件及其所需动态库的所有 .tbss.tdata 段,从而支持c的 _Thread_local 的变量( errno 是一个典型)。原理是,归纳基础,在装载时求出TLS block的大小,从堆区分配空间赋值给 $tp (推广一下,调用栈也是TLS);归纳步骤,在用户程序调用pthread接口创建线程时故技重施。也就是说, $tp 由c库维护,用户程序不应修改这个寄存器。如程序没有运行在c库之上,比如对于系统软件, $tp 只是一个没有用到的寄存器罢了
  • $sp在整个程序执行过程中恒指向调用栈栈顶,是一个保存寄存器。控制流到达callee瞬间的 $sp 的值被称作 $sp on entry ,是一个frame pointer。。
  • 通用寄存器有8个用于整型参数传递,寄存器名字依次是$a0~$a7(编号依次是$r4~$r11),$a0$a1也用于存放返回结果值;浮点寄存器也有8个用于浮点传递参数,寄存器名称依次是$fa0~$fa7$fa0$fa1也用于存放返回的浮点结果值。
  • $t0~%t8 寄存器为临时寄存器,不需要暂存其值。
  • $fp 是一个保存寄存器,但如果过程的栈帧可变,编译器会使用这个寄存器作为 frame pointer (栈帧不变部分的地址加常数偏移量,也指存放该值的寄存器)。所以,栈帧不变的函数的frame pointer是 $sp ,栈帧可变的函数的frame pointer是 $fp

1.2 浮点寄存器

在LoongArch体系中,也有32个浮点寄存器,$fa0~$fa7为参数寄存器,共8个,$fa0$fa1也用于存放返回的浮点结果值;$ft0~$ft15为临时寄存器,共16个。
在这里插入图片描述

二、运行时栈

2.1 运行时栈的基本概念

常说的进程栈则是进程的运行时栈,运行时栈主要用来传递参数(寄存器不够用时)、存储程序的返回信息(寄存器不够用时)、保存寄存器中的值(临时存储一下,待寄存器需要的时候再复制回去)、以及存储局部变量。运行时栈的增长方向和地址增长方向相反,见下图。
在这里插入图片描述
栈有三个常用概念,栈低、栈帧和栈顶。栈帧是栈上存放信息的每一个位置,栈低是栈的最低部的栈帧,栈顶则是最顶部的栈帧。栈顶指针是栈顶的地址值,永远存放在寄存器$sp(x86是%rsp)中,寄存器$fp指向当前函数的栈帧开始处,CPU是通过改变寄存器$sp和寄存器$fp的值来控制运行时栈的。

栈采用后进先出的内存管理,其内存不需要显示释放和申请,在需要栈内存的时候,只需要将栈顶扩充($sp值减小),释放栈内存的时候,将栈顶缩小($sp值增大)。编译器为函数在入口处生成一个函数头(Prologue),在返回处生成一个函数尾(Epilogue),它们负责调整$sp$fp寄存器以生成新的栈帧或者释放一个栈帧,并生成必要的寄存器保存和恢复代码。

思考一个问题$fp寄存器指向当前函数的开始处栈帧,$sp寄存器指向栈顶,则使用$fp$sp都可以访问栈上的元素,是不是说只用$sp寄存器管理栈就ok,不需要用$fp

  • 大部分函数可以只用$sp来管理栈帧。如果在编译时能够确定函数的栈帧大小,编译器可以在函数头分配所需的栈空间(通过调整$sp),这样在函数栈帧里的内容都有一个编译时确定的相对于$sp的偏移,也就不需要帧指针$fp了。

    // 函数原型
    int testF(){
          
          
      int a = 1;
      return a;
    }
    // 生成LoongArch上面对应的汇编码
    testF:
      addi.d  $r3,$r3,-32
      st.d  $r22,$r3,24
      addi.d  $r22,$r3,32
      addi.w  $r12,$r0,1      # 0x1
      st.w  $r12,$r22,-20
      ldptr.w $r12,$r22,-20
      or  $r4,$r12,$r0
      ld.d  $r22,$r3,24
      addi.d  $r3,$r3,32
      jr  $r1 
    
  • 但有时候可能无法在编译时确定一个函数的栈帧大小。在某些语言中,可以在运行时动态分配栈空间,如C程序的alloca调用,这会改变$sp的值。这时函数头会使用$fp寄存器,将其设置为函数入口时的$sp值,函数的局部变量等栈帧上的值则用相对于$fp的常量偏移来表示。

2.2 运行时栈字节对齐

许多计算机系统对基本数据类型的合法地址做了一些限制,要求某种类型对象所在的地址必须是某个值K的倍数。如char是1字节,short是2字节,intfloat是4字节,longdoublechar*是8字节。这种对齐限制简化了处理器和内存系统之间接口的硬件设计,比如我们在内存中读取一个8字节长度的变量,如果这个变量所在的地址是8的倍数,那么就可以通过一次内存操作完成该变量的读取。倘若这个变量所在的地址并不是8的倍数,可能就需要执行两次内存读取,因为该变量被放在两个8字节的内存块中了。

栈的字节对齐,实际是指栈顶指针必须是16字节的整数倍。栈对齐使得在尽可能少的内存访问周期内读取数据,不对齐堆栈指针可能导致严重的性能下降。所以在做编译器时要对程序实施数据对齐,也就需要定制一个标准:

  • 任何内存分配函数(alloca, malloc, calloc或realloc)生成的块的起始地址都必须是16的倍数。
  • 函数的栈帧的边界都必须是16字节的倍数。
  • 在栈上传递的参数和局部变量,都要满足字节对齐,栈指针($sp)的起始地址必须要是16的倍数。

猜你喜欢

转载自blog.csdn.net/qq_42570601/article/details/121621844