RISC-V ABI约定 所有数据保持自然对齐。
ILP32,LP64
| C type | Description | Bytes in RV32 | Bytes in RV64 |
|---|---|---|---|
| char/unsigned char | 8-bit unsigned integer,zero-extended | 1 | 1 |
| signed char | 8-bit signed integer,sign-extended | ||
| short | 16-bit signed integer,sign-extended | 2 | 2 |
| unsigned short | 16-bit unsigned integer,zeroextended | ||
| int | int都是32位 | 4 | 4 |
| long | 指针和long和整数寄存器一样宽 | 4 | 8 |
| long long | long long都是64位 | 8 | 8 |
| void * | 指针和long和整数寄存器一样宽 | 4 | 8 |
| float | 32-bit IEEE 754-2008 | 4 | 4 |
| double | 64-bit IEEE 754-2008 | 8 | 8 |
| long double | 128-bit IEEE floating-point | ||
| IEEE floating-point | 16 | 16 |
在RV64中,32位类型不管是int还是unsigned都是符号扩展到64位。
a0-a7,fa0-fa7:用于函数传递参数,其中0-1用于返回值,a表示arguments。都是调用者负责保存,因为是传参肯定是在函数调用前就要准备好,所部不可能是被调用者去负责保存。
pointer-words的影子shadow。如果是小于8个的浮点值,则使用fai传递;小于8个的整数则使用ai传递。
如果浮点参数是联合体unions的字段,或者结构体的数组字段,则使用整数寄存器传递。
另外可变参数函数中除了显示指定的参数外的参数,如果是浮点数也是使用整数寄存器传递。
pointer-word的参数使用低位传递,子指针字sub-pointer-word的参数通过栈传递时,使用指针字pointer-word的低地址,因为RISC-V是小端的存储系统。pointer-word时通过栈传递,使用自然对齐。当它们使用整数寄存器传递时,使用对齐的偶-奇寄存器对,偶寄存器存低位。比如RV32的void foo(int, long long)使用a0传递第一个参数,a2-a3传递第二个参数,因为由偶寄存器对齐,且a2存低位,返回值通过a0传递。pointer-word的参数通过引用传递。sp指向第一个未使用整数寄存器传递的参数。a0,a1,fa0,fa1用于函数返回值。只有结构体成员只有一个或者两个浮点成员,或者primitives时才使用浮点寄存器返回;其他的由a0-a1组成的两倍指针字的two pointer-words大小返回;更大的返回值通过内存传递;调用者负责分配这个内存,并传递指向该内存的指针,隐含的作为第一个参数传递给被调用者。RISC-V调用中,栈向下生长,并且保持16字节对齐。t0-t6,12个临时浮点寄存器ft0-ft11在调用过程是可变的,如果后面需要使用则必须由调用者负责保存。其中t表示Temporaries。s0-s11,12个浮点寄存器fs0-fs11在调用过程是必须保持的,所以如果被调用者需要使用则必须由被调用者保存。实际上上面的8和9.,t和s寄存器的可变volatile和保持preserved是对被调用者来说的,也就是对被调用者申明,告诉被调用者,
t这些寄存器是可变的,那么被调用者可以随便使用,此时调用者则必须考虑被被调用者随便使用而修改,需要调用者保存;
s这些寄存器是保持的,那么被调用者不能随便使用,如果要用就要负责保存。
所以对于a寄存器也可以这样理解,因为a寄存器用于传递参数,所以是被调用者随便使用的,即不保持的,所以需要调用者负责保存,并赋参数值。
| Register | ABI Name | Description | Saver |
|---|---|---|---|
| x0 | zero | 硬件固定为0 | / |
| x1 | ra | 返回地址 | Caller调用者 |
| x2 | sp | 栈指针 | Callee被调用者 |
| x3 | gp | 全局指针 | / |
| x4 | tp | 线程指针 | / |
| x5-x7 | t0-t2 | 临时使用 | Caller调用者 |
| x8 | s0/fp | 保存寄存器/帧指针 | Callee被调用者 |
| x9 | s1 | 保存寄存器 | Callee被调用者 |
| x10-x11 | a0-a1 | 函数参数/返回值 | Caller调用者 |
| x12-x17 | a2-a7 | 函数参数 | Caller调用者 |
| x18-x27 | s2-s11 | 保存寄存器 | Callee被调用者 |
| x28-x31 | t3-t6 | 临时使用 | Caller调用者 |
| f0-f7 | ft0-ft7 | FP临时使用 | Caller调用者 |
| f8-f9 | fs0-fs1 | FP保存寄存器 | Callee被调用者 |
| f10-f11 | fa0-fa1 | FP函数参数/返回值 | Caller调用者 |
| f12-f17 | fa2-fa7 | FP参数 | Caller调用者 |
| f18-f27 | fs2-fs11 | FP保存寄存器 | Callee被调用者 |
| f28-f31 | ft8-ft11 | FP临时使用 | Caller调用者 |
在没有浮点硬件,或者不使用F,D,Q扩展的硬件浮点,不使用浮点寄存器,完全由软件实现浮点。
整数参数的传入和返回值和RVG一样。
浮点参数和返回值,通过整数寄存器传递,原则是使用大小相同的整数寄存器传递。
比如RV32的
double foo(int, double, long double)
则第一个参数通过a0传递;
第二个参数通过a2和a3传递;
第三个参数通过a4传引用传递;
结果通过a0和a1传递。
如果是RV64则
则第一个参数通过a0传递;
第二个参数通过a1传递;
第三个参数通过a2-a3传递;
结果通过a0传递。
动态舍入模式和产生的异常标志通过C99的fenv.h提供的接口访问。
从以下几个部分去理解
理解函数参数的传递与返回值,a0-a1,a2-a7,fa0-fa1,fa2-fa7,0-1用于返回值。
理解ra寄存器,函数的返回地址
理解SP栈指针,理解栈的向下生长,理解进入子函数时减少sp分配空间,分配的空间用于存储s寄存器和局部变量使用,和退出子函数时增加sp恢复sp。也就是调用完子函数返回后sp要保持不变。
理解t0-t6,ft0-ft11;s0-s11.fs0-fs11,这里重点站在被调用者角度去理解可变和保持,进而理解谁负责保存寄存器。
jal ra label或者jal ra rd imm简化为伪指令jal label或者jalr rd(立即数为0)。jal跳转即将PC + 4存储到ra寄存器,即函数返回后的下一条执行的指令。jalr类似只是设置PC为rd + imm。
注意与无条件跳转jal x0 label和jalr x0 rd imm,伪指令j label ,jr rd(立即数为0)的区别,无条件跳转是不返回了的所以不保存返回地址到ra,而是保存到了x0寄存器,而x0寄存器是硬件固定为0的,所以相当于不保存,
两者指令是统一的,这也体现了RISC-V指令设计的简洁统一的美学。
其中jal的l可以理解为link,类似于ARM的LR寄存器的L。
除非使用栈传递参数,否则子函数返回后sp必须保持不变。
所有的s寄存器在子函数返回后必须保持,这也是其保持的含义,也是为什么被调用者需要负责保存。
子函数退出时返回ra处执行
函数进入时的处理:减少sp,s寄存器个数和局部变量大小的空间,存储使用到的s寄存器到栈中。如果还有子函数调用则存储ra到栈中(因为子函数的子函数的返回值要存到ra会覆盖ra)。
函数退出时的处理:恢复栈中保存的s寄存器,更新sp值。如果有需要恢复ra值,恢复sp值到函数进入之前的值,返回到ra处执行。
最好通过编写c代码,使用编译工具生成汇编代码,对照c和汇编代码的方式去理解。
五.参考
riscv-calling.pdf [Volume I: RISC-V User-Level ISA V2.1draft:Chapter 18Calling Convention]
Understanding RISC-V Calling Convention.pdf [Nick Riasanovsky]
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !