cd ..

RISC-V 简介

Updated December 10, 2022 , by fzdwx | riscvosasm

Resource

  1. RISC-V Green Card
  2. RISC-V Call convertion
  3. P&H(RISC-V)

在汇编语言中没有变量这个概念,汇编语言通常操作的是寄存器.算术指令的操作数必须取自寄存器,内建于硬件的特殊位置(CPU 内?).

寄存器(Register)是中央处理器内用来暂存指令、数据地址电脑存储器.寄存器的存贮容量有限,读写速度非常快.在计算机体系结构里,寄存器存储在已知时间点所作计算的中间结果,通过快速地访问数据来加速计算机程序的执行.

RISC-V Card

RISC-V 操作数

123

  • 如果寄存器的大小是 64 位 则称为双字,32 位 则是单字.
  • x0 被硬连接到 0
    • add x3, x4, x0 => x3 = x4 (x0 is hard-wired to value 0)

汇编指令

存储操作数

将数据从内存复制到寄存器的数据传输指令称为 载入指令(load).在 RISC-V 中指令是 ld,表示取双字.

一个从数组中取值的 C 程序,写出汇编代码

g = h + A[8];

A 是一个 100 个双字组成的数组,g, h 分别存储在 x20, x21 中,数组起始地址或基址位于 x22 中.

ld x9, 8(x22) // x9 = A[8]
add x21, x20, x9; // x21 = x20 + x9

存放基址的寄存器(x22)被称为基址寄存器, 数据传输指令中的 8 称为偏移量.

实际的RISC-V内存地址和内存中双字的内容. 双字地址是 8 的倍数,同理单字地址是 4 的倍数

:::tip 大端与小端编址 计算机分为两种,一种使用最左边或“大端”字节的地址作为双字地址,另一种使用最右端或“小端”字节的地址作为双字地址.

RISC-V 使用小端 .由于仅在以双字形式和 8 个单独字节访问相同数据时,字节顺序才有影响,因此大多情况不需要关系“大小端”. :::

所以为了上面的代码获得正确的字节地址加到 x22 这个寄存器的偏移量为 64(8x8).

与载入指令相反的指令通常被成为存储指令(store),从寄存器复制数据到内存.指令是sd,表示存储双字.

在一些体系结构中,字的起始地址必须是 4 的倍数,双字的起始地址必须是 8 的倍数.该要求成为对齐限制

RISC-V 和 Intel x86 没有对齐限制,但 MIPS 有这个限制.

使用 load 和 store 编译生成指令

A[12] = h + A[8];

h 存放在 x21 中,A 的基址存放在 x22 中.

ld x9, 64(x22)  // x9 = A[8]
add x9, x21, x9 // x9 = h + A[8]
sd x9, 96(x22)  // A[12] = x9

将字符串复制程序编译为汇编

void strcpy(char x[],char y[]){
    size_t i;
    i = 0;
    while((x[i] = y[i]) != '\0'){
        i += 1;
    }
}

x, y 的基址存放在 x10 和 x11 中, i 存放在 x19 中.

strcpy:
    addi sp, sp, -8  // 调整栈指针,以存放一个item(x19)
    sd x19, 0(sp)    // x19 入栈
    add x19, x0, x0  // x19 = 0 + 0
L1: add x5, x19, x11 // x5 = x19 + x11 => address of y[i] in x5
    lbu x6, 0(x5)    // temp: x6 = y[i]
    add x7, x19, x10 // x5 = x19 + x11 => address of x[i] in x7
    sd  x6, 0(x7)    // x[i] = y[i]
    beq x6, x0, L2   // if x6 ==0 then go to L2
    addi x19, x19, 1 // i = i  + 1
    jal x0, L1       // go to L1
L2: ld x19, 0(sp)    // 恢复 x19 以及栈指针
    addi sp, sp, 8
    jalr x0, 0(x1)

一段循环代码编译为汇编

int A[20];
int sum = 0;
for (int  3i = 0; i < 20; i++){
    sum += A[i];
}

RISC-V 汇编(32 bit)

    add x9, x8, x0     # x9 = &A[0]
    add x10, x0, x0    # sum
    add x11, x0, x0    # i
    addi x13,x0, 20    # 20
Loop:
    bge x11, x13, Done # if x11 > x13 go to Down(end loop)
    lw x12, 0(x9)      # x12 = A[i]
    add x10, x10, x12  # sum
    addi x9, x9, 4     # x9 = &A[i+1]
    addi x11, x11, 1   # i++
    j loop
Done:

逻辑操作

  • and andi
    • and x5, x6, x9 => x5 = x6 & x9
    • addi x5, x6, 3 => x5 = x6 & 3
  • sll ssli , 左移(扩大)
    • slli x11, x23, 2 => x11 = x23 << 2
    • 0000 0010 => 2
    • 0000 1000 => 8
  • srl srli , 右移(缩小)
    • srli x23, x11, 2 = > x23 = x11 >> 2
    • 0000 1000 => 8
    • 0000 0010 => 2
  • sra srai, 算数右移
    • 1111 1111 1111 1111 1111 1111 1110 0111 = -25
    • srai x10, x10, 4
    • 1111 1111 1111 1111 1111 1111 1111 1110 = -2

Helpful RISC-V Assmebler Features

  1. a0 - a7 是参数寄存器(x10 - x17,用于函数调用.
  2. zero 代表 x0
  3. mv rd, rs = addi rd, rs, 0
  4. li rd, 13 = addi rd, x0, 13
  5. nop = addi x0, x0
  6. la a1 Lable 将 Label 的 地址 加载到 a1
  7. a0 - a7(x10 - x17): 8 个寄存器用于参数传递以及两个返回值(a0 - a1)
  8. ra(x1): 一个返回 address 的寄存器,用于返回原点(调用的位置)
  9. s0 - s1(x8 - x9) and s2 - s11 (s18 - x27): 保存的寄存器

RISC-V 函数调用的转换

  1. 寄存器比内存快,所以使用它们
  2. jal rd, Label 跳转和链接
    1. jal x1, 100
  3. jalr rd, rs, imm 跳转和链接寄存器
    1. jalr x1, 100(x5)
  4. jal Lable => jal ra, Lable 调用函数
  5. jalr s1 当 s1 是方法指针时,这就是一个函数调用

一段函数调用转换为汇编

...
sum(a,b);
...
int sum(int x, int y){
    retrun x + y;
}
1000 mv a0, s0              # x = a
1004 mv a1, s1              # y= b
1008 addi ra, zero, 1016    # 1016 is sum function
1012 j                      # jump to sum
1016 ...
...
2000 sum: add a0, a0, a1
2004 jr ra

1008 ~ 1012 可以使用 jal sum 来替代、

调用函数的基本步骤

  1. 把需要的参数放到方法可以访问的地方(寄存器)
  2. 转移控制权给函数,使用(jal)
    1. 保持地址,并跳转到函数的地址
  3. 获取函数执行所需的(local)存储资源
  4. 执行预期的函数
  5. 将返回值放在调用代码可以访问的地方,并恢复我们使用到的寄存器,释放本地存储
  6. 将控制器返回给主处理器(ret), 使用存储在寄存器中的地址,返回到调用它的地方

方法调用示例

int leaf(int g, int h, int i, int j){
    int f;
    f = (g + h) - (i + j);
    retrun f;
}
  1. g,h,i,j in a0,a1,a2,a3
  2. f in s0
  3. temp is s1
leaf:
    # prologue start
    addi sp, sp, -8   # 腾出 8byte 来存放的2个整数
    sw s1, 4(sp)      # 保存 s1, s0 到 sp 中
    sw s0, 0(sp)
    # prologue end
    add s0, a0, a1    # f = g + h
    add s1, a2, a3    # temp = i + j
    sub a0, s0, s1    # a0 = (g + h) - (i + j)
    # epilogue
    lw s0, 0(sp)      # 恢复 s1, s0
    lw s1, 4(sp)
    addi sp, sp 8
    jr ra

sp

sp 是栈指针,从内存空间 的最顶部开始向下增长,在 RISC-V 中使用 x2 这个寄存器.

  1. push 是减少 sp 的指针地址
  2. pop 是增加

每个函数都有一组存放在栈上的数据,它们是栈帧(stack frame ),栈帧通常包含:

  1. 返回地址
  2. 参数
  3. 使用的局部变量的空间

嵌套函数调用

int sumSquare(int x,int y){
    return mult(x,x) + y;
}

在 ra 中有一个 sumSquare 想要跳回的值,但是这个值会被调用 mult 覆盖.

  1. caller: 调用函数的人
  2. calle: 被调用的函数
  3. 当被调用者从执行中返回时,调用者需要知道哪些寄存器可能发生了变化,哪些寄存器被保证是不变的.
  4. 寄存器规定: 即哪些寄存器在程序调用(jal) 后将被取消缓存 ,哪些可以被改变.
    1. 即有一些寄存器是易失的(temp),一些是要保存的(调用者需要恢复它们原来的值).
    2. 这优化了每次进入栈帧的寄存器的数量
  5. 分类:
    1. 跨函数调用保留:
      1. sp, gp, tp
      2. s0 - s11 (s0 is also fp)
    2. 不保留:
      1. 参数寄存器以及返回寄存器: a0 - a7, ra
      2. temp 寄存器: t0 - t6

上面代码的 RISC-V

x in a1, y in a1

sumSquare:
    addi sp, sp -8
    sw ra, 4(sp)             // save retrun address to sp
    sw a1, 0(sp)             // save s1 to y
    mv a1, a0                // y = x => mult(x,x)
    jal mult                 // call mult
    lw a1, 0(sp)             // get y from sp
    add a0, a0, a1           // mult() + y
    lw ra, 4(sp)             // get retrun address from sp
    addi sp, sp, 8
    jr ra

RISC-V 寄存器名称

RISC-V 方法调用套路

matmul:
    # 压栈,腾出空间保存我们要使用的几个 s 寄存器
    addi sp sp -36
    sw ra 0(sp)
    sw s0 4(sp)
    sw s1 8(sp)
    sw s2 12
    sw s3 16(sp)
    sw s4 20(sp)
    sw s5 24(sp)
    sw s6 28(sp)
    sw s7 32(sp)
body:
    # xxx xxx
end:
    # 恢复寄存器的值
    lw ra 0(sp)
    lw s0 4(sp)
    lw s1 8(sp)
    lw s2 12(sp)
    lw s3 16(sp)
    lw s4 20(sp)
    lw s5 24(sp)
    lw s6 28(sp)
    lw s7 32(sp)
    addi sp sp 36
    ret

RISC-V 指令二进制的表示

R 格式布局

用于算术和逻辑运算的指令

  1. opcode,funct3, funct7 : 将告诉我们是否要执行加,减,左移,异或等操作.
    1. R-format 的 opcode 固定为 0110011
  2. 一个 add 操作: add x18 x19 x10 => x18 = x19 + x10
  3. 0000000 01010 10011 000 10010 0110011
  4. rs2 = x19, rs1 = x10, rd = x18

I 格式布局

处理立即数,比如addi rd rs1, imm => addi a0 a0 1

  1. imm 的范围是 -2084 ~ 2047

addi x15 x1 -50

RISC-V Loads

load 指令也是 I 类型的.

lw x14 8(x2)

S 格式布局