皋陶 发表于 2020-10-20 21:36:24

RISC-V 入门 Part2: ABI && Calling Convention

本帖最后由 皋陶 于 2020-10-20 21:36 编辑

长度单位:
[*]b byte
[*]h halfword
[*]w word

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!
栈帧内返回地址是在local variables前还是在它们后面? - RednaxelaFX的回答 - 知乎 https://www.zhihu.com/question/33920941/answer/57597076
实际上 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 参数


页: [1]
查看完整版本: RISC-V 入门 Part2: ABI && Calling Convention