RISC-V 入门 Part2: ABI && Calling Convention
长度单位:
i
的尾坠代表 imm
, 立即数, 比如 addi
, andi
jump
基于jalr
,这个 r
是 register。
u类指令格式:
lui
: load upper immediate, 用于构造一个 32-bit constants
auipc
: add upper immediate to PC. PC + 偏移量写入 register
我们上一个 Part 介绍了 bne 等 conditional branch,此外还有难理解一些的 unconditional branch. 例如伪指令 j
, 它基于 jal
, 即 jump and link 实现:
1 2
| jal rd offset jalr rd rs (offset)
|
jal 用来实现函数调用的语义:它 PC 跳转 offset
(长度为 20bits),或者对应的寄存器,然后把 PC + 4 (RV32I 指令长度是 32bit, 即 4Byte,表示下一条指令)写到 rd
寄存器。
jal
用来实现函数调用和循环中的 unconditional jump.
C to RISC-V
上面这张图 cmu 15-445 有更好玩的版本。
这里面写了调用一个函数的6步,即函数调用规范(Calling convention) .
- 把函数参数放到函数能访问的地方
- 把控制权给函数(使用
jal
指令)
- 拿到 memory 中的资源 (获取函数需要的局部存储资源,按需保存寄存器)
- 运行函数中的指令
- 把值写到 memory/register 中 (将返回值存储到调用者能够访问到的位置,恢复寄存器,释放局部存储资源)
- 返回 (
ret
指令)
寄存器有 caller-saved 和 callee-saved 两种:
- caller-saved: callee 可以随便搞,从这里读数据,然后操作它们
- callee-saved: callee 在返回前应该保存的
ABI:调用其它函数时,关于汇编、参数、寄存器等的双方约定。(我觉得有几个答案补充的很好). 实际上 ABI 兼容性是一个和编译器等都有关的话题。ABI 定义了 calling convention,同时 ABI 定义了约束:有些寄存器是不可写的。
同时,你可以在上面的图里面看到很奇妙的事情,这里没有再使用 x0
- x15
这样的记号,而是用了 s0
fp
这样相对来说名字好理解一些的。
1 2 3 4 5
| int Leaf(int g, int h, int i, int j) { int f; f = (g + h) - (i + j); return f; }
|
用 riscv64-unknown-elf-gcc
编译
可以看到,为了存放旧的值,需要 stack.sp
寄存器和 stack 有关,同时有 push/pop. 鉴于 stack 是自顶向下生长的,push 会减小 sp
, pop 会增大 sp
1
| ✗ riscv64-unknown-elf-gcc -march=rv32imac -mabi=ilp32 -S leaf.c
|
编译一下 leaf:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| .file "leaf.c" .option nopic .attribute arch, "rv32i2p0_m2p0_a2p0_c2p0" .attribute unaligned_access, 0 .attribute stack_align, 16 .text .align 1 .globl Leaf .type Leaf, @function Leaf: addi sp,sp,-48 # 修改 stack, 降低以 push 值 sw s0,44(sp) # 把 s0 写进 44(sp), s0 这里代表 frame pointer addi s0,sp,48 # s0 = sp + 48, s0 为 frame pointer sw a0,-36(s0) # 把 a0 - a3 存了。a0-a1 用来存返回值,a0-a7 用来传参 sw a1,-40(s0) sw a2,-44(s0) sw a3,-48(s0)
lw a4,-36(s0) # 把原本 a0 a1 加载到 a4 a5 lw a5,-40(s0) add a4,a4,a5 # a4 = a4 + a5 lw a3,-44(s0) # a3, a5 加载 lw a5,-48(s0) add a5,a3,a5 # a5 = a3 + a5 sub a5,a4,a5 # a5 = a4 - a5 这两段完成函数主要的计算 sw a5,-20(s0) lw a5,-20(s0) mv a0,a5 # a0 = a5, a0 是返回值 lw s0,44(sp) # s0 = sp + 44 addi sp,sp,48 # 修改 sp jr ra # ra 是 return address, 返回 ra .size Leaf, .-Leaf .ident "GCC: (GNU) 9.2.0"
|
- 改变
s0
这个 frame pointer 和 sp
这个 stack pointer
- 把原来的
a0 - a4
放到栈上
- 用
a4 a5
来运算
- 改回
sp, s0
jr
返回
跳转回来的伪指令如下:
顺便,-O2
编译的时候:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| .file "leaf.c" .option nopic .attribute arch, "rv32i2p0_m2p0_a2p0_c2p0" .attribute unaligned_access, 0 .attribute stack_align, 16 .text .align 1 .globl Leaf .type Leaf, @function Leaf: add a0,a0,a1 add a2,a2,a3 sub a0,a0,a2 ret .size Leaf, .-Leaf .ident "GCC: (GNU) 9.2.0"
|
这里 a0, a1, a2, a3
四个是参数。
下面:
1 2 3 4 5
| int mult(int, int);
int sumSquare(int x, int y) { return mult(x, x) + y; }
|
生成汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| .file "ss.c" .option nopic .attribute arch, "rv32i2p0_m2p0_a2p0_c2p0" .attribute unaligned_access, 0 .attribute stack_align, 16 .text .align 1 .globl sumSquare .type sumSquare, @function sumSquare: addi sp,sp,-16 # reserve space on stack sw ra,12(sp) # save ret addr sw s0,8(sp) # 存储原来的 s0 mv s0,a1 # s0 存储 y mv a1,a0 # a1 = a0 (= x) call mult add a0,a0,s0 lw ra,12(sp) lw s0,8(sp) addi sp,sp,16 jr ra .size sumSquare, .-sumSquare .ident "GCC: (GNU) 9.2.0"
|
Stack Pointer & Frame Pointer & Memory
当然,以上演示的很多都在 stack 上,实际上我们可能需要打理的东西还更多:
堆/栈的分配是 ISA 的一部分(指令集同样是 ISA 的一部分)
以上是对 stack 的操作,在 x86 里面我们有原子的 push-pop, 但是这里我们得谨慎的多。
看前面那个 s0 的例子,用 frame pointer(s0
) 而不是 sp 取地址相对值. 同时我们还有 fp
, 即 frame pointer, 它中文叫“帧指针”。
The calling convention says it doesn’t matter if you use a frame pointer or not!
It is just a callee saved register, so if you use it as a frame pointer…
It will be preserved just like any other saved register.
But if you just use it as s0, that makes no difference!
栈帧内返回地址是在local variables前还是在它们后面? - RednaxelaFX的回答 - 知乎 https://www.zhihu.com/question/33920941/answer/57597076
实际上 fp sp 关系类似fp
— sp
, 同时 fp
不是必须的,但是对 debug 而言大有裨益。
接着举例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <stdlib.h>
typedef struct list { void *car; struct list *cdr; } List;
List *map(List *src, void *(*f)(void *)) { List *ret; if (!src) return 0; ret = (List *)malloc(sizeof(List)); ret->car = (*f)(src->car); ret->car = map(src->cdr, f); return ret; }
|
编译一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| .file "rich-list.c" .option nopic .attribute arch, "rv32i2p0_m2p0_a2p0_c2p0" .attribute unaligned_access, 0 .attribute stack_align, 16 .text .align 1 .globl map .type map, @function map: addi sp,sp,-16 sw ra,12(sp) sw s0,8(sp) # 存储 s0-s2 sw s1,4(sp) sw s2,0(sp) beq a0,zero,.L3 # is-null, a0 是 src mv s0,a0 # save src li a0,8 # a0 = 8, call malloc with size 8 mv s2,a1 # s2 = a1 call malloc mv s1,a0 lw a0,0(s0) jalr s2 # jalr 调用函数,a1 是一个 function mv a5,a0 lw a0,4(s0) sw a5,0(s1) mv a1,s2 call map lw ra,12(sp) lw s0,8(sp) sw a0,0(s1) lw s2,0(sp) mv a0,s1 lw s1,4(sp) addi sp,sp,16 jr ra .L3: # is-null, 直接返回了 lw ra,12(sp) lw s0,8(sp) li s1,0 # li rd, imm 读取立即数,这里把 s1, 即返回值,置为0 lw s2,0(sp) mv a0,s1 # a0 = 0 lw s1,4(sp) addi sp,sp,16 jr ra .size map, .-map .ident "GCC: (GNU) 9.2.0"
|
这里的 requirements 是:我们会调用 malloc
, 并在之后使用 src
和 f
参数