一、寄存器
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字节,int
、float
是4字节,long
、double
、char*
是8字节。这种对齐限制简化了处理器和内存系统之间接口的硬件设计,比如我们在内存中读取一个8字节长度的变量,如果这个变量所在的地址是8的倍数,那么就可以通过一次内存操作完成该变量的读取。倘若这个变量所在的地址并不是8的倍数,可能就需要执行两次内存读取,因为该变量被放在两个8字节的内存块中了。
栈的字节对齐,实际是指栈顶指针必须是16
字节的整数倍。栈对齐使得在尽可能少的内存访问周期内读取数据,不对齐堆栈指针可能导致严重的性能下降。所以在做编译器时要对程序实施数据对齐,也就需要定制一个标准:
- 任何内存分配函数(alloca, malloc, calloc或realloc)生成的块的起始地址都必须是16的倍数。
- 函数的栈帧的边界都必须是16字节的倍数。
- 在栈上传递的参数和局部变量,都要满足字节对齐,栈指针(
$sp
)的起始地址必须要是16的倍数。