qemu-system-riscv64运行操作系统微内核踩坑记

最近在弄移植大作业,目标是将学校课程中提供的一个操作系统内核(mips)移植到RISCV64位架构上。

我的qemu版本是5.2.0,OpenSBI版本是0.8。(最新的QEMU已经把OpenSbi更新到了0.9,不过无伤大雅)

设想之中的流程很简单:qemu上电跳转到bios->OpenSBI在M态完成boot第一部分->mret到S态,交给我的内核。

(实际上这个流程是我搞完这一堆才总结出来的…………)

从其他一些博客上参考了各色各样的运行指令后,我最终使用了以下指令来跑起我的内核:

qemu -S -s -machine virt -m 64 -bios default -nographic -kernel /path/to/kernel.elf

这样我就使用了qemu自带的opensbi,跑起了我心爱的RISCV内核。

问题的出现

由于之后内核的迭代,需要建立起页表系统并且将内核的运行位置从实地址换到虚地址上;因此,我修改了我的链接脚本,将程序地址换到了虚空间中;但我想再次尝试运行时,qemu模拟器在打印出OPENSBI的一些基本信息后,就什么也不肯输出了。

不过,这里的错误是明显的。我将我的内核的基址从0x80200000更改到了0xffffffff80200000;在qemu中使用xp指令观察ram中的内容,可以发现0x80200000处空无一物;由此可以推断由于地址的更改,在使用-kernel选项后QEMU不会再将内核装载到正常的区域了。

那么既然-kernel选项不能再使用,我便上网查找了各式资料,最后找到了THU的一个RISCV64内核的教程作为参考。里面给出的指令如下:

qemu -machine virt -m 64 -bios default -nographic -s -S -device loader,file=/path/to/kernel.bin,addr=0x80200000

其中qemu的版本为5.0.0, OpenSBI版本0.6。

这个教程应该是肯定没错的,毕竟还有一堆THU的同学们上了这门课程;但是到了我这里完全行不通。

我试着用xp指令读取0x80200000,没错,内核确实被加载上去了;但是qemu就是运行不到那个地址。

万般无奈之下,我去咨询一位一周半就完成了内核移植任务,对OS有着丰富经验的学长;但他也没能从截图里看出什么端倪。

走投无路之下,我只好git clone opensbi, 开始啃代码。

问题原因的探索

当然,作业那么多,我不可能自己读完所有代码;因此我去网上简单搜索了一下,想借助别人的成果快速定位我想要找到的代码段;

别说,还真叫我找到两个:

RISC-V OpenSBI启动过程, 一个大致的总结,还附带一些环境配置啥的,对于我的需求来说基本够了;

OpenSBIの内部実装(boot~linux kernelを実行するまで)Google到的,总结的很全面,甚至直接定位到代码,虽然需要发挥想象力塞翻;

借助这两篇博文,以及我clone下来的opensbi原码,以及加载了qemu/pc-bios/opensbi-riscv64-generic-fw_dynamic.elf的gdb,我开始了我的探索。

具体的运行过程可以看上面的两篇博文,总之最后,我的目光锁定在了一段代码上:

里面的一些函数具体如下图所示:(实际上在qemu结合了不知道什么payload编译之后,这些函数都被展开了)

可以看到,这是一段从a2寄存器的地址中读取一些boot设置的代码。而运行到这里的代码自始至终就没动过a2寄存器,这个值又是从哪里来的呢?

实际上,QEMU上电后会跳转到0x1000,经过几行代码之后才会跳转到0x80000000处的OpenSBI,而这几行代码中就包括了设置a2为0x1028的指令。

在这里面读取的所有值中,我最关心的就是next_addr:这块内存会被加载到一个scatch结构体里,而那个结构体的next_addr属性就是在单hart、支持S模式下的OpenSBI的跳转地址。(之所以这么说是因为我没有看其他条件下if分支的走向,因此无法确定跳转地址是不是也存在那里,不代表一定不是;这段代码在init_cold_boot函数里)

加上偏移量之后,我们可以得到next_addr最开始的地址:0x00001038。这段地址在ROM里面,说明这应该是由Qemu负责注入的启动信息。

验证这一点很简单:xp 0x00001038.在使用-kernel选项下这个位置储存的是我再熟悉不过、再想念不过的数字:0x80200000。而在换成-loader选项之后,这个位置却只留下一片死寂的0x00000000.

那么,问题就出在Qemu的初始化上。

那么怎么修改呢?我已经花费了太多时间,实在不想再花费时间去读Qemu的长得要命源码了(之前谷歌了很久也没找到相关讯息);-loader也更改不了rom上的值;我思来想去,最后决定从我目前最为熟悉的地方开始:OpenSBI。

行动开始。

破局

  • 对qemu/pc-bios/opensbi-riscv64-generic-fw_dynamic.elf使用readelf和objdump,找出对应的源代码:

图中圈出的四行代码就是加载地址的核心代码。阅读源代码可知红圈上面的几行汇编是在检查信息是否正确加载;虽然会对安全性造成损失,不过在qemu的情况下,我们是可以牺牲掉这几行代码的。

  • 对qemu/pc-bios/opensbi-riscv64-generic-fw_dynamic.elf使用vim和xxd,找到对应的代码段;
  • 对代码段进行编辑后保存;

    在编辑之后我以两行自检代码为代价,硬生生地将0x80200000hard-code到了OpenSBI里面。
  • 对修改后的elf使用objcopy --strip-all -O binary ,将其转化为二进制文件;
  • 使用以下指令启动qemu:
    qemu -S -s -machine virt -m 64 -bios /path/to/qemu/pc-bios/opensbi-riscv64-generic-fw_dynamic.bin -nographic -device loader,file=/path/to/kernel.bin,addr=0x80200000
    问题解决。

后记

虽然修改二进制代码来hack掉OpenSBI的感觉很爽,但是这显然不是正确的方法。希望之后我能找到正确加载的方法。

我在如何让qemu运行起来我的内核上花的时间比移植花的时间还要多……各种百度谷歌gayhub,最后实在没招了才出此下策。

无敌的ida Pro 64在我试图用它分析RISC-V代码时还是倒了,从侧面证明了RISC-V体系结构的优秀的安全性。(雾

如果还使用-bios default选项的话,我们修改过的bios并不会被调用。所以说qemu到底把bios文件藏到哪里去了……

由于不是正常解决问题的方法,并且花费了大量时间捣鼓,因此起名为踩坑记。上一篇踩坑记还是我忘了MySQL密码,而网上只有老版本修改密码对应的记录和新版本的密码加密方式的博文,最后逼得我用老版本修改记录的方法将密码更新为了新版本密码加密方式加密的'123456',最后用123456登录进去时写的…………

2021.7.24新增:或许可以把Qemu内置的设备树导出来修改一番然后让Qemu强制加载该设备树?

2022.6.25新增:其实最简单的方法就是自己写个类似于bootstrap的东西,把内核从指定位置拷过来吧…… 不过都到这地步了直接用uboot不好吗

posted @ 2021-05-19 16:23  tadshi  阅读(3497)  评论(2编辑  收藏  举报