本文仅作为riscv汇编指令集的学习记录。其中可能对指令集的记录并不完善,也可能出现错误疏漏之处,所以仅作为个人学习记录。
对于RISC-V的指令集来说,其使用模块化的方式进行组织,每一个模块用一个英文字母表示,包括基本指令集I、整数乘除法指令集M、原子操作指令集A、单精度浮点型指令集F、双精度浮点型指令集D和压缩指令集C,其中特定的组合“IMAFD”被称为“通用”组合,用英文字母G表示。
而指令集格式如下图所示:
上图基本概括了riscv指令集的机器码格式。其中rs与rd表示通用寄存器(x0~x31);imm表示立即数;而opcode与funct域组合后,用于表示是哪个具体的汇编指令。
下面将通过RV32的汇编指令集来进行学习,对于RV64的指令则不进行过多介绍。因为RV64也是在RV32的基础上进行扩展。
一、基本指令集 I
1.1 整数计算指令
整数计算指令基本只使用2类汇编指令格式,即I类和R类。
当使用I类指令格式时,进行寄存器-立即数操作;当使用R类指令格式时,进行寄存器-寄存器操作。对于这两种指令操作,其目标都是寄存器rd。
1.1.1 寄存器-立即数操作指令
ADDI汇编指令:(add imm)
rd = imm + rs1;
此指令将12bit位的有符号立即数扩展为32位有符号数,然后再与rs1寄存器中的值相加,最后将计算结果赋值给rd寄存器。(并且在计算过程中,如果出现32位数据溢出,将忽略溢出情况)
对于ADDI指令来说,立即数只有12bit位,其值范围为-2048~2047。
MV汇编伪指令:
MV汇编伪指令就是利用ADDI来实现的。MV指令的作用是将某一个通用寄存器中的值复制到另一个通用寄存器,其实现原理为:
MV rd, rs1 ---> ADDI rd, rs1, 0
SLTI汇编指令:(set less than imm)
rd = (rs1 < imm) ? 1 : 0;
此指令作用是,如果rs1寄存器值小于经过有符号扩展后的12bit位的立即数时,将1赋值给rd寄存器;否则将0赋值给rd寄存器。
SLTIU汇编指令:
与SLTI指令相似,只不过此指令进行无符号立即数的比较,不会进行有符号数扩展。
NOP汇编指令:
此指令由ADDI指令实现,其原理为x0通用寄存器加0,并赋值给x0寄存器;此过程相当于没有进行任何有效的操作。
NOP ---> ADDI x0, x0, 0
SEQZ汇编伪指令:(set equal zero)
此伪指令的实现原理为,使用SLTIU指令,并将立即数设置为1;此时只有当rs1寄存器值为0时,才能小于立即数,rd寄存器才会被赋值为1:
SEQZ rd, rs1 ---> SLTIU rd, rs1, 1
ANDI、ORI、XORI汇编指令:
这些指令是逻辑位操作,将有符号扩展后的12bit位立即数与rs1寄存器中的值进行按位逻辑与、逻辑或、逻辑异或操作,并将结果存到rd寄存器中。
NOT汇编伪指令:
此指令利用XORI汇编指令来实现按位取反的功能,其原理是将立即数设置为-1,那么经过扩展后的32位立即数的值为0xFFFF_FFFF;然后再让rs1寄存器值与0xFFFF_FFFF进行异或操作,就能达到按位取反的效果。
NOT rd, rs1 ---> XORI rd, rs1, -1
SLLI、SRLI、SRAI汇编指令:
SLLI:rd = rs1 << imm;(左移后,最低位补0)
SRLI:rd = rs1 >> imm;(右移后,最高位补0)
SRAI:rd = rs1 >> imm;(右移后,最高位补符号位)
上述3个汇编指令分别代表了逻辑左移指令、逻辑右移指令和算术右移指令;并且因为这是32位riscv架构,所以imm只有5个bit位有效。(如果是64位架构,则应该有6个bit位有效)
使用方法如下所示:
slli rd, rs1, 5
LUI汇编指令:
rd = imm << 12;
此汇编指令用于将U型立即数赋值给rd寄存器的高20bit位,并将rd寄存器的低12bit位置0。
AUIPC汇编指令:
rd = pc + (imm << 12);
此汇编指令用于构建pc的相对地址,而当执行AUIPC指令时,pc的值就是AUIPC这条汇编指令的地址;因此用此指令地址加上U型立即数即可。
1.1.2 寄存器-寄存器操作指令
上述的汇编指令都与立即数操作的汇编指令原理一致,只不过是将输入参数中的立即数换成了通用目的寄存器(所以在进行操作时,不需要进行符号位扩展了,直接使用rs1和rs2寄存器中的值进行计算)。下面将仅对SUB等汇编指令进行介绍。
SUB汇编指令:
此指令用于将rs1寄存器中的值减去rs2寄存器中的值,并将计算结果存入rd寄存器中;同时忽略在计算过程中的溢出行为。
SNEZ汇编伪指令:
此指令用于判断传入的通用寄存器中值是否为0,如果值为非0,则rd寄存器置1;如果值为0,则rd寄存器置0。其实现原理如下所示:
SNEZ rd, rs2 ---> SLT rd, x0, rs2
1.2 控制转移指令
RV32I提供了2种类型的控制转移指令:无条件跳转和条件分支。
1.2.1 无条件跳转指令
JAL汇编指令:
此汇编指令采用J类型指令形式,所以立即数一共由20bit位,并且在实际使用时,立即数会左移1位,进行2字节对齐。同时由此可知,跳转的空间范围也就是在-1MB~+1MB之间;并且跳转的地址是采用pc加上符号扩展后的立即数的地址,使用的是pc相对地址,因此支持位置无关码。
在调用此指令后,同时会将当前指令的下一条指令的地址存入rd寄存器种,也就是rd = pc+4。
J汇编伪指令:
实现原理如下:
J imm ---> JAL x0, imm
值得注意的是,标准软件调用约定使用x1寄存器来进行存储跳转指令后的返回地址;当使用x0寄存器来进行存储调用返回地址时,就说明跳转之后,程序不会进行返回了。
JALR汇编指令:
此汇编指令采用I类型指令形式,rd存放的依旧是函数调用后的返回地址;不同的是,跳转的目标地址不再是pc的偏移,而是rs1加上imm立即数后,形成的目标地址;这样jalr指令访问的就是32位的绝对地址。
如果想要跳转到任意一个32位的绝对地址上去时,可以使用lui指令将addr的高20bit位赋值到一个通用寄存器中,然后再使用jalr指令,将addr的低12bit位加上这个通用寄存器,达到组合成一个32bit位的绝对目标跳转地址。
注意:在使用上述组合跳转绝对地址时,有一个问题。就是在将32bit绝对地址进行拆分时,低12bit位是表示一个有符号的立即数,所以当低12bit立即数最高位为1时,jalr指令会将其当作一个负数相加,此时将产生不正确的结果。所以当低12bit立即数最高位为1时,高20bit立即数应该加1,来消除此影响。
1.2.2 条件分支指令
所有的分支指令使用B 类型指令格式。 12 位 B 立即数以 2 字节的倍数编码符号偏移量。偏移量是符号扩展的,加到分支指令的地址上以给出目标地址。条件分支的范围是± 4KiB 。同时可以看出B类型指令格式没有rd寄存器,也就是说没有跳转后的返回地址。BEQ汇编指令:beq x5,x6,1f -> beq rs1,rs2,imm;比较rs1与rs2的值,如果相等,则跳转到新地址执行;并且具体编程时,常用1f、1b等标签来表示imm域的值,由链接器来解析标签,生成imm的值。其余指令基本类似,通过指令名称基本可以看出其作用,所以这里不再进行赘述。
1.3 加载和存储指令
RV32I是一个“加载-存储”架构,只有加载和存储指令访问内存,而算数指令只操作CPU寄存器。RV32I提供一个32位的地址空间,按字节编址。EEI将定义该地址空间的哪一部分是哪个指令可以合法访问的(例如,一些地址可能是只读的,或者只支持按字访问)。以x0为目的寄存器的加载操作必定会引发某些异常,并引起其它的副作用,即使所加载的值被丢弃。并且现在riscv架构可以自行设置选择内存系统的大小端模式。
二、控制与状态寄存器指令
CSRRW(Atomic Read/Write CSR ):指令原子性的交换 CSR 和整数寄存器中的值。 CSRRW指令读取在 CSR 中的旧值,将其零扩展到 XLEN 位,然后写入整数寄存器 rd 中。 rs1 寄存器 中的值将被写入 CSR 中。如果 rd = x0 ,那么这条指令将不会读该 CSR ,且不会导致任何因为 CSR 读而出现的副作用。CSRRS(Atomic Read and Set Bits in CSR ):指令读取 CSR 的值,将其零扩展到 XLEN 位,然后写入整数寄存器 rd 中。整数寄存器 rs1 中的初始值被当做按位掩码指明了哪些 CSR 中的位被置为 1 。 rs1 中的任何为 1 的位,将导致 CSR 中对应位被置为 1 ,如果 CSR 中该位是可以写的话。CSR 中的其他位不受影响(虽然当 CSR 被写入时可能有些副作用)。CSRRC(Atomic Read and Clear Bits in CSR ):指令读取 CSR 的值,将其零扩展到 XLEN 位,然后写入整数寄存器 rd 中。整数寄存器 rs1 中的初始值被当做按位掩码指明了哪些 CSR 中的位被置为 0 。 rs1 中的任何为 1 的位,将导致 CSR 中对应位被置为 0 ,如果 CSR 中该位是可以写的话。CSR 中的其他位不受影响。CSRRWI 指令、CSRRSI 指令、CSRRCI 指令:分别于 CSRRW 指令、 CSRRS 指令、 CSRRC 指令 相似,除了它们是使用一个处于 rs1 字段的、零扩展到 XLEN 位的 5 位立即数( zimm[4:0] )而不是使用 rs1 整数寄存器的值。对于 CSRRSI 指令和 CSRRCI 指令,如果 zimm[4:0] 字段是零,那么这些指令将不会写 CSR ,因此应该不会产生任何由于写 CSR 产生的副作用。对于 CSRRWI 指令,如果 rd=x0 ,则这条指令将不会读 CSR ,且不会导致任何因为 CSR 读而出现的副作用。值得注意的是,以下这些汇编伪指令:CSRR rd, csr 被编码为 CSRRS rd, csr, x0;(读取csr寄存器的值到rd寄存器)CSRW csr, rs1 被编码为 CSRRW x0, csr, rs1;(将rs1寄存器的值写入csr寄存器)
以上介绍了riscv的两大类汇编指令模块,上述的汇编指令也基本涵盖了日常应用的80%。后续其他的riscv汇编指令就不再进行介绍了,需要深入了解更多的汇编指令则请参考riscv的架构手册。