learn-tech/专栏/计算机基础实战课/19RISC-V指令精讲(四):跳转指令实现与调试.md
2024-10-16 10:18:29 +08:00

15 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        19 RISC-V指令精讲跳转指令实现与调试
                        你好我是LMOS。

前面我们学习了无条件跳转指令但是在一些代码实现里我们必须根据条件的判断状态进行跳转。比如高级语言中的if-else 语句,这是一个典型程序流程控制语句,它能根据条件状态执行不同的代码。这种语句落到指令集层,就需要有根据条件状态进行跳转的指令来支持,这类指令我们称为有条件跳转指令。

这节课我们就来学习这些有条件跳转指令。在RISC-V指令集中一共有6条有条件跳转指令分别是beq、bne、blt、bltu、bge、bgeu。

这节课的配套代码,你可以从这里下载。

比较数据是否相等beq和bne指令

我们首先来看看条件相等跳转和条件不等跳转指令即beq指令和bne指令它们的汇编代码书写形式如下所示

beq rs1rs2imm #beq 条件相等跳转指令 #rs1 源寄存器1 #rs2 源寄存器2 #imm 立即数 bne rs1rs2imm #bne 条件不等跳转指令 #rs1 源寄存器1 #rs2 源寄存器2 #imm 立即数

上述代码中rs1、rs2可以是任何通用寄存器imm是立即数也可称为偏移量占用13位二进制编码。请注意beq指令和bne指令没有目标寄存器就不会回写结果。

我们用伪代码描述一下beq指令和bne指令完成的操作。

//beq if(rs1 == rs2) pc = pc + 符号扩展imm << 1 //bne if(rs1 != rs2) pc = pc + 符号扩展imm << 1

你可以这样理解这两个指令。在rs1、rs2寄存器的数据相等时beq指令就会跳转到标号为imm的地方运行。而rs1、rs2寄存器的数据不相等时bne指令就会跳转到imm标号处运行。

下面我们一起写代码来验证。在工程目录下我们需要建立一个beq.S文件在文件里用汇编写上beq_ins、bne_ins函数代码如下所示

.global beq_ins beq_ins: beq a0a1imm_l1 #a0==a1跳转到imm_l1地址处开始运行 mv a0zero #a0=0 jr ra #函数返回
imm_l1: addi a0zero1 #a0=1 jr ra #函数返回

.global bne_ins bne_ins: bne a0a1imm_l2 #a0!=a1跳转到imm_l2地址处开始运行 mv a0zero #a0=0 jr ra #函数返回
imm_l2: addi a0zero1 #a0=1 jr ra #函数返回

我们先看代码里的 beq_ins函数完成了什么操作如果a0和a1相等则跳转到imm_l1处将a0置1并返回否则继续顺序执行将a0置0并返回。然后我们再看下 bne_ins函数的操作如果a0和a1不相等则跳转到imm_l2处将a0置1并返回否则继续顺序执行将a0置0并返回。

我们在main.c文件中声明一下这两个函数并调用它们然后用VSCode打开工程目录按下“F5”键来调试情况如下所示

上图是执行“beq a0a1imm_l1”指令后的状态。由于a0、a1寄存器内容不相等所以没有跳转到imm_l1处运行而是继续顺序执行beq后面的下一条指令最后返回到main函数中。

函数返回结果如下图所示:

从图里我们能看到首先会由main函数调用beq_ins函数然后调用printf输出返回的结果在终端中的输出为0。这个结果在我们的预料之中也验证了beq指令的效果和我们之前描述的一致。

下面我们继续调试就会进入bne_ins函数中如下所示

上图中是执行“bne a0a1imm_l2”指令之后的状态。同样因为a0、a1寄存器内容不相等而bne指令是不相等就跳转。这时程序会直接跳转到imm_l2处运行执行addi a0zero1指令将a0寄存器置为1后返回到main函数中如下所示

上图中第二个printf函数打印出bne_ins函数返回的结果输出为1。bne指令会因为数据相等而跳转将a0寄存器置为1导致返回值为1这个结果是正确的。

经过上面的调试验证我们不难发现其实bne是beq的相反操作作为一对指令搭配使用完成相等和不相等的流程控制。

小于则跳转blt和bltu指令

有了bqe、bne有条件跳转指令后就能实现C语言 ==和 != 的比较运算符的功能。但这还不够,除了比较数据的相等和不等,我们还希望实现比较数据的大小这个功能。

这就要说到小于则跳转的指令即blt指令与bltu指令bltu指令是blt的无符号数版本。它们的汇编代码书写形式如下

blt rs1rs2imm #blt 条件小于跳转指令 #rs1 源寄存器1 #rs2 源寄存器2 #imm 立即数 bltu rs1rs2imm #bltu 无符号数条件小于跳转指令 #rs1 源寄存器1 #rs2 源寄存器2 #imm 立即数

和bqe、bne指令一样上述代码中rs1、rs2可以是任何通用寄存器imm是立即数也可称为偏移量占用13位二进制编码它们同样没有目标寄存器不会回写结果。

blt指令和bltu指令所完成的操作可以用后面的伪代码描述

//blt if(rs1 < rs2) pc = pc + 符号扩展imm << 1 //bltu if((无符号)rs1 < (无符号)rs2) pc = pc + 符号扩展imm << 1

你可以这样理解这两个指令。当rs1小于rs2时且rs1、rs2中为有符号数据blt指令就会跳转到imm标号处运行。而当rs1小于rs2时且rs1、rs2中为无符号数据bltu指令就会跳转到imm标号处运行。

我们同样通过写代码验证一下加深理解。在beq.S文件中我们用汇编写上blt_ins、bltu_ins函数代码如下所示

.global blt_ins blt_ins: blt a0a1imm_l3 #a0<a1跳转到imm_l3地址处开始运行 mv a0zero #a0=0 jr ra #函数返回
imm_l3: addi a0zero1 #a0=1 jr ra #函数返回

.global bltu_ins bltu_ins: bltu a0a1imm_l4 #a0<a1跳转到imm_l4地址处开始运行 mv a0zero #a0=0 jr ra #函数返回
imm_l4: addi a0zero1 #a0=1 jr ra #函数返回

blt_ins函数都做了什么呢如果a0小于a1则跳转到imm_l3处将a0置1并返回否则继续顺序执行将a0置0并返回。

接着我们来看bltu_ins函数的操作如果a0中的无符号数小于a1中的无符号数程序就会跳转到imm_l4处将a0置1并返回否则继续顺序执行将a0置0并返回。

我们还是用VSCode打开工程目录按下“F5”键来调试验证。下图是执行“blt a0,a1,imm_l3”指令之后的状态。

由于a0中的有符号数小于a1中的有符号数而blt指令是小于就跳转这时程序会直接跳转到imm_l3处运行执行addi a0zero1指令将a0寄存器置为1后返回到main函数中。返回结果如下所示

对照上图可以发现main函数先调用了blt_ins函数然后调用printf在终端上打印返回的结果输出为1。这个结果同样跟我们预期的一样也验证了blt指令的功能确实是小于则跳转。

我们再接再厉继续调试进入bltu_ins函数中如下所示

图里的代码表示执行“bltu a0a1imm_l4”指令之后的状态。

由于bltu把a0、a1中的数据当成无符号数所以a0的数据小于a1的数据而bltu指令是小于就跳转这时程序就会跳转到imm_l4处运行执行addi a0zero1指令将a0寄存器置为1后就会返回到main函数中。

对应的跳转情况,你可以对照一下后面的截图:

我们看到上图中调用bltu_ins函数传递的参数是3和-1应该返回0才对。然而printf在终端上输出为1这个结果是不是出乎你的意料呢

我们来分析一下原因没错这是因为bltu_ins函数会把两个参数都当成无符号数据把-1当成无符号数是0xffffffff远大于3。所以这里返回1反而是bltu指令正确的运算结果。

大于等于则跳转bge和bgeu指令

有了小于则跳转的指令我们还是需要大于等于则跳转的指令这样才可以在C语言中写出类似”a >= b”这种表达式。在RISC-V指令中为我们提供了bge、bgeu指令它们分别是有符号数大于等于则跳转的指令和无符号数大于等于则跳转的指令。

这是最后两条有条件跳转指令,它们的汇编代码形式如下:

bge rs1rs2imm #bge 条件大于等于跳转指令 #rs1 源寄存器1 #rs2 源寄存器2 #imm 立即数 bgeu rs1rs2imm #bgeu 无符号数条件大于等于跳转指令 #rs1 源寄存器1 #rs2 源寄存器2 #imm 立即数

代码规范和前面四条指令都相同,这里不再重复。

下面我们用伪代码描述一下bge、bgeu指令如下所示

//bge if(rs1 >= rs2) pc = pc + 符号扩展imm << 1 //bgeu if((无符号)rs1 >= (无符号)rs2) pc = pc + 符号扩展imm << 1

我们看完伪代码就能大致理解这两个指令的操作了。当rs1大于等于rs2且rs1、rs2中为有符号数据时bge指令就会跳转到imm标号处运行。而当rs1大于等于rs2时且rs1、rs2中为无符号数据bgeu指令就会跳转到imm标号处运行。

我们继续在beq.S文件中用汇编写上bge_ins、bgeu_ins函数进行调试验证代码如下所示

.global bge_ins bge_ins: bge a0a1imm_l5 #a0>=a1跳转到imm_l5地址处开始运行 mv a0zero #a0=0 jr ra #函数返回
imm_l5: addi a0zero1 #a0=1 jr ra #函数返回

.global bgeu_ins bgeu_ins: bgeu a0a1imm_l6 #a0>=a1跳转到imm_l6地址处开始运行 mv a0zero #a0=0 jr ra #函数返回
imm_l6: addi a0zero1 #a0=1 jr ra #函数返回

结合上面的代码我们依次来看看bge_ins函数和bgeu_ins函数都做了什么。先看bge_ins函数如果a0大于等于a1则跳转到imm_l5处将a0置1并返回否则就会继续顺序执行将a0置0并返回。

而bgeu_ins函数也类似如果a0中无符号数大于等于a1中的无符号数则跳转到imm_l6处将a0置1并返回否则继续顺序执行将a0置0并返回。

我们用VSCode打开工程目录按“F5”键调试情况如下

上图中是执行“bge a0a1imm_l5”指令之后的状态由于a0中的有符号数大于等于a1中的有符号数。而bge指令是大于等于就跳转所以这时程序将会直接跳转到imm_l5处运行。执行addi a0zero1指令将a0寄存器置为1后就会返回到main函数中。

对照下图可以看到调用bge_ins(4,4)函数后之后就是调用printf在终端上打印其返回结果输出为1。

因为两个数相等所以返回1这个结果正确也验证了bge指令的功能确实是大于等于则跳转。

下面我们继续调试就会进入bgeu_ins函数之中如下所示

上图中是执行“bgeu a0a1imm_l6”指令之后的状态。

由于bgeu把a0、a1中的数据当成无符号数所以a0的数据小于a1的数据。而bgeu指令是大于等于就跳转这时程序就会就会顺序运行bgeu后面的指令“mv a0zero”将a0寄存器置为0后返回到main函数中。

可以看到意料外的结果再次出现了。你可能疑惑下图里调用bgeu_ins函数传递的参数是3和-1应该返回1才对然而printf在终端上的输出却是0。

出现这样的情况跟前面bltu_ins函数情况类似bgeu_ins函数会把两个参数都当成无符号数据把-1当成无符号数是0xffffffff3远小于0xffffffff所以才会返回0。也就是说图里的结果恰好验证了bgeu指令是正确的。

到这里我们已经完成了对beq、bne、blt、bltu、bge、bgeu指令的调试熟悉了它们的功能细节现在我们继续一起看看beq_ins、bne_ins、blt_ins、bltu_ins、bge_ins、bgeu_ins函数的二进制数据。

沿用之前查看jal_ins、jalr_ins函数的方法我们将main.elf文件反汇编成main.ins文件然后打开这个文件就会看到这些函数的二进制数据如下所示

上图里的反汇编代码中使用了一些伪指令,它们的机器码以及对应的汇编语句、指令类型,我画了张表格来梳理。

有了这些机器码数据,我们同样来拆分一下这些指令各位段的数据,在内存里它们是这样编码的:

看完图片我们可以发现bqe、bne、blt、bltu、bge、bgeu指令的操作码是相同的区分指令的是功能码。

这些指令的立即数都是相同的这和我们编写的代码有关其数据正常组合起来是0b00000000110这个二进制数据左移1位等于十六进制数据0xc。看看那些bxxx_ins函数代码你就明白了bxxx指令和imm_lxxx标号之间包含标号正好间隔3条一条指令4字节其偏移量正好是12pc+12正好落在imm_lxxx标号处的指令上。

重点回顾

这节课就要结束了,我们做个总结。

RISC-V指令集中的有条件跳转指令一共六条它们分别是beq、bne、blt、bltu、bge、bgeu。

bne和beq指令用于比较数据是否相等它们是一对相反的指令操作搭配使用就能完成相等和不相等的流程控制。blt、bltu是小于则跳转的指令bge、bgeu是大于等于则跳转的指令区别在于有无符号数。这六条跳转指令的共性是都会先比较两个源操作数然后根据比较结果跳转到具体的偏移地址去运行。

这节课的要点我给你准备了导图,供你参考复习。

到这里我们用两节课的时间掌握了RISC-V指令集的八条跳转指令。正是这些“辛勤劳作”的指令CPU才获得了顺序执行之外的新技能进而让工程师在高级语言中顺利实现了函数调用和流程控制与比较表达式。

下节课我们继续挑战访存指令,敬请期待。

思考题

我们发现在RISC-V指令集中没有大于指令和小于等于指令这是为什么呢

别忘了在留言区记录收获,或者向我提问。如果觉得课程还不错,别忘了推荐给身边的朋友,跟他一起学习进步。