7.4. 寄存器使用约定

龙架构有32个通用寄存器($r0-$r31)和32个浮点寄存器($f0-$f1)(可选)。
在LoongArch32或者LA32架构中,寄存器宽度为32-bit,第0位位于寄存器作右边,第31位位于最左边;
在LoongArch64或者LA64架构中,寄存器宽度为64-bit,第0位位于寄存器作右边,第63位位于最左边;

如果是支持向量扩展,在LSX扩展中,还有32个向量寄存器。记作v0-v31。支持LSX的向量寄存器的数据位宽是128位。
在LASX中,有32个扩展向量寄存器,记作x0-x31。扩展向量寄存器的位宽都是256位。

注意: 在龙架构中,每个向量寄存器vx的低半部分(也就是63-0位)和同号的浮点寄存器fx是重合的。
也就是说,当执行了某一条的LSX向量指令,更新了寄存器vx,则对应的同编号的fx寄存器也会被修改。

同理,对于LASX扩展,每个扩展向量寄存器xx的低128位与向量寄存器vx是重合的,每个扩展向量寄存器xx
的低64位与浮点寄存器fx是重合的。

我们在讨论函数调用规约时,还是以通用寄存器r0-r31和浮点寄存器为主。

下面的表格展示了通用寄存器的使用规约。

名称 别名 用途 在调用中是否保留 保存者
$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 静态寄存器 被调用者

下面的表格展示了浮点寄存器的使用规约。

名称 别名 用途 在调用中是否保留 保存者
$f0-$f1 $fa0-$fa1 传参数寄存器、返回值寄存器 不存在
$f2-$f7 $fa2-$fa7 传参数寄存器 不存在
$f8-$f23 $ft0-$ft15 临时寄存器 调用者
$f24-$f31 $fs0-$fs7 静态寄存器 被调用者

临时寄存器也称为调用者保存寄存器,静态寄存器也称为被调用者保存寄存器。

调用者和被调用者如下所示:

void callee_func(int num) {
	//do anythings.
	printf("callee: %d\n", num);
}

void caller_func(int arg) {
	// do_some_thing...
	callee_func(arg++);
}

以上面的C函数来说明上述的术语。我们称caller_func为调用者,称callee_func为被调用者。

调用者caller_func调用了被调用者函数callee_func。

Tip

我们所说的调用者或者被调用者保存指的是汇编层面的寄存器,由编译器生成。我们手动写汇编调用库函数时
也需要满足我们上述的调用规则。

7.4.1. 使用说明

上述调用规约表格中,调用者保存和被调用者保存是C语言等高级语言遵循的。

  • 表格中不存在或者不分配,说明此寄存器在函数过程中不会主动的使用,在高级语言比如C中,除非你手动嵌入内敛汇编代码,
    否则不会被使用。比如下面的示例:

    static inline uintptr_t __get_tp()
    {
    	register uintptr_t tp __asm__("tp");
    	__asm__ ("" : "=r" (tp) );
    	return tp;
    }
    

    上述代码我们获得$tp寄存器的值作为线程结构体的指针。在正常的函数编译过程中,是不会分配tp寄存器的。
    类似的$r21寄存器。

    我们在Linux内核中,LoongArch也是使用$tp寄存器作为一个全局的寄存器,用于存放thread infomation。

    register struct thread_info *__current_thread_info __asm__("$tp");
    
    static inline struct thread_info *current_thread_info(void)
    {
    	return __current_thread_info;
    }
    
  • $ra寄存器保存着是当前函数返回的地址。函数返回时使用这个寄存器进行跳转。

    • 当当前函数内部有调用其他函数的情况下时,$ra寄存器保存在当前栈帧的顶端。

    • 如果当前的函数内部没有调整,ra寄存器不做任何处理。内部函数不能随意分配使用。

  • $fp/$s9寄存器,用于保存栈帧结束的地址,此处我们认为我们的栈是由高到底增长。

    • 当我们编译程序的时候,使用-O0(没有任何优化时),程序默认使用$fp指针 如下反汇编代码所示:

    func:
    20:   02be8063 	addi.w      	$sp, $sp, -96
    24:   29817061 	st.w        	$ra, $sp, 92
    28:   29816076 	st.w        	$fp, $sp, 88
    2c:   02818076 	addi.w      	$fp, $sp, 96
    30:   29bef2c4 	st.w        	$a0, $fp, -68
    34:   29bee2c5 	st.w        	$a1, $fp, -72
    ...
    
    134:  00150184 	move        	$a0, $t0
    138:  28817061 	ld.w        	$ra, $sp, 92
    13c:  28816076 	ld.w        	$fp, $sp, 88
    140:  02818063 	addi.w      	$sp, $sp, 96
    144:  4c000020 	ret
    

    栈帧指针$fp寄存器用户保存栈顶的地址。addi.w  $sp, $sp, -96先开辟一个96字节的栈空间,
    然后使用addi.w  $fp, $sp, 96将fp指向其顶端。

    后续的基于栈的寻址都是依据$fp寄存器来寻找。

    函数刚开始时,先保存fp再更新新的fp,当函数返回前,恢复旧的fp寄存器。

    • 当我们使用编译器优化,比如-O1/-O2/-O3/-Os时,默认会不使用$fp。 但是,我们使用-fno-omit-frame-pointer 编译器选项,来强制使用$fp寄存器。

    比如下面的代码:

    c:	02bf4063 	addi.w      	$sp, $sp, -48
    10:	2980b061 	st.w        	$ra, $sp, 44
    14:	2980a076 	st.w        	$fp, $sp, 40
    18:	29809077 	st.w        	$s0, $sp, 36
    1c:	0280c076 	addi.w      	$fp, $sp, 48
    ...
    a8:	2880b061 	ld.w        	$ra, $sp, 44
    ac:	2880a076 	ld.w        	$fp, $sp, 40
    b0:	28808078 	ld.w        	$s1, $sp, 32
    b4:	28807079 	ld.w        	$s2, $sp, 28
    b8:	2880607a 	ld.w        	$s3, $sp, 24
    bc:	2880507b 	ld.w        	$s4, $sp, 20
    c0:	001502e4 	move        	$a0, $s0
    c4:	28809077 	ld.w        	$s0, $sp, 36
    c8:	0280c063 	addi.w      	$sp, $sp, 48
    cc:	4c000020 	ret
    

    上述的代码和前面的代码是同一个C函数编译的,只是增加了-O2和-fno-omit-frame-pointer参数。

    可以看出$fp寄存器只是保存了函数的栈顶,在函数进入和退出时会保存和恢复其值。

    但是在实际使用中,并不会像-O0那样使用$fp寻址,还是会使用$sp栈指针来寻址。

Tip

那栈指针有什么用呢?

随着栈指针在加入优化的情况下,会保存到栈顶,进入函数和退出函数时需要恢复(额外的两次访存,保存和加载)。

栈指针在程序的调试,或者在出现异常的情况下,能够快速的定位到函数的调用链,即子函数fp–>父函数fp,一直追溯到最开始
调用的地方。因此在栈回溯(unwind)中有很重要的应用。

在有栈指针的函数栈帧中,当前栈的$fp-4(32位架构),$fp-8(64位架构)保存的是返回地址$ra寄存器。
而当前栈的$fp-8(32位架构),$fp-16(64位架构)保存的是上一个函数的栈帧$fp的值。

  • $a0-$a1寄存器保存着函数的返回值。

    • 在32位架构(LoongArch32)中,如下代码所示:

    long long get_long_value(int a) {
    long long res = 0;
    res = a * a;
    return res;
    }
    

    它的反汇编如下代码所示:

    20:	  02bf4063 	   addi.w      	$sp, $sp, -48
    24:	  2980b076 	   st.w        	$fp, $sp, 44
    28:	  0280c076 	   addi.w      	$fp, $sp, 48
    2c:	  29bf72c4 	   st.w        	$a0, $fp, -36
    30:	  0015000c 	   move        	$t0, $zero
    34:	  0015000d 	   move        	$t1, $zero
    38:	  29bfa2cc 	   st.w        	$t0, $fp, -24
    3c:	  29bfb2cd 	   st.w        	$t1, $fp, -20
    40:	  28bf72cc 	   ld.w        	$t0, $fp, -36
    44:	  001c318c 	   mul.w       	$t0, $t0, $t0
    48:	  29bfa2cc 	   st.w        	$t0, $fp, -24
    4c:	  0048fd8c 	   srai.w      	$t0, $t0, 0x1f
    50:	  29bfb2cc 	   st.w        	$t0, $fp, -20
    54:	  28bfa2cc 	   ld.w        	$t0, $fp, -24
    58:	  28bfb2cd 	   ld.w        	$t1, $fp, -20
    5c:	  00150184 	   move        	$a0, $t0       // 返回值a0
    60:	  001501a5 	   move        	$a1, $t1       // 返回值a1
    64:	  2880b076 	   ld.w        	$fp, $sp, 44
    68:	  0280c063 	   addi.w      	$sp, $sp, 48
    6c:	  4c000020 	   ret
    

    当函数的返回值为long long 也就是64位时,使用两个寄存器$a0-$a1作为返回值寄存器

    当函数返回值为指针,int,long时,使用$a0作为返回寄存器。

    • 在64位架构(LoongArch64)中,如下代码所示:

    struct test_abi_foo64
    {
    	long value;
    	int * ptr;
    };
    
    struct test_abi_foo64 get_long_value(struct test_abi_foo64 a) {
    	struct test_abi_foo64 foo;
    	foo.value = a.value + a.value;
    	foo.ptr = (long)a.ptr << 2;
    	return foo;
    }
    

    反汇编代码如下:

    000000000 <get_long_value>:
       0:	02ff4063 	addi.d      	$sp, $sp, -48
       4:	29c0a076 	st.d        	$fp, $sp, 40
       8:	02c0c076 	addi.d      	$fp, $sp, 48
       c:	29ff42c4 	st.d        	$a0, $fp, -48
      10:	29ff62c5 	st.d        	$a1, $fp, -40
      14:	28ff42cc 	ld.d        	$t0, $fp, -48
      18:	0041058c 	slli.d      	$t0, $t0, 0x1
      1c:	29ff82cc 	st.d        	$t0, $fp, -32
      20:	28ff62cc 	ld.d        	$t0, $fp, -40
      24:	0041098c 	slli.d      	$t0, $t0, 0x2
      28:	29ffa2cc 	st.d        	$t0, $fp, -24
      2c:	28ff82cc 	ld.d        	$t0, $fp, -32
      30:	28ffa2cd 	ld.d        	$t1, $fp, -24
      34:	0015018e 	move        	$t2, $t0
      38:	001501af 	move        	$t3, $t1
      3c:	001501c4 	move        	$a0, $t2
      40:	001501e5 	move        	$a1, $t3
      44:	28c0a076 	ld.d        	$fp, $sp, 40
      48:	02c0c063 	addi.d      	$sp, $sp, 48
      4c:	4c000020 	ret
    

    当结构体的成员可以使用两个寄存器传递时,通过a0和a1来传递。

  • 传递参数时,a0-a7传递参数时,每次函数调用都会准备参数。并不会重复利用a2-a7寄存器。

如下代码所示:

extern int foo(int a, int b, int c, int d, int e, 
               int f, int g, int h, int i, int j); 

extern int foo1(int a1, int b1, int c1, int d1, int e1, 
                int f1, int g1, int h1, int i1, int j1); 

int func(int arg0, int arg1) {
	int res = 0;
	int a = 1, b = 2, c = 3, d = 4, e = 5, f = 6, g = 7;
	int h = 8, i = 9, j = 10;

	res =  foo(a, b, c, d, e, f, g, h, i, j);

	res += foo1(a+arg0, b+arg1, c, d, e, f, g, h, i, j);

    return res;
}

我们在函数func中调用了foo和foo1函数,其中foo和foo1的部分参数是重合的。

其汇编代码如下所示:

00000000 <func>:
func():
   0:	02bf4063 	addi.w      	$sp, $sp, -48
   4:	2980b061 	st.w        	$ra, $sp, 44
   8:	2980a077 	st.w        	$s0, $sp, 40
   c:	29809078 	st.w        	$s1, $sp, 36
  10:	29808079 	st.w        	$s2, $sp, 32
  14:	2980707a 	st.w        	$s3, $sp, 28
  18:	2980607b 	st.w        	$s4, $sp, 24
  1c:	0015009a 	move        	$s3, $a0
  20:	001500b9 	move        	$s2, $a1

  24:	02802818 	li.w        	$s1, 10
  28:	29801078 	st.w        	$s1, $sp, 4
  2c:	02802417 	li.w        	$s0, 9
  30:	29800077 	st.w        	$s0, $sp, 0
  34:	0280200b 	li.w        	$a7, 8
  38:	02801c0a 	li.w        	$a6, 7
  3c:	02801809 	li.w        	$a5, 6
  40:	02801408 	li.w        	$a4, 5
  44:	02801007 	li.w        	$a3, 4
  48:	02800c06 	li.w        	$a2, 3
  4c:	02800805 	li.w        	$a1, 2
  50:	02800404 	li.w        	$a0, 1

  54:	54000000 	bl          	foo	# 54
  
  58:	0015009b 	move        	$s4, $a0
  5c:	29801078 	st.w        	$s1, $sp, 4
  60:	29800077 	st.w        	$s0, $sp, 0
  64:	0280200b 	li.w        	$a7, 8
  68:	02801c0a 	li.w        	$a6, 7
  6c:	02801809 	li.w        	$a5, 6
  70:	02801408 	li.w        	$a4, 5
  74:	02801007 	li.w        	$a3, 4
  78:	02800c06 	li.w        	$a2, 3
  7c:	02800b25 	addi.w      	$a1, $s2, 2
  80:	02800744 	addi.w      	$a0, $s3, 1

  84:	54000000 	bl          	foo1 # 84

  88:	00101364 	add.w       	$a0, $s4, $a0
  8c:	2880b061 	ld.w        	$ra, $sp, 44
  90:	2880a077 	ld.w        	$s0, $sp, 40
  94:	28809078 	ld.w        	$s1, $sp, 36
  98:	28808079 	ld.w        	$s2, $sp, 32
  9c:	2880707a 	ld.w        	$s3, $sp, 28
  a0:	2880607b 	ld.w        	$s4, $sp, 24
  a4:	0280c063 	addi.w      	$sp, $sp, 48
  a8:	4c000020 	ret

由上面的汇编代码可以看出,在调用了foo函数之前,准备了参数24-50

在调用foo1时,也准备了参数64-80

不能因为foo和foo1的部分参数一致,而导致只准备一次的参数加载。(此处不能优化!)

即每次函数调用前,都必须满足准备参数到a0-a7寄存器。

  • 调用临时寄存器t0-t8时,调用者必须得保存。 函数调用前后,必须保存和重新加载寄存器的值,否则会出现错误。

  30:	0015000c 	move        	$t0, $zero
  34:	0015000d 	move        	$t1, $zero
  38:	29bfa2cc 	st.w        	$t0, $fp, -24
  3c:	29bfb2cd 	st.w        	$t1, $fp, -20
  40:	28bf72cc 	ld.w        	$t0, $fp, -36
  44:	001c318c 	mul.w       	$t0, $t0, $t0
  48:	29bfa2cc 	st.w        	$t0, $fp, -24
  4c:	0048fd8c 	srai.w      	$t0, $t0, 0x1f
  
  // 中间有函数调用foo_func,之后还会在12c出调用$t0寄存器
  // 因此这里需要保存
  50:	29bfb2cc 	st.w        	$t0, $fp, -20
  
  // 此时准备参数
  a4:	02800b25 	addi.w      	$a1, $s2, 2
  a8:	0280200b 	li.w        	$a7, 8
  ac:	02801c0a 	li.w        	$a6, 7
  b0:	02801809 	li.w        	$a5, 6
  b4:	02801408 	li.w        	$a4, 5
  b8:	02801007 	li.w        	$a3, 4
  bc:	02800c06 	li.w        	$a2, 3
  c4:	54000000 	bl          	foo_func
  
  // ... ...
  
  // 函数调用后,必须重新加载寄存器$t0,然后参与t0的计算
  128:	29bfb2cc 	ld.w        	$t0, $fp, -20	
  12c:	001c318c 	mul.w       	$t0, $t0, $t0
  130:	0048fd8c 	srai.w      	$t0, $t0, 0x1f

假设如果没有50128处,$t0的保存和加载,试想一下:

在调用foo_func的时候,也会用到$t0寄存器,这个时候由于调用者

没有保存$t0,导致在被调用者f00_func中修改了$t0,这样会导致错误的结果。

Tip

尤其在手动书写汇编的时候,调用者保存临时寄存器$t0-$t8

有一种情况是不用保存的,就是在函数调用之前,临时寄存器$t0-$t8的生命周期已经结束,

此时我们就不用保存$t0了,因为当前函数中不会在使用到它了。

Warning

寄存器$t0-$t8之所以称为是临时寄存器,就是在于,在函数内部可以随时使用。

但是,当使用的临时寄存器生命周期跨越函数调用时,就必须得自行在栈帧中分配空间,

保存临时寄存器,以保证程序的正确运行。

  • 调用静态寄存器s0-s8时,调用者必须先保存原有的寄存器的值,然后再随意使用。 在使用的时候保存,函数返回的时候恢复。

使用规则:静态寄存器在函数中如果想要使用,必须先在栈中分配空间,然后在进入函数的时候先保存, 最后再使用其寄存器。

_out_rev():
  c:	02fe8063 	addi.d      	$sp, $sp, -96
  10:	29c14076 	st.d        	$fp, $sp, 80
  
  // 进入函数后,首先保存s0-s8,因为后面要使用
  14:	29c12077 	st.d        	$s0, $sp, 72
  18:	29c0e079 	st.d        	$s2, $sp, 56
  1c:	29c0c07a 	st.d        	$s3, $sp, 48
  20:	29c0a07b 	st.d        	$s4, $sp, 40
  24:	29c0807c 	st.d        	$s5, $sp, 32
  28:	29c0607d 	st.d        	$s6, $sp, 24
  2c:	29c0407e 	st.d        	$s7, $sp, 16
  30:	29c0207f 	st.d        	$s8, $sp, 8
  34:	29c16061 	st.d        	$ra, $sp, 88
  38:	29c10078 	st.d        	$s1, $sp, 64
  
  // 此处使用静态寄存器s0-s8
  40:	001500da 	move        	$s3, $a2
  44:	001500bb 	move        	$s4, $a1
  48:	00150099 	move        	$s2, $a0
  4c:	001500fc 	move        	$s5, $a3
  50:	0015011d 	move        	$s6, $a4
  54:	00150137 	move        	$s0, $a5
  58:	00150156 	move        	$fp, $a6
  5c:	0340097e 	andi        	$s7, $a7, 0x2
  60:	001500df 	move        	$s8, $a2

  // 此处进入另一个函数 
  c0:	4c000321 	jirl        	$ra, $s2, 0
  
  // 函数返回后直接使用s0-s8
  c8:	02c0075a 	addi.d      	$s3, $s3, 1
  cc:	02c00718 	addi.d      	$s1, $s1, 1

  // 退出函数时,恢复原来的s0-s8
  e4:	28c12077 	ld.d        	$s0, $sp, 72
  e8:	28c0e079 	ld.d        	$s2, $sp, 56
  ec:	28c0c07a 	ld.d        	$s3, $sp, 48
  f0:	28c0a07b 	ld.d        	$s4, $sp, 40
  f4:	28c0807c 	ld.d        	$s5, $sp, 32
  f8:	28c0607d 	ld.d        	$s6, $sp, 24
  fc:	28c0407e 	ld.d        	$s7, $sp, 16
 100:	28c0207f 	ld.d        	$s8, $sp, 8
 104:	00150304 	move        	$a0, $s1
 108:	28c10078 	ld.d        	$s1, $sp, 64
 10c:	02c18063 	addi.d      	$sp, $sp, 96
 110:	4c000020 	ret

如果当前函数中想要使用s0-s8,首先需要保存之前函数的静态寄存器。

静态寄存器不像临时寄存器那样,进入函数之前需要保存。

s0-s8不管进入函数还是退出函数,都可以随意使用,并不需要进入函数之前保存, 函数调用完恢复,这里和临时寄存器真好相反。

在退出当前函数时,需要恢复静态寄存器到原来的值。

Tip

尤其在手动书写汇编的时候,如果想要使用s0-s8的时候,首先保存。

也就是说,静态寄存器s0-s8在进入函数和退出函数的时候需要保持一致。

换句话说,静态寄存器需要被调用者保存和回复,调用函数时,前后是一样的。

  • 整数和浮点数混合出现

如下所示,整形参数和浮点参数交叉出现。

extern int foo(int a, double b, int c, 
	           double d, int e, double f, int g, 
	           int h, int i, double j); 

extern int foo1(int a, double b, int c, 
	           double d, int e, double f, int g, 
	           int h, int i, double j); 
int func(int arg0, int arg1) {
	int res = 0;
	int a = 1, c = 3;
	double  b = 2.0, d = 4.0, f = 6.0, j = 10.0;
	int e = 5, g = 7, h = 8, i = 9;

	res =  foo(a, b, c, d, e, f, g, h, i, j);

	res += foo1(a+arg0, b+arg1, c, d, e, f, g, h, i, j);
    return res;
}

其反汇编如下所示:

func():
  
  // 分配栈空间,并且更新sp寄存器 
  c:	02ff4063 	addi.d      	$sp, $sp, -48
  
  // 保存ra, fp, s0-s2寄存器
  10:	29c0a061 	st.d        	$ra, $sp, 40
  14:	29c08076 	st.d        	$fp, $sp, 32
  18:	29c06077 	st.d        	$s0, $sp, 24
  1c:	29c04078 	st.d        	$s1, $sp, 16
  20:	29c02079 	st.d        	$s2, $sp, 8
  
  // 设置fp指向栈顶位置
  24:	02c0c076 	addi.d      	$fp, $sp, 48
  
  // 此处的是加载b,d,f,j四个浮点数。
  // 对于浮点,将其保存在rodata段中,通过浮点fld指令加载到寄存器中
  28:	1a00000f 	pcalau12i   	$t3, 0
  2c:	1a00000e 	pcalau12i   	$t2, 0
  30:	1a00000d 	pcalau12i   	$t1, 0
  34:	1a00000c 	pcalau12i   	$t0, 0
  38:	2b8001e3 	fld.d       	$fa3, $t3, 0
  3c:	2b8001c2 	fld.d       	$fa2, $t2, 0
  40:	2b8001a1 	fld.d       	$fa1, $t1, 0
  44:	2b800180 	fld.d       	$fa0, $t0, 0
  
  // 加载整形参数到a0-a5中
  50:	02802409 	li.w        	$a5, 9
  54:	02802008 	li.w        	$a4, 8
  58:	02801c07 	li.w        	$a3, 7
  5c:	02801406 	li.w        	$a2, 5
  60:	02800c05 	li.w        	$a1, 3
  64:	02800404 	li.w        	$a0, 1
  
  // 调用函数foo
  68:	54000000 	bl          	0	# foo

  // 加载浮点数 j
  6c:	1a00000c 	pcalau12i   	$t0, 0
  70:	02c0018c 	addi.d      	$t0, $t0, 0
  74:	2b800183 	fld.d       	$fa3, $t0, 0
  
  // 加载浮点数 f
  78:	1a00000c 	pcalau12i   	$t0, 0
  7c:	02c0018c 	addi.d      	$t0, $t0, 0
  80:	0114ab20 	movgr2fr.d  	$fa0, $s2
  84:	2b800182 	fld.d       	$fa2, $t0, 0
  
  // 加载浮点数 d
  88:	1a00000c 	pcalau12i   	$t0, 0
  8c:	02c0018c 	addi.d      	$t0, $t0, 0
  90:	2b800181 	fld.d       	$fa1, $t0, 0
  
  // 加载浮点数 b
  94:	1a00000c 	pcalau12i   	$t0, 0
  98:	02c0018c 	addi.d      	$t0, $t0, 0
  9c:	011d2004 	ffint.d.w   	$fa4, $fa0
  a0:	2b800180 	fld.d       	$fa0, $t0, 0
  
  a4:	00150098 	move        	$s1, $a0
  a8:	02802409 	li.w        	$a5, 9
  ac:	01010080 	fadd.d      	$fa0, $fa4, $fa0
  b0:	028006e4 	addi.w      	$a0, $s0, 1
  
  // 加载整形参数
  b4:	02802008 	li.w        	$a4, 8
  b8:	02801c07 	li.w        	$a3, 7
  bc:	02801406 	li.w        	$a2, 5
  c0:	02800c05 	li.w        	$a1, 3
  
  // 调用函数foo1
  c4:	54000000 	bl          	0	# foo1
  
  // 恢复相应的寄存器的值
  c8:	28c0a061 	ld.d        	$ra, $sp, 40
  cc:	28c08076 	ld.d        	$fp, $sp, 32
  d0:	28c06077 	ld.d        	$s0, $sp, 24
  d4:	28c02079 	ld.d        	$s2, $sp, 8
  d8:	00101304 	add.w       	$a0, $s1, $a0
  dc:	28c04078 	ld.d        	$s1, $sp, 16
  e0:	02c0c063 	addi.d      	$sp, $sp, 48
  e4:	4c000020 	ret

可以看出一下:

  • 整形参数加载在a0-a7,如果超过的话,在栈中保存

  • 浮点参数加载在fa0-fa7中,如果超过的话,也在栈中保存

  • 出现混合型参数时,从左往右,整形的按照顺序保存在a0-a7

  • 出现混合型参数时,从左往右,浮点的按照顺序保存在fa0-fa7

  • 出现混合型参数时,整形在a0-a7中,浮点在fa0-fa7中,不能将整形的放到浮点寄存器,
    相反也不能将浮点参数放到整形寄存器中。

7.5. 函数调用规约

我们这里介绍LoongArch32常用的ILP32 ABI中的函数调用约定。其实读者了解其中第1、2、10条就能满足大多数场景下的汇编开发需求。

  1. 基本整型调用规范提供了8个参数寄存器$a0 ~ $a7用于参数传递,
    前两个参数寄存器$a0 ~ $a1也用于返回值。

  2. 如下所述:

  • 若一个标量位宽至多32位,则它在单个参数寄存器中传递,若没有可用的寄存器,则在堆栈中传递;

  • 若一个标量宽度超过32位,不超过64位,则可以在一对参数寄存器中传递,低32位在小编号寄存器中,

    高32位在大编号寄存器中;

    若没有可用的参数寄存器,则在堆栈上传递标量;

    若只有一个寄存器可用,则低32位在寄存器中传递,高32位在堆栈中传递。

  • 若一个标量宽度大于64位,则通过引用传递,并在参数列表中用地址替换。传递到堆栈上的标量会对齐
    到类型对齐(Type Alignment)和32中的较大者,但不会超过堆栈对齐。

  • 当整型参数传入寄存器或堆栈时,小于32位的整型标量根据其类型的符号扩展至32位。

  • 当浮点型参数传入寄存器或堆栈时,比32位窄的浮点类型将被扩展为32位。

  1. 如下所述:

  • 若一个聚合体的的位宽不超过32位,则这个聚合体可以在寄存器中传递,并且这个聚合体
    在寄存器中的字段布局同它在内存中的字段布局保持一致;
    若没有可用的寄存器,则在堆栈上传递聚合体;

  • 若一个聚合体的位宽超过32位,不超过64位,则可以在一对寄存器中传递,若只有一个寄存器可用,
    则聚合体的前半部分在寄存器中传递,后半部分在堆栈中传递;
    若没有可用的寄存器,则在堆栈上传递聚合体。由于填充(padding)而未使用的位,以及从聚合体的
    末尾至下一个对齐位置之间的位,都是未定义的;

  • 若一个聚合体位宽大于64位,则它通过引用传递,并在参数列表中被替换为地址。
    传递到堆栈上的聚合体会对齐到类型对齐(type alignment)和32中的较大者,但不会超过堆栈对齐。

  1. 对于空的结构体或联合体(unions)参数或返回值,C编译器会认为它们是非标准扩展并忽略;

    C++编译器则不是这样,C++编译器要求它们必须是分配了大小的类型(sized types)。

  2. 位域(bitfields)以小端顺序排列。跨越其整型类型的对齐边界的位域将从下一个对齐边界开始。例如:

    • struct {int x:10; int y:12;}是一个32位类型,x 为 9-0 位,y 为 21-10 位,31-22 位未定义。

    • struct {short x:10; short y:12;}是一个 32 位类型,x 为 9-0 位,y 为 27-16 位,31-28 位和 15-10位未定义。

  3. 通过引用传递的实参可以由被调用方修改。

  4. 浮点实数的传递方式与相同大小的聚合体相同,浮点型复数的传递方式与包含两个浮点实数的结构体相同。

  5. 在基本整型调用规范中,可变参数的传递方式与命名参数相同,但有一个例外。

    64位对齐的可变参数和至多64位大小的可变参数通过一对对齐的寄存器传递(例如:寄存器对中的第一个寄存器为偶数),

    如果没有可用的寄存器,则在堆栈上传递。当可变参数在堆栈上被传递后,

    所有之后的参数也将在堆栈上被传递(例如,最后一个参数寄存器可能由于对齐寄存器对规则而未被使用)。

  6. 返回值的传递方式与第一个同类型命名参数(named value)的传递方式相同。

    如果这样的实参是通过引用传递的,则调用者为返回值分配内存,并将地址作为隐式的第一个参数传递。

  7. 堆栈向下增长(朝向更低的地址),堆栈指针应该对齐到一个16字节的边界上作为函数入口。

    在堆栈上传递的第一个实参位于函数入口的堆栈指针偏移量为零的地方;后面的参数存储在更高的地址中。

  8. 在标准 ABI 中,堆栈指针在整个函数执行过程中必须保持对齐。非标准 ABI 代码必须在调用标准 ABI

    过程之前重新调整堆栈指针。操作系统在调用信号处理程序之前必须重新调整堆栈指针;

    因此,POSIX 信号处理程序不需要重新调整堆栈指针。在服务中断的系统中使用被中断对象的堆栈,

    如果连接到任何使用非标准堆栈对齐规则的代码,中断服务例程必须重新调整堆栈指针,

    但如果所有代码都遵循标准 ABI ,则不需要重新调整堆栈指针。

  9. 函数所依赖的数据必须位于函数栈帧范围之内。

  10. 被调用函数应该负责保证寄存器$s0 ~ $s8的值在返回时和调用入口处一致。

上面的规范描述提到的“堆栈对齐”的概念就是其中的第10条。

上面的规范描述还提到了“类型对齐”。举例来说,标量int型的类型对齐意味着它的访存地址必须是4的倍数。

下面是所有标量类型的对齐情况:

标量类型 大小(字节) 对齐(字节)
unsinged/signed char 1 1
unsinged/signed short 2 2
unsinged/signed int 4 4
unsinged/signed long 4 4
unsinged/signed long long 8 8
pointer 4 4
float 4 4
double 8 8
long double 16 16