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个通用寄存器划分为若干类别,并给出了基本调用约定。

  1. 寄存器别名

为了便于阅读和记忆,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,该文件定义了寄存器编号和对应别名。

  1. 寄存器功能介绍

表5-3概括了LoongArch 32个通用寄存器的用途。下面按功能介绍其中几类常用寄存器。

(1) zero寄存器

r0(别名zero)是常量寄存器。无论向它写入什么值,读取时始终返回0。例如,要得到某个变量的相反数,可以用zero寄存器与保存该变量的寄存器做减法,从而避免额外加载立即数0。

sub.w 	$t5, zero, $t4 	//	这里t5和t4互为相反数

zero寄存器也常用于合成指令,使编码更简洁、语义更清楚。例如,LoongArch常用宏指令move可由oradd.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返回。