6.2. LoongArch寄存器使用约定
任何体系架构都会对自身提供的寄存器制定使用约定,用于说明函数调用时哪些寄存器负责传参、哪些寄存器保存返回值、哪些寄存器可临时使用且无需保留旧值等。寄存器使用约定是汇编层面组织函数调用的基础,也让大型程序能够按模块拆分和协作。
LoongArch ABI分别规定了通用寄存器和浮点寄存器的使用方式。
6.2.1. 通用寄存器使用约定
LoongArch定义了32个通用寄存器(General-Purpose Register,GPR),记为r0~r31。各通用寄存器的别名和使用约定如表5-3所示。
| 名称 | 别名 | 用途 | 在调用中是否保留 | 保存者 |
|---|---|---|---|---|
| $r0 | $zero | 常数0 | (常数) | 不存在 |
| $r1 | $ra | 返回地址 | 否 | 被调用者 |
| $r2 | $tp | 线程指针 | 不可分配 | 不可用 |
| $r3 | $sp | 栈指针 | 是 | 被调用者 |
| $r4-$r5 | $a0-$a1 | 传参数寄存器、返回值寄存器 | 否 | 不存在 |
| $r6-$r11 | $a2-$a7 | 传参数寄存器 | 否 | 不存在 |
| $r12-$r20 | $t0-$t8 | 临时寄存器 | 否 | 调用者 |
| $r21 | 保留 | 不可分配 | 不可用 | |
| $r22 | $fp/$s9 | 栈帧指针、静态寄存器 | 是 | 被调用者 |
| $r23-$r31 | $s0-$s8 | 静态寄存器 | 是 | 被调用者 |
表5-3按功能将32个通用寄存器划分为若干类别,并给出了基本调用约定。
寄存器别名
为了便于阅读和记忆,ABI为每个寄存器定义了带有功能含义的别名。别名通常来自寄存器用途的英文首字母、数字或缩写。例如,r4的别名为a0,其中a表示arguments,即它可作为函数调用的第一个参数寄存器;r5的别名为a1,可作为第二个参数寄存器;r3的别名为sp,是stack pointer的缩写,表示栈指针。编写汇编指令时,可以使用寄存器编号,也可以使用别名。下面两条指令功能完全相同。
addi.d $r3, $r3, 32
addi.d $sp, $sp, 32
这两条指令都实现寄存器与立即数32相加。但使用别名sp时,读者能直接看出该操作针对栈指针。实际编写汇编程序时,更推荐使用寄存器别名,以提升可读性。本书后续示例也会尽量采用别名形式。
使用LoongArch寄存器别名时,需要包含头文件regdef.h,该文件定义了寄存器编号和对应别名。
寄存器功能介绍
表5-3概括了LoongArch 32个通用寄存器的用途。下面按功能介绍其中几类常用寄存器。
(1) zero寄存器
r0(别名zero)是常量寄存器。无论向它写入什么值,读取时始终返回0。例如,要得到某个变量的相反数,可以用zero寄存器与保存该变量的寄存器做减法,从而避免额外加载立即数0。
sub.w $t5, zero, $t4 // 这里t5和t4互为相反数
zero寄存器也常用于合成指令,使编码更简洁、语义更清楚。例如,LoongArch常用宏指令move可由or或add.d等真实指令实现。
// 宏指令 ---> // 有效汇编指令
move $t0, $t1 or $t0, $t1, zero
宏指令是为方便软件编程或增强语义表达而定义的指令形式。编译时,汇编器会把宏指令转换为处理器能够识别并执行的真实指令。也就是说,宏指令不是体系架构直接提供的机器指令,而是由汇编器识别并翻译的辅助指令。
LoongArch没有用于两个寄存器之间直接复制数据的真实move指令。上面的or t0,t1,zero表示将寄存器t1与zero做或运算,并把结果写入t0,从而完成t1到t0的数据复制。汇编器也可以使用真实指令add.d t0, t1, zero实现同样功能。
(2)函数调用与寄存器 v0~v1、a0~a7、ra
LoongArch ABI规定,函数调用时,a0~a7用于传递前8个整型参数或指针参数;其中a0和a1(别名也可为v0和v1)还用于保存返回值;ra用于保存返回地址。
例如,下面是一个带2个整型参数和1个整型返回值的加法函数调用:
int ret = add(2,3);
用汇编语言实现如下:
add:
add.w $a0, $a0, $a1
jirl $zero, $ra, 0
main:
li.w $a0, 0x2
li.w $a1, 0x3
bl add
其中,add:和main:是标签,用来表示函数入口。main函数前两条指令把整型参数2和3分别写入a0、a1。第3条bl指令把当前PC+4写入ra,并跳转到函数add。在add中,add.w a0, a0, a1完成两个参数相加,并把结果写入a0(v0)作为返回值;jirl zero, ra, 0返回到main。
需要注意,LoongArch中v0、v1与a0、a1分别是同一组寄存器,也就是返回值寄存器和参数寄存器复用。因此,编写汇编代码时要避免无意覆盖仍需使用的值。
超过8个整型参数、包含浮点参数、返回结构体等更复杂的调用规则,将在5.3节介绍。
(3)临时寄存器t0~t8和保存寄存器s0~s8
临时寄存器t0~t8中的t可理解为temporary,常在函数内部充当临时变量。使用这些寄存器时,函数通常不需要保存它们的旧值。
保存寄存器s0~s8中的s表示saved。当前函数必须保证这些寄存器在函数返回时与函数入口处的值一致。如果函数内部需要使用某个s寄存器,应先把旧值保存到栈上,并在返回前恢复。例如,函数中使用s0时,入口和出口附近通常会出现如下进栈和出栈操作。
st.d $s0, $sp, 32
...
ld.d $s0, $sp, 32
这里假设使用s0前,把s0保存到栈中sp+32的位置;省略号处可自由使用s0;函数退出前,再用ld.d从同一栈位置恢复s0的旧值。
(4)tp寄存器
tp寄存器用于支持线程局部存储(Thread-Local Storage,TLS)。TLS是一种线程局部变量机制,能够保证变量在线程内部全局可访问,但不会被其他线程访问。例如,libc库中的_Thread_local errno就是典型线程局部变量,用于记录当前线程最近的错误编号。LoongArch ABI专门使用一个寄存器指向当前线程的TLS区域,以便快速定位和访问该区域中的变量,提高执行效率。通常,tp寄存器由系统libc库维护,用户程序最好不要修改它。
(5)函数栈和寄存器sp、fp
在数据结构中,栈(Stack)是一种只允许在同一端插入和删除数据的动态存储空间,遵循先进后出原则。函数栈也是动态存储空间,用于保存函数内局部变量和相关寄存器,但使用方式并不一定像抽象数据结构中的栈那样严格。不同函数的参数数量和局部变量数量不同,所需函数栈大小也不同。LoongArch ABI规定使用sp、fp记录每个函数栈的边界。具体函数栈约定将在5.4节介绍。
6.2.2. 浮点寄存器使用约定
LoongArch提供32个浮点寄存器,记为f0~f31。这些浮点寄存器的使用约定如表5-4所示。
| 名称 | 别名 | 用途 | 在调用中是否保留 | 保存者 |
|---|---|---|---|---|
| $f0-$f1 | $fa0-$fa1 | 传参数寄存器、返回值寄存器 | 否 | 不存在 |
| $f2-$f7 | $fa2-$fa7 | 传参数寄存器 | 否 | 不存在 |
| $f8-$f23 | $ft0-$ft15 | 临时寄存器 | 否 | 调用者 |
| $f24-$f31 | $fs0-$fs7 | 静态寄存器 | 是 | 被调用者 |
与整数寄存器相比,浮点寄存器使用约定更简单。根据用途不同,浮点寄存器也有不同别名。f0~f7共8个浮点寄存器用于传递函数浮点参数,别名为fa0~fa7;其中f0和f1还用于保存函数返回值,别名也可写为fv0和fv1。临时浮点寄存器别名含t,共16个,即ft0~ft15;保存浮点寄存器别名含s,共8个,即fs0~fs7。
例如,实现两个浮点数相加的函数,对应C语言代码如下:
float fadd(float var1, float var2){
return var1 + var2;
}
用汇编语言实现如下:
fadd:
fadd.s $fv0, $fa1, $fa0
jirl $zero, $ra, 0
其中,参数寄存器fa0、fa1分别保存var1和var2,浮点加法结果写入fv0,并通过jirl返回。