下面看一个程序如何编译以及在QEMU环境中运行,从1加到10。
.section.text
.globl_start
_start:
addit0,x0,0x1
addit1,x0,0x0
addit3,x0,0xa
loop:
addt1,t1,t0
addt0,t0,0x1
addt3,t3,-1
bnet3,x0,loop
nop
jrra
nop
用下面的命令进行编译,生成二进制代码:
riscv64-unknown-elf-c++ -c -nostdlib -nostdinc -static -g -Ttext 0x80000000 -o sum.elf sum.s
或者生成32位结构的代码:
riscv64-unknown-elf-c++ -nostdlib -nostdinc -static -g -Ttext 0x80000000 sum.s -o sum.elf -march=rv32i -mabi=ilp32
各个编译参数的解释:
-nostdlib -nostdinc 不需要标准库
-c 只进行编译和汇编,不进行链接
-static 静态编译,不做动态的特殊处理
-g 加入调试信息(符号表等),便于调试
-o 输出文件
-Ttext 0x80000000把代码段放到这个位置(模拟器刚刚启动的时候,会从0x1000的位置执行,这个位置的代码会把PC值指向0x8000 0000位置。这样就需要把用户的代码放到0x8000 0000位置。)(见下面的执行截图)
-march=rv32i 指定使用的RISC-V指令集
-mabi=ilp32 指定使用的abi结构(指令集是32位,不指定这个的话,会使用64位的abi,会不兼容。注意,这里使用的是默认64位的编译器。)
执行完上述5条指令后的寄存器的内容。
t0就是目标地址,即用户地址,通过jr t0跳入。
下面通过编译为32位的代码来看一下程序的执行过程(由于程序不需要任何操作系统的支持,会容易跑偏,所以要用远程gdb进行单条指令执行,观察寄存器的执行情况。)
上述代码保存为sum.s,调用:
riscv64-unknown-elf-c++ -nostdlib -nostdinc -static -g -Ttext 0x80000000 sum.s -o sum.elf -march=rv32i -mabi=ilp32
生成可执行文件:
生成的可执行文件能在RISC-V的Linux操作系统下执行,在Windows下显然无法执行。
可以使用objectdump来反汇编,看看可执行程序里面的内容:
使用QEMU模拟器开始装入可执行程序:
QEMU-system-riscv32 -m 2G -nographic -machine virt -kernel sum.elf -s -S
注意一定要带入-s -S参数。-s的意思是在QEMU中启动gdb server,端口号为1234,-S的意思是,完成装载之后,不要启动模拟的处理器,等待调试器接入。
另外-bios non代表的意思就是不需要装载qemu默认的bios,在模拟的平台上不需要这部分的信息。
启动gdb的调试器客户端:
riscv64-unknown-elf-gdb sum.elf
后面要跟上sum.elf,主要目的是两个,一个是gdb本身是64位的,需要告诉它调试的对象是32位的,另外一个是sum.elf里面有调试的符号表,在进入调试的时候能够看到被调试的代码。
输入调试目标:
target remote localhost:1234
之后可以开始调试,先把断点设置到0x8000 0000的位置,之后可以控制程序单步执行。
可以通过下面的一些指令来观察程序的执行行为。
gdb的一些命令:
break 设置断点,比如break *0x80000000
disas pc, pc+20 在一个范围内反编译
info reg 列出所有的寄存器
x /24i $pc 在一个地址之后反编译24条指令
x /24d $pc 在一个地址之后列出内存中24字节的内容
p $x1 打印某一个寄存器的内容
容易产生错误的地方:如果QEMU和gdb互相不能匹配的最大问题就是32位和64位不匹配。要保证32位的QEMU模拟器执行32位的程序,64位模拟器去执行64位的程序。在进行调试的时候也指定正确的位数。
下面是综合起来看一下的效果:
上面的那个是启动qemu和用户程序的,用命令行启动qemu等待连接,注意-s 和-S。下面是启动gdb,连接。先target remote localhost:1234。然后break *0x80000000。然后c,这个时候就会启动上面的qemu。然后碰到第一个断点,停下来。然后,只能si了,因为没有断点了。如果再continue的话会跑飞。
当前目录的.gdbinit文件可能会带来困扰,找一个没有.gdbinit的目录下面执行上面的目录,避开使用和kernel一样的目录。
上述执行的过程中都使用"打开命令行.cmd"打开的命令行窗口,然后可以用cd命令切换到别的目录。这个命令已经设置好一系列的环境了。
下面过程说明如何使用gdb和QEMU来调试监控程序:
使用命令行:
QEMU-system-riscv32 -M virt -m 32M -kernel .\kernel.elf -nographic -monitor none -serial tcp::6666,server -s -S
其中几个参数意义是:
在调试监控程序的时候,QEMU会开两个端口,一个端口用于串口通信(端口号:6666),一个端口用以gdb server程序的端口(端口号:1234)。实际QEMU在开始执行的时候,会首先打开串口的通信,然后再打开gdb server程序的端口。所以,如果没有一个终端程序来连到串口的话,QEMU是不会打开调试端口的。因此,为了能够对监控程序进行调试,需要把终端程序也启动起来。
第一步:使用上述命令来启动QEMU模拟器。
第二步:使用term终端程序来连接6666号端口。
QEMU模拟器的状态也发生了变化。
可以看到,tcp已经连接上了。但是,这个时候监控程序没有起来。因为QEMU还在等待gdb程序的连接。
第三步:下一步,使用gdb连接到QEMU的gdb sever。
注意,这里的命令是riscv64-unknown-elf-gdb kernel.elf,后面跟着对应的二进制代码。这样可以通知gdb的客户端程序是在服务器端调试那个程序。而且,riscv64-unknown-elf-gdb默认任务服务器端的程序是64位的,但是实际上监控程序是32位的。这里通过kernel.elf的参数明确告诉gdb客户端,需要调试的程序是32位的,这样双方才能够正确通信。
下图是gdb启动之后的情况。
这个时候就可以输入调试的目标了
target remote localhost:1234
可以看到,gdb使用localhost:1234开始调试。
这时,其它的两个窗口(QEMU窗口和term窗口)没有发生变化,因为这个时候QEMU并没有启动模拟的处理器(或者说虚拟机内部的处理器)。在gdb窗口中键入continue,启动QEMU中的模拟的处理器。
终端程序已经可以进行交互了:
关于-M的解释
可以看到,这一部分是制定对应的模拟机器的型号,在实验中其实不重要,因为并不需要外设。
转载自:https://lab.cs.tsinghua.edu.cn/cod-lab-docs/labs/5-riscv-assem/
更多回帖