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

200 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
22 RISC-V指令精讲访存指令实现与调试
你好我是LMOS。
上节课我们说了RISC-V是加载储存体系结构的典型只有加载指令和储存指令才有资格访问内存。
计算机运算完成的结果一开始会放在寄存器中但最终归宿还是内存此时就需要存储指令发挥作用了。这节课我们就来看看RISC-V提供的存储指令一共有三条分别是储存字节指令、储存双字节指令和储存字指令。
课程的代码你可以从这里下载。话不多说,咱们进入正题。
储存字节指令sb指令
我们先从储存字节指令即sb指令学起。
这个指令存储的字节单位是一个字节也就是8位数据。说得再具体一些这个指令会把一个通用寄存器里的低[7:0]位,储存到特定地址的内存单元里。而这个特定地址,要由另一个通用寄存器和一个立即数之和来决定。
储存字节指令的汇编代码,书写形式如下所示:
sb rs2,imm(rs1)
#sb 储存字节指令
#rs2 源寄存器2
#rs1 源寄存器1
#imm 立即数(-2048~2047
上述代码中rs1和rs2可以是任何通用寄存器。立即数imm为12位二进制数据其范围是-2048~2047。因为rs1、rs2以及立即数imm的规定对后面的sh指令和sw指令同样适用后面我就不重复说了。
sb指令完成的操作用伪代码描述是这样的
[rs1+imm]= rs2[7:0]
我来为你解释一下伪代码执行的操作。首先取得rs2寄存器第0位到第7位这8位数据即一个字节。然后把这个字节数据储存到rs1+imm为地址的内存单元中。
接着是代码验证环节为了方便调试我们在工程目录下新建一个store.S文件并在其中用汇编写上sb_ins函数。代码如下所示
.text
.globl sb_ins
#a0内存地址
#a1储存的值
sb_ins:
sb a1, 0(a0) #储存a1低8位到a0+0地址处
jr ra #返回
sb_ins函数我已经帮你写好了只有两条指令第一条指令把a1寄存器的低8位数据储存到a0+0地址处的内存单元中第二条指令就返回了。
现在我们一起用VSCode打开工程目录把断点打在“sb a1, 0(a0) ”指令处按下“F5”键调试一下效果如下图
图片里对应的是刚刚执行完sb a10(a0)指令之后执行jr ra指令之前的状态。这时候a0寄存器中的值是0x20a80这是byte变量的地址a1是0x80正是十进制数据128。
我们继续单步调试返回到main函数中执行printf函数打印一下byte变量的值如下图所示
从图中可以看到byte变量的初始值为-5。调用sb_ins函数时我们把byte的地址强制为无符号整数传给sb_ins函数第一个参数把整数128传给sb_ins函数第二个参数。
C语言调用规范告诉我们sb_ins函数会通过a0、a1寄存器传递第一个、第二个参数之后printf函数输出byte变量的值为128这证明了sb指令是正常工作的。
储存双字节指令sh指令
接下来要说的是储存半字指令,也是储存双字节指令。它可以把一个通用寄存器中的低[15:0]位一共16位的数据即两个字节储存到特定地址的内存单元中这个地址由另一个通用寄存器与一个立即数之和决定。
储存半字指令的汇编代码,书写形式是这样的:
sh rs2,imm(rs1)
#sh 储存半字指令
#rs2 源寄存器2
#rs1 源寄存器1
#imm 立即数(-2048~2047
sh指令完成的操作用伪代码描述如下所示
[rs1+imm]= rs2[15:0]
我来为你解释一下上面的伪代码执行了怎样的操作。首先取得rs2的第0位到第15位的数据。然后把这两个字节16位数据的数据储存到rs1+imm这个地址的内存单元中。
咱们写个代码来验证一下。在store.S文件中用汇编写上sh_ins函数。代码如下所示
.globl sh_ins
#a0内存地址
#a1储存的值
sh_ins:
sh a1, 0(a0) #储存a1低16位到a0+0地址处
jr ra #返回
与sb_ins函数一样sh_ins函数只有两条指令但第一条指令是把a1寄存器的低16位数据储存到a0+0地址处的内存单元中第二条指令同样是返回指令。
现在我们一起用VSCode打开工程目录在“sh a1, 0(a0) ”指令处打上断点按“F5”键调试的截图如下所示
图片对应的是刚刚执行完sh a1,0(a0)指令之后执行jr ra指令之前的状态a0寄存器中的值是half变量的地址a1寄存器中的值是0xa5a5。
我们继续进行单步调试返回到main函数中执行printf函数打印一下half变量的值。
如上图所示half变量的初始值为-1。随后调用sh_ins函数我们把half的地址强制为无符号整数传给sh_ins函数第一个参数再把整数0xa5a5传给sh_ins函数第二个参数之后printf函数输出half变量的值为0xa5a5。这证明了sh指令工作正常。
储存字指令sw指令
最后我们来学习一下储存字指令就是储存32位四字节指令也是最常用的储存指令它是把一个32位的通用寄存器储存到特定地址的内存单元中这个地址由另一个通用寄存器与一个立即数之和决定。
储存字指令的汇编代码书写形式如下所示:
sw rs2,imm(rs1)
#sw 储存字指令
#rs2 源寄存器2
#rs1 源寄存器1
#imm 立即数(-2048~2047
上述代码中rs1和rs2可以是任何通用寄存器。立即数imm为12位二进制数据其范围是-2048~2047。
然后我们看看sw指令完成的操作对应的伪代码描述如下
[rs1+imm]= rs2
这段伪代码执行的操作就是把rs2的32位数据即四个字节数据储存到rs1+imm为地址的内存单元中。
下面我们一起写代码验证一下在store.S文件中用汇编写上sw_ins函数。代码如下
.globl sw_ins
#a0内存地址
#a1储存的值
sw_ins:
sw a1, 0(a0) #储存a1到a0+0地址处
jr ra #返回
sw_ins函数只有两条指令第一条指令是把a1寄存器储存到a0+0地址处的内存单元中第二条指令同样是返回指令。
毕竟眼见为实咱们调试观察一下。用VSCode打开工程目录在“sw a1, 0(a0) ”指令处打上断点按下“F5”键调试如下所示
上图是刚刚执行完sw a1,0(a0)指令之后执行jr ra指令之前的状态。a0寄存器中的值是word变量的地址a1寄存器中的值是0执行完这个sw_ins函数后word变量的值应该变为0了。
我们继续单步调试执行返回到main函数中执行printf函数打印一下word变量的值如下图所示
可以看到图中word变量的初始值为0xfffffffff随后调用sw_ins函数我们把word变量的地址强制为无符号整数传给sw_ins函数第一个参数把整数0传给sw_ins函数第二个参数之后printf函数输出word变量的值确实为0。这证明了sw指令工作正常。
我们已经对sb、sh、sw指令进行了调试了解了它们的功能现在我们继续一起看看sb_ins、sh_ins、sw_ins函数的二进制数据。
打开终端切换到该工程目录下输入命令riscv64-unknown-elf-objdump -d ./main.elf > ./main.ins就会得到main.elf的反汇编数据文件main.ins我们打开这个文件就会看到上述这些函数的二进制数据如下所示
可以看到在图片里的反汇编代码中不但有伪指令还有两个字节的压缩指令。编译器为了节约内存所以会把指令压缩。比如说ret的机器码是0x8082sw a1,0(a0)机器码是0xc10c它们只占用16位编码即二字节。
截图里五条加载指令的机器码与指令的对应关系,你可以参考后面这张表格。
我画了示意图帮你拆分一下sb、sh、sw指令各位段的数据这样更容易看清楚它们是如何编码的。如下所示
对照上图可以看到sb、sh、sw指令的功能码都不一样借此就能区分它们。而这些储存指令的操作码都相同立即数也相同都是0这和我们编写的代码有关。
我还想提示你注意一下sw指令图片里的情况跟反汇编出来的数据可能不一致原因是编译器使用了压缩指令。图片里我还原的是sw a1,0(a0)正常的编码。
你可以手动在sw_ins函数中插入0x00b52023这个数据进行验证。怎么插入这个数据使之变成一条指令呢参考[上节课]还原lw指令的讲解我相信你这次自己也能搞定它。
关于RISC-V的三条储存指令已经介绍完了它们可以将字节、双字节、四字节储存到内存中去。实现了保存运算指令运算结果的功能给高级语言实现各种类型的变量提供了基础。
重点回顾
今天我们一口气学完了三条储存指令。有了三条储存指令加上我们上节课学过的五条加载指令就构成了RISC-V的访存指令。
RISC-V提供的储存字节指令、储存半字指令和储存字指令。储存指令可以把寄存器的运算结果或者其他数据储存到特定的内存空间中。储存单位可以是一个字节、两个字节或者四个字节。有了这些指令不同大小、位宽的数据处理起来都很方便。
运算指令的运算结果,要通过储存指令保存到内存中,这也给高级语言实现各种类型的变量,打下了基础。
我照例用导图梳理了这节课的要点,你可以做个参考。
思考题
为什么三条储存指令,不需要处理数据符号问题呢?
期待你在留言区跟我互动,也可以记录一下自己的收获。如果觉得课程还不错,也别忘了分享给更多朋友。