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
假设如果没有50和128处,$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条就能满足大多数场景下的汇编开发需求。
基本整型调用规范提供了8个参数寄存器
$a0 ~ $a7用于参数传递,
前两个参数寄存器$a0 ~ $a1也用于返回值。如下所述:
若一个标量位宽至多32位,则它在单个参数寄存器中传递,若没有可用的寄存器,则在堆栈中传递;
若一个标量宽度超过32位,不超过64位,则可以在一对参数寄存器中传递,低32位在小编号寄存器中,
高32位在大编号寄存器中;
若没有可用的参数寄存器,则在堆栈上传递标量;
若只有一个寄存器可用,则低32位在寄存器中传递,高32位在堆栈中传递。
若一个标量宽度大于64位,则通过引用传递,并在参数列表中用地址替换。传递到堆栈上的标量会对齐
到类型对齐(Type Alignment)和32中的较大者,但不会超过堆栈对齐。当整型参数传入寄存器或堆栈时,小于32位的整型标量根据其类型的符号扩展至32位。
当浮点型参数传入寄存器或堆栈时,比32位窄的浮点类型将被扩展为32位。
如下所述:
若一个聚合体的的位宽不超过32位,则这个聚合体可以在寄存器中传递,并且这个聚合体
在寄存器中的字段布局同它在内存中的字段布局保持一致;
若没有可用的寄存器,则在堆栈上传递聚合体;若一个聚合体的位宽超过32位,不超过64位,则可以在一对寄存器中传递,若只有一个寄存器可用,
则聚合体的前半部分在寄存器中传递,后半部分在堆栈中传递;
若没有可用的寄存器,则在堆栈上传递聚合体。由于填充(padding)而未使用的位,以及从聚合体的
末尾至下一个对齐位置之间的位,都是未定义的;若一个聚合体位宽大于64位,则它通过引用传递,并在参数列表中被替换为地址。
传递到堆栈上的聚合体会对齐到类型对齐(type alignment)和32中的较大者,但不会超过堆栈对齐。
对于空的结构体或联合体(unions)参数或返回值,C编译器会认为它们是非标准扩展并忽略;
C++编译器则不是这样,C++编译器要求它们必须是分配了大小的类型(sized types)。
位域(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位未定义。
通过引用传递的实参可以由被调用方修改。
浮点实数的传递方式与相同大小的聚合体相同,浮点型复数的传递方式与包含两个浮点实数的结构体相同。
在基本整型调用规范中,可变参数的传递方式与命名参数相同,但有一个例外。
64位对齐的可变参数和至多64位大小的可变参数通过一对对齐的寄存器传递(例如:寄存器对中的第一个寄存器为偶数),
如果没有可用的寄存器,则在堆栈上传递。当可变参数在堆栈上被传递后,
所有之后的参数也将在堆栈上被传递(例如,最后一个参数寄存器可能由于对齐寄存器对规则而未被使用)。
返回值的传递方式与第一个同类型命名参数(named value)的传递方式相同。
如果这样的实参是通过引用传递的,则调用者为返回值分配内存,并将地址作为隐式的第一个参数传递。
堆栈向下增长(朝向更低的地址),堆栈指针应该对齐到一个16字节的边界上作为函数入口。
在堆栈上传递的第一个实参位于函数入口的堆栈指针偏移量为零的地方;后面的参数存储在更高的地址中。
在标准 ABI 中,堆栈指针在整个函数执行过程中必须保持对齐。非标准 ABI 代码必须在调用标准 ABI
过程之前重新调整堆栈指针。操作系统在调用信号处理程序之前必须重新调整堆栈指针;
因此,POSIX 信号处理程序不需要重新调整堆栈指针。在服务中断的系统中使用被中断对象的堆栈,
如果连接到任何使用非标准堆栈对齐规则的代码,中断服务例程必须重新调整堆栈指针,
但如果所有代码都遵循标准 ABI ,则不需要重新调整堆栈指针。
函数所依赖的数据必须位于函数栈帧范围之内。
被调用函数应该负责保证寄存器
$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 |