Riscv学习笔记 - 常用指令

Riscv学习笔记 - 常用指令

1935字9分钟


目录

常用指令

要评判一个指令集架构,不仅要看它包括了什么,而且要看它省略了什么

算术运算

指令格式功能
addadd rd, rs1, rs2rd = rs1 + rs2
addiaddi rd, rs1, immrd = rs1 + imm
subsub rd, rs1, rs2rd = rs1 - rs2

addaddi的区别就是add用于将两个寄存器相加后存入另一个寄存器,addi用于将一个寄存器加上立即数后存入。

立即数

与操作码一起放在指令代码段中的数值。直观来说就是直接写在指令后边的数。

为何没有subi指令?

可以用sub rd, rs1, -imm来实现。

逻辑运算

指令格式功能
andand rd, rs1, rs2逻辑与
oror rd, rs1, rs2逻辑或
xorxor rd, rs1, rs2逻辑异或
sllsll rd, rs1, shamt逻辑左移
srlsrl rd, rs1, shamt逻辑右移
srasra rd, rs1, shamt算术右移

srl相较于srasrl右移后左侧补零,而sra会扩展符号位。

为什么没有not指令?

可以用xor rd, rs1, -1来实现。

分支控制

指令格式功能
beqbeq rs1, rs2, L1If rs1==rs2 goto L1
bnebne rs1, rs2, L1If rs1!=rs2 goto L1
bltblt rs1, rs2, L1If rs1<rs2 goto L1
bgebge rs1, rs2, L1If rs1>=rs2 goto L1
bltubltu rs1, rs2, L1上面两个条件的无符号版本
bgeubgeu rs1, rs2, L1
Riscv的else语句通常放在if语句之前。

使用if-else语句时,判断条件是==,就要用bne;条件是!=,就要用beq

IF: bne rs1, rs2, Else # --> Else: rs1!=rs2
    # ↓ If: rs1==rs2
    ...
    j Then


Else:
    # code for else statement
    ...
    j Then

Then:
    # code for then statement
    ...

伪指令

这些指令并不是可执行的指令,没有相应机器码,在编译时会被替换成其他指令。

以下几个伪指令就相当于是一些指令的简写,编译时会被替换为相应指令。

指令格式对应指令功能
mvmv rd, rsaddi rs, rs, 0移动寄存器的值
negneg t0, t1sub t0, zero, t1取负数(补码)
notnot t0, t1xori t0, t1, -1取反码
lili rd, immaddi rd, x0, imm加载立即数
lala t0, strlui t0, str[31:12]; addi t0, t0, str[11:0]
auipc t0, str[31:12]; addi t0, t0, str[11:0]
详见下一节
加载地址
nopnopaddi x0, x0, 0空操作
beqzbeqz t0, loopbeq t0, zero, loopt0==0时跳转

加载与储存

有时我们需要从内存中加载数据到寄存器中,或者将寄存器中的数据保存到内存中。 我们将内存中数据的地址放在寄存器中,然后将数据读出或写入到另一个寄存器里。

指令格式功能
lwlw rd, imm(rs1)rs1+imm处取一个字到rd
lblb rd, imm(rs1)rs1+imm处取一个字节到rd
lbulb rd, imm(rs1)rs1+imm处取一个字节(无符号数)到rd
swsw rs1, imm(rs2)保存rs1rs2+imm
sbsb rs1, imm(rs2)保存rs1rs2+imm

例如,从栈里取一个字:

lw t0, 0(sp)

栈和寄存器sp的概念稍后就会讲到

这里要注意数据流动的方向:

函数调用与跳转

函数调用的六个步骤:

指令格式功能对应指令
jj label跳转jal x0, label
jrjr rs1跳转到寄存器
jaljal rd, label跳转并链接
jalrjalr rd, rs1, offset跳转并链接(带偏移)
retret返回jr ra

jal 相当于addi ra, zero, imm; j label,省去每次都手动设置返回地址的麻烦。 但实际上j才是伪指令,这里只是说作用上的的等价。

Tip
1008 addi ra, zero, 1016
1012 sum

等价于:

1008 jal sum

Tip
寄存器功能别名
x00zero
x1返回地址ra
x2栈指针sp

栈是一种先进后出的数据结构,用来保存函数调用时的临时变量。我们使用sp寄存器来指向栈顶,sp自增或自减来移动栈顶指针。要注意的是,栈是一块连续的内存空间,由高地址向低地址增长,故增长栈的时候,栈指针sp要向低地址移动,反之增加。

栈顶:0x000000栈底:0xFFFFF0sp8(sp)

例子:

int Leaf(int g, int h, int i, int j) {
    int f;
    f = (g + h) - (i + j);
    return f;
}

我们把Leaf的四个参数分别放在a0, a1, a2, a3中,把f放在s0中。为了进行计算,我们可能还需要一个临时变量s1。 所以我们还需要2*4=8个字节的栈空间用来保存s0s1的原始值,以便返回后恢复。

RISC-V中没有类似pushpop的指令,而是直接通过addilw等来实现栈操作。
Leaf:

  # Step 1: prologue
  addi sp, sp, -8 # 增加8个字节的栈空间
  sw s1, 4(sp) # 保存临时变量s1
  sw s0, 0(sp) # 保存临时变量s0

  # Step 2
  add s0, a0, a1 # 计算f = (g + h)
  add s1, a2, a3 # 计算s = (i + j)
  sub s0, s0, s1 # 计算f = (g + h) - (i + j)

  # Step 3: epilogue
  jal func
  lw s0, 0(sp) # 恢复s0
  lw s1, 4(sp) # 恢复s1
  addi sp, sp, 8 # 释放栈空间
  ret

prologue序言 是施法前摇,而 epilogue结语 是后摇。

嵌套函数的返回

设想这样的场景:函数A调用函数B,记录下A的栈指针;B又调用C,记录下B的栈指针。但问题是,我们只有一个栈指针寄存器sp,怎么保存函数调用过程中的两个栈指针呢?

寄存器ABI名称功能Saver
x0$zero硬编码为0-
x1ra返回地址Caller
x2sp栈指针Callee
x3gp全局指针-
x4tp线程指针-
x5t0临时寄存器Caller
x6-7t1-2临时寄存器Caller
x8s0/fp保存寄存器/帧指针Callee
x9s1保存寄存器Callee
x10-11a0-1参数寄存器、返回值寄存器Caller
x12-17a2-7参数寄存器Caller
x18-27s2-11保存寄存器Callee
x28-31t3-6临时寄存器Caller
Preserved(callee-saved)NonPreserved(caller-saved)
Saved registers:s0-s11Return address: ra
Stack pointer:spArgument registers:a0-a7
Return values:a0-a1
Temporary registers:t0-t6

Caller-saved指的是,在调用函数之前,调用者需要自己保存的寄存器,比如为了传递参数,需要覆盖参数寄存器a0-a7,这些寄存器的原始值就需要调用者保存,即放到栈上,以便在被调用函数返回时恢复。(实际上恢不恢复由具体情况决定,总之是调用者在管理)

Callee-saved指的是,在调用函数时,Callee需要保存的寄存器,比如s0-s11,被调用函数需要提前保存原始值并在返回时恢复

区分二者最简单的方法就是看Callee退出时需不需要恢复其值,如果需要,就属于Callee-saved;否则就属于Caller-saved。

Caller保存寄存器Callee保存寄存器
函数调用前后可能改变函数调用前后不变
会被Callee污染需要Callee维护,防止污染
“谁污染,谁治理”

程序的位置

一个简单的程序在riscv上运行时在内存里的位置如图:

Reserved0 Textpc = 0001 0000hex Staticpc = 1000 0000hex Dynamic DataStackSp = bfff fff0hex

其中:

小结

有了以上知识,我们就可以用汇编来实现几乎所有的程序了,其他的无非是一些系统调用和“让我们生活更加轻松的东西”。

芜湖!