目录
本篇介绍RISC-V的常用指令,帮助建立汇编编程的初步印象。
我们已经知道,处理器执行的大部分指令都是在存取、运算、比较寄存器和主存中的数据,或是确定下一条指令的位置(PC的值)以实现循环、分支等功能。下面,我们来介绍RISC-V指令集中的各种指令,了解如何用RISC-V编写简单的程序。
历史上,指令集的设计有RISC(Reduced Instruction Set Computer)和CISC(Complex Instruction Set Computer)两种理念,从名字中可以看出,RISC强调指令集的精简,CISC则强调指令集的复杂。CISC中的指令种类要多得多,单条指令的功能也往往比RISC更复杂,同样的指令在RISC中可能需要多条指令实现。因此CISC完成任务需要的指令数更少,但代码的编写、CPU的设计也更为复杂。Intel的x86是CISC的代表,而移动端和嵌入式设备中常见的arm(Advanced RISC Machine)则是RISC的代表。
RISC-V读作“risk-five”,是加州大学伯克利分校设计的一种开源ISA(x86是闭源的),其目标是称为ISA领域的Linux。该项目发起于2010年,为解决常见指令集的诸多问题:闭源,扼制创新;过于复杂,不利于学术研究,且很多复杂性源于历史或设计问题;针对性强,如arm面向移动端,x86面向服务器,缺少统一性架构;商业指令集易受企业发展的影响。 {% endnote %}
-
运算指令
RISC-V中的运算指令包括算术运算,逻辑运算,移位运算。运算只在寄存器之间运行,即想要对内存中的数据进行运算,需要先将其取至寄存器。
这里介绍立即数的概念。汇编中立即数即为常数,一般在运算时会对其作符号扩展,关于符号扩展和零扩展会在介绍RISC-V的机器码表示时介绍。
值得注意的是,RISC-V中有一个寄存器x0被硬编码为0,其值无法修改,作为常数存在。
算术运算
- add rd,rs1,rs2
:将寄存器rs1与rs2的值相加并写入寄存器rd。 - sub rd,rs1,rs2
:将寄存器rs1与rs2的值相减并写入寄存器rd。 - addi rd,rs1,imm
:将寄存器rs1的值与立即数imm相加并存入寄存器rd。 - mul rd,rs1,rs2
:将寄存器rs1与rs2的值相乘并写入寄存器rd。 - div rd,rs1,rs2
:将寄存器rs1除以寄存器rs2的值,向零舍入并写入寄存器rd。 - rem rd,rs1,rs2
:将寄存器rs1模寄存器rs2的值并写入寄存器rd。
以上运算发生溢出时会自动截断高位。乘法可以用
mulh
,
mulhu
获得两个32位数乘积的高32位,细节不赘述。
逻辑运算
- and rd,rs1,rs2
:将寄存器rs1与rs2的值按位与并写入寄存器rd。 - andi rd,rs1,imm
:将寄存器rs1的值与立即数imm的值按位与并写入寄存器rd。 - or rd,rs1,rs2
:将寄存器rs1与rs2的值按位或并写入寄存器rd。 - ori rd,rs1,imm
:将寄存器rs1的值与立即数imm的值按位或并写入寄存器rd。 - xor rd,rs1,rs2
:将寄存器rs1与rs2的值按位异或并写入寄存器rd。 - xori rd,rs1,imm
:将寄存器rs1的值与立即数imm的值按位异或并写入寄存器rd。
移位运算
- sll rd,rs1,rs2
:将寄存器rs1的值左移寄存器rs2的值这么多位,并写入寄存器rd。 - slli rd,rs1,imm
:将寄存器rs1的值左移立即数imm的值这么多位,并写入寄存器rd。 - srl rd,rs1,rs2
:将寄存器rs1的值逻辑右移寄存器rs2的值这么多位,并写入寄存器rd。 - srli rd,rs1,imm
:将寄存器rs1的值逻辑右移立即数imm的值这么多位,并写入寄存器rd。 - sra rd,rs1,rs2
:将寄存器rs1的值算数右移寄存器rs2的值这么多位,并写入寄存器rd。 - srai rd,rs1,imm
:将寄存器rs1的值算数右移立即数imm的值这么多位,并写入寄存器rd。
左移会在右边补0,逻辑右移会在最高位添0,算数右移在最高位添加符号位。
区分算数右移和逻辑右移,是从计算的角度考虑的:左移一位等于乘2,右移一位等于除2是算数的规律;无论正数负数,在右边补0都等于乘2;而负数进行逻辑右移的结果不等于除以2,需要用算数右移;而若只有算术右移,则无符号数的运算又会受影响。
数据传输指令
前面讲到,想要对主存中的数据进行运算,需要先将其取至寄存器,数据传输指令实现了这个目的。
现代计算机以字节(byte,1byte=8bits)为基本单位,而内存本身可被视作由byte组成的一维数组,地址从0开始。字(word)则是存取数据的另一个单位,在RISC-V中1word=4Bytes=32bits,在其他体系结构中可能会发生变化。
- lb rd,offset(rs1)
:从地址为寄存器rs1的值加offset的主存中读一个字节,符号扩展后存入rd - lh rd,offset(rs1)
:从地址为寄存器rs1的值加offset的主存中读半个字,符号扩展后存入rd - lw rd,offset(rs1)
:从地址为寄存器rs1的值加offset的主存中读一个字,符号扩展后存入rd - lbu rd,offset(rs1)
:从地址为寄存器rs1的值加offset的主存中读一个无符号的字节,零扩展后存入rd - lhu rd,offset(rs1)
:从地址为寄存器rs1的值加offset的主存中读半个无符号的字,零扩展后存入rd - lwu rd,offset(rs1)
:从地址为寄存器rs1的值加offset的主存中读一个无符号的字,零扩展后存入rd - sb rs1,offset(rs2)
:把寄存器rs1的值存入地址为寄存器rs2的值加offset的主存中,保留最右端的8位 - sh rs1,offset(rs2)
:把寄存器rs1的值存入地址为寄存器rs2的值加offset的主存中,保留最右端的16位 - sw rs1,offset(rs2)
:把寄存器rs1的值存入地址为寄存器rs2的值加offset的主存中,保留最右端的32位
l是load的首字母,即加载数据;s是store的缩写,即存储数据。b,h,w分别是byte,half word,word的首字母,除此之外还有存取双字的d,即double word。
举例:
long long A[100];
A[10] = A[3] + a;
假设数组A首地址在寄存器x3,a在x2:
ld x10,24(x3) # long long占64bits=8bytes,A[3]的地址为A[0]+3*8
add x10,x2,x10
sd x10,80(x3)
比较指令
有符号数:
- slt rd,rs1,rs2
:若rs1的值小于rs1的值,rd置为1,否则置为0 - slti rd,rs1,imm
:若rs1的值小于立即数imm,rd置为1,否则置为0
无符号数:
- sltu rd,rs1,rs2
:若rs1的值小于rs1的值,rd置为1,否则置为0 - sltiu rd,rs1,imm
:若rs1的值小于立即数imm,rd置为1,否则置为0
条件分支指令
这部分用来实现控制流,即if语句,循环等。汇编中没有C等高级语言中的
{}
语句块,而是用
Lable:
的形式,下面会举例说明。
- beq rs1,rs2,lable
:若rs1的值等于rs2的值,程序跳转到lable处继续执行 - bne rs1,rs2,lable
:若rs1的值不等于rs2的值,程序跳转到lable处继续执行 - blt rs1,rs2,lable
:若rs1的值小于rs2的值,程序跳转到lable处继续执行 - bge rs1,rs2,lable
:若rs1的值大于等于rs2的值,程序跳转到lable处继续执行
blt
和
bge
也有无符号版本
bltu
,
bgeu
。举例:
int i = 0;
do{
i++;
}while(i<10)
add x2,x0,10 # x2 = 10
add x3,x0,0 # i = 0存储在x3
Loop:
add x3,x3,1 # i++
blt x3,x2,Loop # i<10则继续循环
无条件跳转指令
- j label
:程序直接跳转到lable处继续执行 - jal rd,label
:用于调用函数,把下一条指令的地址保存在rd中(通常用x1),然后跳转到label处继续执行 - jalr rd,offset(rs)
:可用于函数返回,把下一条指令的地址存到rd中,然后跳转到rs+offset地址处的指令继续执行。若rd=x0就是单纯的跳转(x0不能被修改)
这里详细解释一下
jal
和
jalr
。在调用函数时,我们希望函数返回后,继续执行下一条指令,所以要把这下一条指令的地址存起来,再跳转到函数的代码块。函数执行完之后,根据先前存起来的指令地址,再跳回到调用处继续执行。
- lw rd,offset(rs1)
:从地址为寄存器rs1的值加offset的主存中读一个字,符号扩展后存入rd
RV32I指令介绍
-- 符号扩展:--
(1)U-TYPE
LUI: 将20位立即数放32位的高位,低12位置0,将结果载入目标寄存器。
汇编写法:
lui rd, imm