一、来写第一个算术运算指令
指的就是加减。
ADD:Rtype,寄存器类型, 例子:
add x5,x6,x7
x5=x6+x7
指令结构之前已经说过:
opcode为op,rs1,rs2为源寄存器地址(例子里为6:00110和7:00111),rd为目标位置寄存器地址(例子里为5:00101),这里可以理解为rd=rs1+rs2。
funct3取值000,funct7取值0000000,和opcode:0110011一起决定了指令类别。
可以观察到add和sub的区别就在funct7。
最后例子里的指令在内存中:
add x5,x6,x7
内容为:
0000000 00111 00110 000 00101 0110011
但还要注意字节序,因为有四个字节。
转成16进制方便阅读:
0x007302B3
高地址 00 73 02 B3 低地址
如果字节这样排,那就是小端排序。
对照指令结构自己分析一下即可。这就是add指令。
后面可以来练习写一段汇编来模拟运行并调试。
二.汇编和调试的相关文件
makefile定义的make过程,这个文件的作用可以看第五篇文章。
# Add
# Format:
# ADD RD, RS1, RS2
# Description:
# The contents of RS1 is added to the contents of RS2 and the result is
# placed in RD.
.text # Define beginning of text section
.global _start # Define entry _start
_start:
li x6, 1 # x6 = 1
li x7, 2 # x7 = 2
add x5, x6, x7 # x5 = x6 + x7
stop:
j stop # Infinite loop to stop execution
.end # End of file
rule.mk:
下面这段读到调试再来看:
include ../../common.mk
.DEFAULT_GOAL := all
all:
@${CC} ${CFLAGS} ${SRC} -Ttext=0x80000000 -o ${EXEC}.elf
@${OBJCOPY} -O binary ${EXEC}.elf ${EXEC}.bin
.PHONY : run
run: all
@echo "Press Ctrl-A and then X to exit QEMU"
@echo "------------------------------------"
@echo "No output, please run 'make debug' to see details"
@${QEMU} ${QFLAGS} -kernel ./${EXEC}.elf
.PHONY : debug
debug: all
@echo "Press Ctrl-C and then input 'quit' to exit GDB and QEMU"
@echo "-------------------------------------------------------"
@${QEMU} ${QFLAGS} -kernel ${EXEC}.elf -s -S &
@${GDB} ${EXEC}.elf -q -x ${GDBINIT}
.PHONY : code
code: all
@${OBJDUMP} -S ${EXEC}.elf | less
.PHONY : hex
hex: all
@hexdump -C ${EXEC}.bin
.PHONY : clean
clean:
rm -rf *.o *.bin *.elf
include …/…/common.mk
这一行告诉make包含一个叫做common.mk的文件,包含一些通用的变量或规则。
.DEFAULT_GOAL := all all: @${CC} ${CFLAGS} ${SRC} -Ttext=0x80000000 -o ${EXEC}.elf @${OBJCOPY} -O binary ${EXEC}.elf ${EXEC}.bin
这一部分定义了一个默认的目标all,它会在没有指定其他目标时运行。all目标使用CC编译器和CFLAGS选项编译SRC源文件,并使用-Ttext=0x80000000指定程序加载地址为0x80000000,生成一个可执行文件EXEC.elf。然后使用OBJCOPY工具将可执行文件转换为二进制文件EXEC.bin。
.PHONY : run run: all @echo “Press Ctrl-A and then X to exit QEMU” @echo “------------------------------------” @echo “No output, please run ‘make debug’ to see details” @${QEMU} ${QFLAGS} -kernel ./${EXEC}.elf
这一部分定义了一个伪目标run,它会在运行all目标后运行。run目标使用echo命令打印一些提示信息,然后使用QEMU模拟器和QFLAGS选项加载并运行可执行文件作为内核。
.PHONY : debug debug: all @echo “Press Ctrl-C and then input ‘quit’ to exit GDB and QEMU” @echo “-------------------------------------------------------” @${QEMU} ${QFLAGS} -kernel ${EXEC}.elf -s -S & @${GDB} ${EXEC}.elf -q -x ${GDBINIT}
这一部分定义了一个伪目标debug,它会在运行all目标后运行。debug目标使用echo命令打印一些提示信息,然后使用QEMU模拟器和QFLAGS选项加载并运行可执行文件作为内核,并开启-s和-S选项以便于调试。同时,在后台启动GDB调试器,并加载可执行文件和GDBINIT脚本。
.PHONY : code code: all @${OBJDUMP} -S ${EXEC}.elf | less
这一部分定义了一个伪目标code,它会在运行all目标后运行。code目标使用OBJDUMP工具反汇编可执行文件,并显示其源代码和汇编代码,并使用less命令进行分页浏览。
.PHONY : hex hex: all @hexdump -C ${EXEC}.bin
这一部分定义了一个伪目标hex,它会在运行all目标后运行。hex目标使用hexdump工具显示二进制文件的十六进制格式。
.PHONY : clean clean: rm -rf *.o *.bin *.elf
这一部分定义了一个伪目标clean,它会在指定为参数时运行。clean目标使用rm命令删除当前目录下所有的对象文件、二进制文件和可执行文件。
.PHONY指令告诉make这些目标不是实际的文件,而只是命令的名称。这样可以防止make跳过它们如果有同名的文件存在。
更细节的:
这里的debug为target,对象为all,下方使用的 :
@{QEMU} ${QFLAGS} -kernel ${EXEC}.elf -s -S &
@${GDB} ${EXEC}.elf -q -x ${GDBINIT}
是调试的内容也就是command, -s选项 和-S选项的意思可以了解一下
在 QEMU 中,"-s"选项表示启用一个GDB调试服务器,可以让GDB连接到QEMU模拟的目标机器并对其进行调试。具体来说,该选项会在QEMU中打开一个监听端口(默认是1234),等待来自GDB的连接请求。
一旦GDB连接到了QEMU模拟的目标机器,就可以使用GDB进行源代码级别的单步调试、断点设置、内存查看等操作。
而"-S"选项则表示在启动时暂停目标机器的执行,等待调试器连接。这个选项通常和"-s"一起使用,这样可以让GDB在连接到目标机器之前,先暂停目标机器的执行,等待调试器的指令。
使用"-s"和"-S"选项,可以方便地进行QEMU虚拟机的调试工作,特别是对于操作系统内核或嵌入式系统的开发者而言,这是非常有用的功能。
这就是我们交叉调试的开发环境。
@符号是一个shell命令行中的特殊字符,用于执行命令并将其输出作为另一个命令的输入。
在这个例子中,@符号用于执行QEMU和GDB命令。
&符号是一个shell命令行中的特殊字符,用于将命令放入后台执行,从而使终端不会被阻塞。
在这个例子中,它用于将QEMU放入后台执行,以便可以在GDB中进行调试。
-q选项是GDB的一个选项,它表示安静模式,即在运行GDB时不显示一些冗余的信息。
-x选项是GDB的一个选项,它允许在启动GDB时自动执行一些命令。
在这个例子中,它用于自动加载一个包含GDB命令的文件${GDBINIT},
从而在启动GDB时自动执行这些命令。
common.mk:
CROSS_COMPILE = riscv64-unknown-elf-
CFLAGS = -nostdlib -fno-builtin -march=rv32ima -mabi=ilp32 -g -Wall
QEMU = qemu-system-riscv32
QFLAGS = -nographic -smp 1 -machine virt -bios none
GDB = gdb-multiarch
CC = ${CROSS_COMPILE}gcc
OBJCOPY = ${CROSS_COMPILE}objcopy
OBJDUMP = ${CROSS_COMPILE}objdump
解释:-smp,单核。-nographic,无图像界面,-machine virt,一种机器类型。
QEMU被赋值:qemu-system-riscv32gdbinit文件:
display/z $x5
display/z $x6
display/z $x7
set disassemble-next-line on
b _start
target remote : 1234
c
具体内容什么含义我也不多说。b,c是断点,用于gdb调试。
set disassemble-next-line on是设置汇编级别的单步调试模式。display就是调试时会把这三个寄存器展示出来。
那么makefile的内容就讲到这,回到汇编。
# Add
# Format:
# ADD RD, RS1, RS2
# Description:
# The contents of RS1 is added to the contents of RS2 and the result is
# placed in RD.
.text # Define beginning of text section
.global _start # Define entry _start
_start:
li x6, 1 # x6 = 1
li x7, 2 # x7 = 2
add x5, x6, x7 # x5 = x6 + x7
stop:
j stop # Infinite loop to stop execution
.end # End of file
分析注释,对应上一节讲的汇编规则,.text属于指示(directive),.global _start 就是指示一个全局变量,前面在gdbinit文件里也有影子:
_start属于什么,标签,对应的后面三行汇编则是具体汇编指令。stop同理。.end也是指示,给汇编器看的。
j stop则是跳转到执行stop,自己跳转自己然后循环跳转,相当于死循环。
现在准备开始调试可以回去看刚刚的rule.mk文件的介绍了。
三、调试
在这里我遇到了一个问题,起初是make时出现:
查看源文件:
后面解决的方法是使用ubuntu系统重新下载解压得到了和windows下解压不同的文件。
可以看到这里makefile的实际内容还是指向build.mk但是由于Ubuntu与Windows解压方式可能有区别,导致得到的makefile实际文件不同,另外所谓缺失分割符的问题也可能是windows下空格与tab的混淆导致的。
最后make生成了两个文件。一个elf文件一个bin文件,16字节的bin文件事实上就是:
这里起到的作用, 把无关的内容剥离了(调试等信息)
四条指令, ,每条指令32位四字节也就是最后16字节。
执行make code反汇编:
那么对这个汇编程序汇编得到的bin(机器指令)就介绍到这。
接下来运行:make debug:
如果是通过命令行安装各个程序并且没有下载工具包的,这里不会出现问题,如果下载了工具包可能会出一些问题,所以我一开始就推荐大家使用apt安装的方式。大家到这里做理论学习,开发环境也可也重新配置一遍,后面我可能会录屏给大家来一遍。总之不要钻牛角尖,学到东西才是最重要的。
可以看到断点在_start处:
至于x5为什么是0x80000000,也暂且不提,这里我们看到x6是0,断点下一句就是给x6赋值,我们输入si进行单步调试。(single instruction)
可以看到x6已经变成1了,下一步就是x7赋值了,直接敲回车就会重复si了。后续:
x5最后为3,并且得到下一步是无限死循环的提示。后面再按回车也无法退出,只能ctrl+C再输入quit。
调试和运行就写到这。
四、拓展必要概念
无符号数和有符号数
这个是很古老的问题了,计组里应该有讲到,这里给大家再提一下,也就是说,有符号的数字,表达范围绝对值就会缩小。因为要拿一位作为符号位。另外在计算机中有符号数用补码表示,这样才能进行快速的加减,这个也是计组内容,补码是什么,如果不清楚的小伙伴建议恶补计组知识。
符号拓展和零拓展
符号拓展就是比如-4的补码为1111100,而零拓展就是10000100。想知道具体一点可以自行了解。
后面自己尝试一下add加法负数的汇编:
# Add
# Format:
# ADD RD, RS1, RS2
# Description:
# The contents of RS1 is added to the contents of RS2 and the result is
# placed in RD.
.text # Define beginning of text section
.global _start # Define entry _start
_start:
li x6, 1 # x6 = 1
li x7, -2 # x7 = -2
add x5, x6, x7 # x5 = x6 + x7
stop:
j stop # Infinite loop to stop execution
.end # End of file
这一篇就到这里,下一篇继续。