打印
[复制链接]
1233|1
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
elephant00|  楼主 | 2023-5-9 10:26 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式


  • 一.C数据类型和对齐
  • 二.RVG调用约定
  • 三.Soft-Float调用约定
  • 四.总结
  • 使用编译器生成汇编代码分析调用过程
  • 五.参考
一.C数据类型和对齐
所有数据保持自然对齐。
ILP32,LP64
C typeDescriptionBytes in RV32Bytes in RV64
char/unsigned char8-bit unsigned integer,zero-extended11
signed char8-bit signed integer,sign-extended

short16-bit signed integer,sign-extended22
unsigned short16-bit unsigned integer,zeroextended

intint都是32位44
long指针和long和整数寄存器一样宽48
long longlong long都是64位88
void *指针和long和整数寄存器一样宽48
float32-bit IEEE 754-200844
double64-bit IEEE 754-200888
long double128-bit IEEE floating-point

IEEE floating-point1616
在RV64中,32位类型不管是int还是unsigned都是符号扩展到64位。
二.RVG调用约定

  • a0-a7,fa0-fa7:用于函数传递参数,其中0-1用于返回值,a表示arguments。
都是调用者负责保存,因为是传参肯定是在函数调用前就要准备好,所部不可能是被调用者去负责保存。

  • 如果函数参数为结构体的字段,每一个都是指针对齐的,则参数寄存器是结构体前面8个指针字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字节对齐。
  • 7个临时整数寄存器t0-t6,12个临时浮点寄存器ft0-ft11在调用过程是可变的,如果后面需要使用则必须由调用者负责保存。其中t表示Temporaries。

  • [ ] 这里有点疑惑,临时寄存器是被调用者使用的,只有被调用者才知道自己要用哪些寄存器,为什么不是被调用者负责保存?
    这样理解,因为这些寄存器是可变的,对于被调用者来说既然是可变的则可以随便使用,也就是可能被被调用者修改,所以对于调用者来说,如果这些寄存器的值不能被破坏则自己需要负责保存。

  • 12个整数寄存器s0-s11,12个浮点寄存器fs0-fs11在调用过程是必须保持的,所以如果被调用者需要使用则必须由被调用者保存。

使用特权

评论回复

相关帖子

沙发
elephant00|  楼主 | 2023-5-9 10:26 | 只看该作者
实际上上面的8和9.,t和s寄存器的可变volatile和保持preserved是对被调用者来说的,也就是对被调用者申明,告诉被调用者,
t这些寄存器是可变的,那么被调用者可以随便使用,此时调用者则必须考虑被被调用者随便使用而修改,需要调用者保存;
s这些寄存器是保持的,那么被调用者不能随便使用,如果要用就要负责保存。
所以对于a寄存器也可以这样理解,因为a寄存器用于传递参数,所以是被调用者随便使用的,即不保持的,所以需要调用者负责保存,并赋参数值。
RegisterABI NameDescriptionSaver
x0zero硬件固定为0/
x1ra返回地址Caller调用者
x2sp栈指针Callee被调用者
x3gp全局指针/
x4tp线程指针/
x5-x7t0-t2临时使用Caller调用者
x8s0/fp保存寄存器/帧指针Callee被调用者
x9s1保存寄存器Callee被调用者
x10-x11a0-a1函数参数/返回值Caller调用者
x12-x17a2-a7函数参数Caller调用者
x18-x27s2-s11保存寄存器Callee被调用者
x28-x31t3-t6临时使用Caller调用者
f0-f7ft0-ft7FP临时使用Caller调用者
f8-f9fs0-fs1FP保存寄存器Callee被调用者
f10-f11fa0-fa1FP函数参数/返回值Caller调用者
f12-f17fa2-fa7FP参数Caller调用者
f18-f27fs2-fs11FP保存寄存器Callee被调用者
f28-f31ft8-ft11FP临时使用Caller调用者
三.Soft-Float调用约定
在没有浮点硬件,或者不使用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]

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

983

主题

3060

帖子

7

粉丝