RISC-V汇编语言入门(六) —RV32I指令集
第六章
RV32I指令集
RISC-V是模块化的指令集架构,允许设计成各种微处理器。这种灵活性允许工业上根据场景需求设计不同的微处理器。包括嵌入式设备的超低功耗微处理器和运行在数据中心的高性能服务器处理芯片。
达到这种灵活性的基础依赖于RV32指令集的四大基本指令集架构和十几种扩展指令集的结合。
基础指令集
名称 | 说明 |
---|---|
RV32I | 32位整数指令集 |
RV32E | 嵌入式32位整数指令集 |
RV64I | 64位整数指令集 |
RV128I | 128位整数指令集 |
扩展
后缀 | 说明 |
---|---|
M | 整型乘除法扩展 |
A | 原子指令扩展 |
F | 单精度浮点类型扩展 |
D | 双精度浮点类型扩展 |
G | 基础及扩展指令的简写 |
Q | 四精度浮点类型扩展 |
L | 十进制浮点类型扩展 |
C | 压缩指令集扩展 |
B | 位操作扩展 |
J | 动态翻译语言扩展 |
T | 事务内存扩展 |
P | SIMD指令扩展 |
V | 向量操作扩展 |
N | 用户中断扩展 |
H | 虚拟机监控扩展 |
表6.1 基础指令集与扩展指令集
这里我们主要关注RV32IM,讲解的内容也是基于该指令集。M后缀代码表指令集包含整数乘除法指令。该指令集主要有以下特点:
- 支持32位地址空间;
- 包含33个32位寄存器;
- 补码表示有符号数;
- 除基本指令外,还包含整数乘除法、整数加载、整数存储和控制流;
- 乘除指令可以直接对整数寄存器的值进行运算。
6.1 数据类型和内存组织
RV32I原始数据类型:
RV32I原始数据类型 | 字节数 |
---|---|
byte | 1 |
unsigned byte | 1 |
halfword | 2 |
unsigned halfword | 2 |
word | 4 |
unsigned word | 4 |
与其他指令集相同,RISC-V也是基于可寻址内存设计,每个单字节存储位置都有唯一的地址。
多个字节的数据需要多个内存地址存储,halfword需要2个内存地址,word需要4个。表6.3显示了C语言中的数据类型和RV32I中的数据类型对应关系。
6.2 RV32I寄存器
RV32I包含33个32位通用寄存器,x0是一个硬链接的特殊寄存器,读取时总时返回0。pc寄存器,也叫程序计数器,执行时会被自动更新为下一条将要执行的指令地址,也会被分支指令更新。
剩下的x1-x31寄存器是通用的,之间可以互换,然而,后面将要讲到,通常使用时遵循一定的标准很重要。例如通常用一组特定的寄存器用来函数调用时的传参,并给它们一组别名,使用一组有意义的名字在编程中使用更方便。
寄存器 | 别名 | 说明 | 调用者保存 | 被调用者保存 |
---|---|---|---|---|
x0 | zero | 硬编码 | ||
x1 | ra | 返回地址 | X | |
x2 | sp | 栈指针 | X | |
x3 | gp | 全局指针 | ||
x4 | tp | 线程指针 | ||
x5-x7 | t0-t2 | 临时变量寄存器 | X | |
x8 | s0/fp | 保存寄存器0/栈帧指针 | X | |
x9 | s1 | 保留存存器1 | X | |
x10-x17 | a0-a7 | 函数参数0到7 | X | |
x18-x27 | s2-s11 | 保存寄存器2到11 | X | |
x28-x31 | t3-t6 | 临时变量寄存器3到6 | X | |
pc | pc | 程序计数器 |
表6.1
s0 - s11都是Callee寄存器,一些数据在函数调用之后仍然能够保存。
6.3 加载与存储
load/store指令用来在操作数据前从内存中读取和写入数据。换句话说,在数据作运算前需要用load指令将内存中的数据加载到寄存器,用store指令将计算的结果存回内存。看下面的汇编代码:
第一条指令lw,a0寄存器内的值+偏移0得到要加载的内存地址,从该地址处加载一个字节到a5。假设a0的值是8000,那么结果就是从内存地址8000处加载一个字节的数据放到寄存器a5中。
第二条指令add,将a5中的值再加上一次本身,也就是a5的值乘以2,得到的结果值放到a6。
最后一条指令,将a6中的值存储到a0+0地址处的内存中。
整个过程是:在a0+0内存地址处取数据,将数据加上他本身,然后再存回原来地址。
6.4 伪指令
对于汇编代码,每一条汇编指令都会被翻译成对应的机器码(二进制值),如add x10, x11, x12
编译后的机器指令就是“0x00c58533”。
伪指令是一种没有直接对应机器码的指令,但它们可以被翻译成一组同等效果的机器指令。例如“nop”伪指令,被翻译后的到的指令是“add x0, x0, 0”。由于我们讲解的的是汇编语言,之后的章节我们不去关注伪指令和其真实的RV32I机器码,你可以去查阅RISC-V指令集介绍手册。
6.5 逻辑运算、位移及算术运算
6.5.1 语法及操作数
这几种指令都包含三个操作数:一个目标操作数,两个源操作数。第一个操作数是一个目标寄存器,用来存储结果。第二第三个操作数代报两个需要进行运算的寄存器或者立即数。例如下面的形式: MNM rd, rs1, rs2
或MNM rd, rs1, imm
(imm代表立即数,MNM代表指令)。以下代码分别是逻辑运算、位移运算和算术运算:
add a0, a2, a6 # a0 = a2 & a6
slli a1, a3, 2 # a1 = a3 << 2
sub a4, a5, a6 # a4 = a5 - a6
第一条指令:a2,a6按位与,结果存储在a0; 第二条指令:a3向左位移2位,结果存储在a1; 第三条指令:a5减a6,结果存储在a4。
通用寄存器x0-x31都可能被用做rd,rs1或rs2。然而注意,如果x0被用做目标操作数,结果值会被丢弃(还记得前面提到过,x0值永远是0吗)。
6.5.2 处理较大的立即数
立即数会被直接编码到机器指令中,与此同时,还有操作码和操作数也会一起编译进去。由于RV32I指令只有32位,所能直接编译进去的立即数不能超过32位。实际上RV32I中算术、逻辑和位于运算最大只能支持12位的有符号立即数。也就是立即数的值介于-2048($-2^{11}$)和2047($-2^{11}$)。所以以下的汇编代码是无效的:
用as汇编器编译是报错:
为了使用小于-2048或者大于2047的立即数,程序员可以通过几条指令来组合出这些想要的数值。例如:
上面的三条指令就得到了一个大于2047的数。首选将一个较小的数1000加载到a5,然后向左位移2位(即乘以$2^2$)得到的值4000再存回a5,然后再将a5的值加5得到,再存回a5,最终a5的值是4005。
在实际的编程过程中,RISC-V推荐使用“load immediate”伪指令li。汇编器可以按照最佳的指令顺序将其翻译为机器码。如: li rd, imm
6.5.3 逻辑运算指令
表6.5展示了各逻辑运算指令:
指令 | 说明 |
---|---|
and rd, rs1, rs2 | rs1,rs2按位与,结果存储在rd |
or rd, rs1, rs2 | rs1,rs2按位或,结果存储在rd |
xor rd, rs1, rs2 | rs1,rs2按位异或,结果存储在rd |
andi rd, rs1, imm | rs1,imm按位与,结果存储在rd |
ori rd, rs1, imm | rs1,imm按位或,结果存储在rd |
xori rd, rs1, imm | rs1,imm按位异或,结果存储在rd |
表6.5 imm代表立即数
and a0, a2, s2 # a0 = a2 & s2
or a1, a3, s2 # a1 = a3 | s2
xor a2, a2, a1 # a2 = a2 ^ a1
andi a0, a2, 3 # a0 = a2 & 3
ori a1, a3, 4 # a1 = a3 | 4
xori a1, a2, 1 # a2 = a2 ^ 1
assembly li a1, 0xFE01AB23
li a2, 0X0000FF00
and a0, a1, a2
分别加载两个立即数到a1、a2,按位与后结果(0x0000AB00)存储到a0。
6.5.4 位移指令
移位指令用于将二进制值左移或右移。这些指令可用于将位压缩或解压缩成字或执行算术乘法和除法运算。表6.6显示了RV32I移位指令。
指令 | 说明 |
---|---|
sll rd, rs1, rs2 | 将rs1逻辑左移,左移位数为rs2,结果存储rd |
srl rd, rs1, rs2 | 将rs1逻辑右移,右移位数为rs2,结果存储rd |
sra rd, rs1, rs2 | 将rs1算术右移,右移位数为rs2,结果存储rd |
slli rd, rs1, imm | 将rs1逻辑左移,左移位数为imm,结果存储rd |
srli rd, rs1, imm | 将rs1逻辑左移,右移位数为imm,结果存储rd |
srai rd, rs1, imm | 将rs1算术左移,右移位数为imm,结果存储rd |
表6.6 逻辑位移是移动所有二进制bit位,算术位移是除符号位之外的bit位位移
下面的代码展示了逻辑位移:
第二行位移指令slli,将a2的值左右了2位,结果存储在a0; 第三行指令sll,也对a2进行了左移,左移的位数由a3的值指明。
左移指令相当于原数乘以$2^N$,N代表左移位数,在左移时原数随着移动左侧位被丢弃,右侧位进行补零。
对与硬件来说,位移操作比乘法运算更高效,因此编译器尽可能的将乘法运算编译为位移运算。
逻辑右移指令与逻辑左移指令类似,顾名思义,他的移动方向是向右。看下面的代码:
第二行位移指令srli,将a5的值按位向右移动2位,结果存储在a0; 第三行指令srl,也对a5按位进行了整体右移,左移的位数由a3的值指明。
对于无符号数来说,右移相当于原数除以$2^N$,N代表右移动位数,在右移时原数随着移动左侧位补零,右侧位会被丢弃。
我们上面强调了无符号数的右移相当于除以$2^N$。我们看一下有符号数是怎样的,假设右数值-24,它的二进制表示:
我们按照右移操作,将其按位右移2位得到:
再看一下这个数,变成了一个非常大的数:1073741818。原来最左侧的符号位1(代表负数)变成了0。因此,逻辑右移指令只能用在无符号数的除法中,原因是逻辑右移会影响到最左侧的符号位。
为了应对上面的问题,RV32I引入了算术右移指令:sra和srai。与逻辑右移相同的是在右移时最右侧的位会被丢弃,但与逻辑右移最左侧补零不同的是,算术右移时会在左侧不断复制最左侧的位来填充,同样考虑上面的例子中数字-24的操作:
对其进行算术右移2位:
得到的值是-6,也就是-24除以4。这才符合算术中的除法运算。因此在进行算术除法运算时应该使用算术右移指令。
6.5.5 算术指令
带M后缀的RV32I指令集,增加了算术运算指令mul、div和rem。
指令 | 说明 |
---|---|
add rd, rs1, rs2 | rd = rs1 + rs2 |
sub rd, rs1, rs2 | rd = rs1 - rs2 |
addi rd, rs1, rs2 | rd = rs1 + imm |
mul rd, rs1, rs2 | rd = rs1 x rs2 |
div{u} rd, rs1, rs2 | rd = rs1 / rs2,u后缀表示无符号数运算 |
rem{u} rd, rs1, rs2 | rd = rs1 % rs2,u后缀表示无符号数运算 |
表6.7
RV32IM没有提供subi这种对立即数做减法运算的指令,然而,可以很容用addi指令实现同样的功能:addi a0, a2, -10 ## a0 = a2 - 10
。
下面的代码展示了这几种算术指令的用法:
如果使用的不是M扩展的RV32I指令,那么你需要结合位移操作和加法运算来实现乘除法运算:
6.6 数据移动指令
RV32I数据移动指令可以从内存中加载数据到寄存器,或者将寄存器值存储到内存。亦或者将数据从一个寄存器复制到另一个。或者将立即数或标签地址加载到寄存器。下表列出了RV32I数据移动指令:
指令 | 说明 |
---|---|
lw rd, imm(rs1) | 从内存地址中加载一个32位有符号或无符号字到寄存器rd,地址为rs1+imm |
lh rd, imm(rs1) | 从内存地址中加载一个16位有符号半字到寄存器rd,地址为rs1+imm |
lhu rd, imm(rs1) | 从内存地址中加载一个16位无符号半字节到寄存器rd,地址为rs1+imm |
lb rd, imm(rs1) | 从内存地址中加载一个8位有符号字节到寄存器rd,地址为rs1+imm |
lbu rd, imm(rs2) | 从内存地址中加载一个8位无符号字节到寄存器rd,地址为rs1+imm |
sw rd, imm(rs2) | 将rd中的32位值存储到(rs2+imm)地址的内存中 |
sh rd, imm(rs2) | 将rd中的最低16位值存储到(rs2+imm)地址的内存中 |
sb rd, imm(rs2) | 将rd中的最低8位值存储到(rs2+imm)地址的内存中 |
表6.8 数据移动指令
6.6.1 加载指令
加载指令(lw,lh,lhu,lb和lbu)的语法格式: MNM rd, imm(rs1)
“MNM”代表操作指令,rd是目标寄存器,rs1的值加上立即数imm就是内存地址,从该地址处复制相应的字节数据到寄存器rd中。
字加载指令lw,用来从内存中加载32位数据到寄存器的指令,一次加载4个字节。
指令 | 说明 |
---|---|
mv rd, rs | 从rs复制值到rd |
li rd, imm | 将立即数imm加载到寄存器rd |
la rd, rot | 将地址值rot加载到寄存器rot |
L{W|H|HU|B|BU|} rd, lab | 都是从标签lab指定的地址中加载数据到寄存器rd,不同的是加载的数据字节数不同 |
S{W|H|B|} rd, lab | 都是将寄存器rd的值存储到标签lab指定的地址内存中,不同的是村粗的字节数不同 |
表6.9 数据移动伪指令
RV32I遵循小端地址规范,因此低地址端的数据会被放到寄存器的最低有效字节。如图6.2所示:
图6.2 四字节数0x0A0E0108加载过程
从8000(加偏移0)地址开始的4字节数按顺序从寄存器低字节开始,加载到寄存器a0。
下图是字节加载指令,加载8位的无符号字节到寄存器中的示意图,由于寄存器是32位,所以无符号字节加载指令,加载单个字节到寄存器时,字节被放到寄存器最低有效字节中,其余的寄存器字节置0。
图6.3 无符号数0x08加载到a0
字节加载指令常用来加载char型数据结构。
有符号字节加载指令,在加载时分两种情况: 一、如果值是正数,那么与无符号字节加载指令的效果一样;
二、如果值是负数,那么加载字节到寄存器最低有效字节后,其他3个高字节位被全部置1。
同理,lhu与lbu类似,lh与lb类似,只不过lhu、lh加载的是2个字节。
lh加载负数时,寄存器剩余字节位置1。
6.6.2 存储指令
存储指令将寄存器中的值存储到内存中,语法格式: MNM rs1, imm(rs2)
MNM代表指令指令助记符,rs1代表源寄存器,imm(rs2)代表内存地址为:rs2加一个立即数(偏移)。
sw指令将一个32位的寄存器值存储到指定内存地址,与加载指令是一个逆向过程,同样遵循的是小端地址模式,如下图6.9表示sw a0, 0(a2)
这条汇编指令的工作过程:
存储指令可以用来存储int、unsigned int、long、unsigned long以及指针(地址)。
16位半字(四个字节为一个字)存储指令sh,会将寄存器低位的两个字节存储到内存中,存储地址也是按照低字节在低地址,高字节在高地址的顺序。如图6.10所示:
图6.10 将值为0x0108的半字存储到内存
半字(32位架构为2个字节)存储指令用来存储C语言中的short和unsigned short类型数据。
字节存储指令sb将rs1最低有效字节(8-bit)存储到内存。由于sb正好存储一个字节,恰好可以将数据存储到内存中的一个可寻址的有效地址。如下图6.11所示,sb指令将a0中的一个字节存储到内存的一个有效地址8000处,该内存地址保存在寄存器a2中($8000_{10}$)。
图6.11 单字节(0x80)存储指令
sb指令用来存储C语言中的char和unsigned char类型数据。
6.6.3 数据移动指令
mv指令用于将一个寄存器的值复制到另一个,指令语法: mv rd, rs
rd为目标寄存器,rs为源寄存器。
li指令用于将一个立即数复制到寄存器中,6.5.2小节讨论过,该伪指令会依据立即数的值翻译为单条或多条机器指令。指令语法: li rd, imm
rd为目标寄存器,imm代表立即数。
la指令用于将一个符号代表的地址复制到寄存器中,指令语法为: l{w|h|hu|b|bu} rd, symbol
rd代表目标寄存器,symbol是标签名。例如: lh a0, var_x
从var_x标签地址处取半字(2个字节)数据存储到a0。由于标签代表的是32位地址且没有被编码为立即数,所欲汇编器会将改条指令翻译为多条可执行的机器指令。这种情况下,会先通过指令将标签处的地址加载到rd,然后再生成最终的机器指令。
存储指令是一组用来将值存储到标签地址的指令,语法: s{w|h|b} rs, symbol, rt
rs代表源寄存器,symbol是标签,rt代表用来计算地址的临时寄存器。例如,指令: sw a0, var_x, a5
该指令将a0中的一个字的数据存储到var_x标签地址处,与加载指令类似,标签不是立即数,因此在汇编器计算该标签实际值时,需要一个临时寄存器存储临时计算结果。之所以不直接用rs作为临时寄存器,是因为在存储到内存之前,寄存器内的值会被破坏。因此用户必须明确指定一个一般寄存器作为计算的临时场所。
6.7 控制流指令
通常,处理器执行指令的顺序是按照指令在内存中的排列顺序执行的。也就是说,当一条指令执行完成后,处理器立马取得内存中下一个指令继续执行。在RISC-V处理器中,一条指令占4个字节。因此0x8000处的指令被执行完后,处理器去下一个地址0x8004处去下一条指令。
控制流指令可以打破这种顺序执行的流程。这种情况下,下一条指令的地址由控制流指令中的语法指定。
控制流指令可以分为有条件的和无条件的,也可以分为直接的和间接的。
6.7.1 条件跳转指令
顾名思义,条件跳转指令是依据指令中的条件来决定跳转。换句话说就是,通过判断条件来决定是否改变当前执行流程。例如beq指令,它会比较两个寄存器值,如果相等则跳转到目标地址。 下面的代码中,beq指令在判断a0和a1相等时跳转到标签L处,也就是sub指令。如果a0和a1不相等,则不会跳转,继续执行下一条指令add。
RV32I架构中有多个条件控制流指令:
指令 | 描述 |
---|---|
beq rs1, rs2, lab | 如果rs1等于rs2,跳转到lab |
bne rs1, rs2, lab | 如果rs1不等于rs2,跳转到lab |
beqz rs1, lab | rs1等于零,跳转到lab |
bnez rs1, lab | rs1不等于零,跳转到lab |
blt rs1, rs2, lab | rs1小于rs2(有符号),跳转到lab |
bltu rs1, rs2, lab | rs1小于rs2(无符号),跳转到lab |
bge rs1, rs2, lab | rs1大于等于rs2(有符号),跳转到lab |
bgeu rs1, rs2, lab | rs1大于等于rs2(无符号),跳转到lab |
表6.0 RV32I 条件控制流指令
blt与bltu的区别是比较的值是否考虑符号,如过有符号比较,0xFFFFFFFF小于0x00000000,但如果是无符号比较,则0xFFFFFFFF大于0x00000000。
下面是几个例子:
控制流指令通常应用在条件语句和循环语句中。如下面的一段代码:
寄存器a0用作循环计数,在第2行初始化后值为10。之后的每一次循环,a0都会由sub指令减1(第7行)。然后第8行用bne指令判断a0是否为0,如果不是,则跳转到LOOP标签继续执行循环体。如果等于0,则分支指令bne不产生跳转,继续执行第9行指令。
第7章会讨论这些控制流指令如何在C/C++等高级语言中实现if-then、if-then-else和while语句的。
6.7.2 直接跳转指令 vs 间接跳转指令
直接跳转指令如beq a0, a1, L
,因为跳转的指令被定义在标签L,所以是直接跳转指令,目标地址值会被直接编译到机器指令中。间接跳转指令,顾名思义,目标地址是以非直接的方式给出的,通常保存在内存中的某个位置或者保存在寄存器,这样的指令都叫做间接跳转指令,如jr rs1
,地址被保存在寄存器rs1中,通过修改rs1的值可以任意更改目标地址。
前面说过,直接跳转指令目标地址会被直接编译到机器码中,在RV32I体系中,地址是32位,所以可以用32位的值表示目标地址,但是加上指令和其他操作数,长度就超过了32位,所以,实际实现中,只用了12位来表示目标地址,这12位表示的值代表了当前程序计数器的偏移量。只是用12位还有一个考虑,就是避免程序执行过程总跳到太远的位置。
6.7.3 无条件跳转指令
非条件指令,或者叫强制跳转指令,没有判断条件,即无条件调转到指定的目标地址。例如j L
,只要执行到这条指令,那么程序接下来一定会跳转到标签L处执行。再看一个例子:
第2行指令跳转到第6行FOO标签处继续执行,所以第3和第4行指令不会被执行。 RV32I中提供了多个无条件指令,如下表:
指令 | 描述 |
---|---|
j lab | 跳转到lab标签地址处(伪指令) |
jr rs1 | 跳转到寄存器rs1指定的地址(伪指令) |
jal lab | 首先将PC+4的地址值存储到返回寄存器ra,然后跳转到lab标签处(伪指令) |
jal rd, lab | 首先将PC+4的地址值存储到返回寄存器rd,然后跳转到lab标签处 |
jarl rd, rs1, imm | 首先将PC+4的地址值存储到返回寄存器rd,然后跳转到imm+rs1得到的地址 |
ret | 跳转到返回寄存器ra保存的地址(伪指令) |
表6.11 无条件跳转指令
再看一段代码:
首先第1行指令将当前PC+4的地址值(0x8000+4=0x8004)存储到寄存器ra,然后跳转到FOO标签地址处继续执行,执行到第8行,jr指令跳转到ra寄存器保存的地址值0x8004,执行sub指令,然后执行到第3行,有出现一个跳转指令jal,将当前PC+4(0x800C)的值保存到ra寄存器,跳转到FOO,执行到第8行后,jr又跳转到ra(0x800C)地址执行,也就是第4行mul指令。
需要指出的是jarl rd, rs1, imm
指令是一条间接跳转指令,因为目标地址由寄存器rs1和立即数imm共同组成。
j、jr和ret指令都是伪指令,最终都会被编译器翻译位jarl指令。如jr rs1
被翻译为jarl zero, rs1, 0
,ret
被翻译为jal zero ra, 0
;j lab
翻译为jal zero, lab
,注意,在RV32I体系中任何保存到zero寄存器的值都会被丢弃。
6.7.4 系统调用
用户程序通常需要用的输入输出操作,如执行完一些操作后需要打印一些信息,或者将信息写到文件中。 输入输出操作通常由输入输出设备来操作,如键盘、鼠标、显示器、打印机、网卡等。这些硬件通常由操作系统直接管理,为了便利性和可移植性,操作系统通常提供一系列的抽象接口和服务例程,让用户可以方便的编写操作代码。这种情况下,用户程序的输入输出操作需要用到操作系统的系统服务程序。 RV32I提供了ecall
指令来支持这种调用系统服务的操作。
假设用户想要通过系统调用在屏幕上打印一些信息。系统调用指令需要用到3个参数:文件描述符、包含要打印信息的缓存地址、需要打印的字节数量。 文件描述符是一个用来代表文件和设备的一个整数。在Linux系统中,整数1通常被用来代表标准输出stdout,它通常代表的是显示终端。接下来的一段代码展示将msg地址处的字符带引到文件描述符1(也就是显示终端)的过程。:
首先代码6-8行设置系统调用参数,a0存储文件描述符1(stdout),a1存储msg标签指定的字符缓存地址,a2存储的是要输出的字节数14。第9行a7保存值64代表接下来ecall系统调用指令在进入系统内核需要调用的服务号,也就是write服务号。
6.8 条件置位指令
与条件控制流指令类似,RV32I还提供了一组条件置位指令set。它会通过判断指令中给定的条件然会对目标寄存器置0或1。
指令 | 描述 |
---|---|
slt rd, rs1, rs2 | 如果rs1值小于rs2(有符号),则rd置1,否则置0 |
slti rd, rs1, imm | 如果rs1值小于立即数imm(符号扩展后),则rd置1,否则置0 |
sltu rd, rs1, rs2 | 如果rs1值小于rs2(无符号),则rd置1,否则置0 |
sltui rd, rs1, imm | 如果rs1值小于立即数imm(无符号),则rd置1,否则置0 |
seqz rd, rs1 | 如果rs1值等于0,则rd置1,否则置0(伪指令) |
snez rd, rs1 | 如果rs1值不等于0,则rd置1,否则置0(伪指令) |
sltz rd, rs1 | 如果rs1值小于0,则rd置1,否则置0(伪指令) |
sgtz rd, rs1 | 如果rs1值大于0,则rd置1,否则置0(伪指令) |
表6.12 条件置位指令
6.9 数字溢出检查
RISC-V并没有提供算术运算中数字溢出的指令,然鹅,固定的条件分支和条件置位指令可以用来检测是否有数字溢出。下面的代码展示了怎样使用这两种指令来检查数字溢出:
也可以使用条件置位指令:
这个例子中,a0 = a1 + a2,然后判断a0是否小于a1,如果是,那么判定发生了溢出,否则就没溢出。
下面的例子展示了如何判断两个有符号数相加后结果是否溢出: