有人预言,RISC-V或将是继Intel和Arm之后的第三大主流处理器体系。欢迎访问全球首家只专注于RISC-V单片机行业应用的中文网站
您需要 登录 才可以下载或查看,没有帐号?立即注册
x
本帖最后由 皋陶 于 2020-10-20 21:36 编辑
长度单位:
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 实现:
- 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 这样相对来说名字好理解一些的。
- 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 - ✗ riscv64-unknown-elf-gcc -march=rv32imac -mabi=ilp32 -S leaf.c
复制代码
编译一下 leaf:
- .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 编译的时候: - .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 四个是参数。
下面: - int mult(int, int);
- int sumSquare(int x, int y) {
- return mult(x, x) + y;
- }
复制代码
生成汇编: - .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!
实际上 fp sp 关系类似fp -- sp , 同时 fp 不是必须的,但是对 debug 而言大有裨益。
接着举例子: - #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;
- }
复制代码
编译一下: - .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 参数
完
|