查看: 2560|回复: 0
收起左侧

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

[复制链接]

  离线 

  • TA的每日心情
    奋斗
    2021-3-3 12:32
  • 签到天数: 10 天

    [LV.3]

    发表于 2020-10-20 21:36:24 | 显示全部楼层 |阅读模式

    有人预言,RISC-V或将是继Intel和Arm之后的第三大主流处理器体系。欢迎访问全球首家只专注于RISC-V单片机行业应用的中文网站

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    本帖最后由 皋陶 于 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 实现:

    1. jal rd offset
    2. jalr rd rs (offset)
    复制代码
    jal 用来实现函数调用的语义:它 PC 跳转 offset (长度为 20bits),或者对应的寄存器,然后把 PC + 4 (RV32I 指令长度是 32bit, 即 4Byte,表示下一条指令)写到 rd 寄存器。

    jal 用来实现函数调用和循环中的 unconditional jump.

    C to RISC-V


    国内芯片技术交流-RISC-V 入门 Part2: ABI && Calling Conventionrisc-v单片机中文社区(1)


    上面这张图 cmu 15-445 有更好玩的版本。

    国内芯片技术交流-RISC-V 入门 Part2: ABI && Calling Conventionrisc-v单片机中文社区(2)


    这里面写了调用一个函数的6步,即函数调用规范(Calling convention) .

    • 把函数参数放到函数能访问的地方
    • 把控制权给函数(使用 jal 指令)
    • 拿到 memory 中的资源 (获取函数需要的局部存储资源,按需保存寄存器)
    • 运行函数中的指令
    • 把值写到 memory/register 中 (将返回值存储到调用者能够访问到的位置,恢复寄存器,释放局部存储资源)
    • 返回 ( ret 指令)

    寄存器有 caller-savedcallee-saved 两种:

    • caller-saved: callee 可以随便搞,从这里读数据,然后操作它们
    • callee-saved: callee 在返回前应该保存的

    ABI:调用其它函数时,关于汇编、参数、寄存器等的双方约定。(我觉得有几个答案补充的很好). 实际上 ABI 兼容性是一个和编译器等都有关的话题。ABI 定义了 calling convention,同时 ABI 定义了约束:有些寄存器是不可写的。

    同时,你可以在上面的图里面看到很奇妙的事情,这里没有再使用 x0 - x15 这样的记号,而是用了 s0 fp 这样相对来说名字好理解一些的。

    1. int Leaf(int g, int h, int i, int j) {
    2.     int f;
    3.     f = (g + h) - (i + j);
    4.     return f;
    5. }
    复制代码

    用 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. .file        "leaf.c"
    2.         .option nopic
    3.         .attribute arch, "rv32i2p0_m2p0_a2p0_c2p0"
    4.         .attribute unaligned_access, 0
    5.         .attribute stack_align, 16
    6.         .text
    7.         .align        1
    8.         .globl        Leaf
    9.         .type        Leaf, @function
    10. Leaf:
    11.         addi        sp,sp,-48 # 修改 stack, 降低以 push 值
    12.         sw        s0,44(sp)   # 把 s0 写进 44(sp), s0 这里代表 frame pointer
    13.         addi        s0,sp,48  # s0 = sp + 48, s0 为 frame pointer
    14.         sw        a0,-36(s0)  # 把 a0 - a3 存了。a0-a1 用来存返回值,a0-a7 用来传参
    15.         sw        a1,-40(s0)
    16.         sw        a2,-44(s0)
    17.         sw        a3,-48(s0)

    18.         lw        a4,-36(s0) # 把原本 a0 a1 加载到 a4 a5
    19.         lw        a5,-40(s0)
    20.         add        a4,a4,a5   # a4 = a4 + a5
    21.         lw        a3,-44(s0) # a3, a5 加载
    22.         lw        a5,-48(s0)
    23.         add        a5,a3,a5         # a5 = a3 + a5
    24.         sub        a5,a4,a5   # a5 = a4 - a5  这两段完成函数主要的计算
    25.         sw        a5,-20(s0)
    26.         lw        a5,-20(s0)
    27.         mv        a0,a5                         # a0 = a5, a0 是返回值
    28.         lw        s0,44(sp)         # s0 = sp + 44
    29.         addi        sp,sp,48 # 修改 sp
    30.         jr        ra                                 # ra 是 return address, 返回 ra
    31.         .size        Leaf, .-Leaf
    32.         .ident        "GCC: (GNU) 9.2.0"
    复制代码

    • 改变 s0 这个 frame pointer 和 sp 这个 stack pointer
    • 把原来的 a0 - a4 放到栈上
    • 用 a4 a5 来运算
    • 改回 sp, s0
    • jr 返回

    跳转回来的伪指令如下:

    国内芯片技术交流-RISC-V 入门 Part2: ABI && Calling Conventionrisc-v单片机中文社区(3)


    国内芯片技术交流-RISC-V 入门 Part2: ABI && Calling Conventionrisc-v单片机中文社区(4)


    顺便,-O2 编译的时候:
    1. .file        "leaf.c"
    2.         .option nopic
    3.         .attribute arch, "rv32i2p0_m2p0_a2p0_c2p0"
    4.         .attribute unaligned_access, 0
    5.         .attribute stack_align, 16
    6.         .text
    7.         .align        1
    8.         .globl        Leaf
    9.         .type        Leaf, @function
    10. Leaf:
    11.         add        a0,a0,a1
    12.         add        a2,a2,a3
    13.         sub        a0,a0,a2
    14.         ret
    15.         .size        Leaf, .-Leaf
    16.         .ident        "GCC: (GNU) 9.2.0"
    复制代码

    这里 a0, a1, a2, a3 四个是参数。

    下面:
    1. int mult(int, int);

    2. int sumSquare(int x, int y) {
    3.     return mult(x, x) + y;
    4. }
    复制代码

    生成汇编:
    1. .file        "ss.c"
    2.         .option nopic
    3.         .attribute arch, "rv32i2p0_m2p0_a2p0_c2p0"
    4.         .attribute unaligned_access, 0
    5.         .attribute stack_align, 16
    6.         .text
    7.         .align        1
    8.         .globl        sumSquare
    9.         .type        sumSquare, @function
    10. sumSquare:
    11.         addi        sp,sp,-16 # reserve space on stack
    12.         sw        ra,12(sp)   # save ret addr
    13.         sw        s0,8(sp)                # 存储原来的 s0
    14.         mv        s0,a1                                # s0 存储 y
    15.         mv        a1,a0                                # a1 = a0 (= x)
    16.         call        mult
    17.         add        a0,a0,s0
    18.         lw        ra,12(sp)
    19.         lw        s0,8(sp)
    20.         addi        sp,sp,16
    21.         jr        ra
    22.         .size        sumSquare, .-sumSquare
    23.         .ident        "GCC: (GNU) 9.2.0"
    复制代码




    Stack Pointer & Frame Pointer & Memory
    当然,以上演示的很多都在 stack 上,实际上我们可能需要打理的东西还更多:

    国内芯片技术交流-RISC-V 入门 Part2: ABI && Calling Conventionrisc-v单片机中文社区(5)


    堆/栈的分配是 ISA 的一部分(指令集同样是 ISA 的一部分)

    国内芯片技术交流-RISC-V 入门 Part2: ABI && Calling Conventionrisc-v单片机中文社区(6)


    以上是对 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. #include <stdlib.h>

    2. typedef struct list {
    3.     void *car;
    4.     struct list *cdr;
    5. } List;

    6. List *map(List *src, void *(*f)(void *)) {
    7.     List *ret;
    8.     if (!src)
    9.         return 0;
    10.     ret = (List *)malloc(sizeof(List));
    11.     ret->car = (*f)(src->car);
    12.     ret->car = map(src->cdr, f);
    13.     return ret;
    14. }
    复制代码

    编译一下:
    1. .file        "rich-list.c"
    2.         .option nopic
    3.         .attribute arch, "rv32i2p0_m2p0_a2p0_c2p0"
    4.         .attribute unaligned_access, 0
    5.         .attribute stack_align, 16
    6.         .text
    7.         .align        1
    8.         .globl        map
    9.         .type        map, @function
    10. map:
    11.         addi        sp,sp,-16
    12.         sw        ra,12(sp)
    13.         sw        s0,8(sp) # 存储 s0-s2
    14.         sw        s1,4(sp)
    15.         sw        s2,0(sp)
    16.         beq        a0,zero,.L3 # is-null, a0 是 src
    17.         mv        s0,a0    # save src
    18.         li        a0,8     # a0 = 8, call malloc with size 8
    19.         mv        s2,a1                 # s2 = a1
    20.         call        malloc
    21.         mv        s1,a0
    22.         lw        a0,0(s0)
    23.         jalr        s2  # jalr 调用函数,a1 是一个 function
    24.         mv        a5,a0
    25.         lw        a0,4(s0)
    26.         sw        a5,0(s1)
    27.         mv        a1,s2
    28.         call        map
    29.         lw        ra,12(sp)
    30.         lw        s0,8(sp)
    31.         sw        a0,0(s1)
    32.         lw        s2,0(sp)
    33.         mv        a0,s1
    34.         lw        s1,4(sp)
    35.         addi        sp,sp,16
    36.         jr        ra
    37. .L3:                                                # is-null, 直接返回了
    38.         lw        ra,12(sp)
    39.         lw        s0,8(sp)
    40.         li        s1,0      # li rd, imm 读取立即数,这里把 s1, 即返回值,置为0
    41.         lw        s2,0(sp)
    42.         mv        a0,s1     # a0 = 0
    43.         lw        s1,4(sp)
    44.         addi        sp,sp,16
    45.         jr        ra
    46.         .size        map, .-map
    47.         .ident        "GCC: (GNU) 9.2.0"
    复制代码

    这里的 requirements 是:我们会调用 malloc, 并在之后使用 src 和 f 参数







    上一篇:我为什么看好RISC-V
    下一篇:RISC-V 入门 Part3: 指令格式
    RISCV作者优文
    全球首家只专注于RISC-V单片机行业应用的中文网站
    回复

    使用道具 举报

    高级模式
    B Color Image Link Quote Code Smilies

    本版积分规则

    关闭

    RISC-V单片机中文网上一条 /2 下一条



    版权及免责声明|RISC-V单片机中文网 |网站地图

    GMT+8, 2024-3-29 15:04 , Processed in 0.472141 second(s), 48 queries .

    快速回复 返回顶部 返回列表