TestOS移植K210开发板
概述
本⽂介绍前六个部分在移植K210开发板遇到的问题,第七章⽐较⿇烦,就放弃了。这⾥选⽤的原因是提供了K210的使⽤教程,同时也提供了相应的适配,使得我能够在⼏乎不改变内核代码的情况下进⾏移植,所以还是感谢rCore教程的作者、RustSBI的作者洛佳⼤佬以及借我开发板的ccc⼤佬。
内容
第⼀部分
第⼀部分没什么需要修改的,主要是学习开发板的使⽤。⾸先将开发板使⽤Type-C的数据线连上电脑。由于我编译和烧录使⽤的是虚拟机,所以需要映射USB端⼝,VirtualBox启动系统后窗⼝右下⾓有个USB的图标,右键点击,选择那个中⽂拼⾳的端⼝打上勾,就映射成功了。然后下载,把项⽬⾥的kflash.py下载下来。为了获得K210的输出信息,需要安装串⼝通信的Python库:
pip3 install pyserial
修改makefile:
rustsbi-k210_size := 131072
k210_serialport := /dev/ttyUSB0
k210:
riscv64-unknown-elf-gcc os.c printf.c entry.S -T linker_k210.ld -ffreestanding -nostdlib -g -o os_k210 -mcmodel=medany
riscv64-unknown-elf-objcopy os_k210 --strip-all -O binary os_k210.binpython怎么读取串口数据
@cp rustsbi-k210.py
@dd if=os_k210.bin py bs=$(rustsbi-k210_size) seek=1
@py os_k210.bin
@sudo chmod 777 $(k210_serialport)
python3 kflash.py -p $(k210_serialport) -b 1500000 os_k210.bin
python3 -ls.miniterm --eol LF --dtr 0 --rts 0 --filter direct $(k210_serialport) 115200
除了编译之外,需要建⽴⼀个⼆进制映像⽂件,包含rustsbi-k210.bin和编译出的os_k210.bin。具体操作是先将rustsbi-k210.bin复制⼀份,然后使⽤dd命令将其和编译出的os_k210.bin组合成⼆进制映像⽂件,注意os_k210.bin必须放在新⽂件⾥偏移量为rustsbi-k210_size的位置,rustsbi-k210_size的值是将rustsbi-k210.bin的⽂件⼤⼩按4096字节向上对齐的结果。K210版的链接脚本也有区别,需要修改⼀下内核的起始地址,因为rustsbi-k210强制指定了从0x80020000这⾥启动内核。
接着通过kflash.py把映像烧录到端⼝设备⽂件,在我的系统上这个设备⽂件是/dev/ttyUSB0。在烧录的时候终端会出现⼀个[INFO] Trying to Enter the 的提⽰,这个时候需要按住K210开发板上的Boot按钮,才能识别到开发板。识别过后就会开始烧录,烧录完就可以运⾏
了。我在识别和烧录的过程中都有可能发⽣连接失败的错误,不知道是数据线、接⼝的问题还是开发板都会这样。
运⾏的过程中可以按K210开发板上的Reset按钮重启内核,终端上的输出有时候会混乱或缺失,我猜测应该是串⼝通信的刷新率问题,毕竟这⾥不像qemu那样是直接输出,K210的输出传到终端还是需要⼀定时间的,运⾏太快⽽丢失输出信息也不是不可能。内核运⾏结束后如果正常调⽤RustSBI的关机功能会输出:
[rustsbi] reset triggered! todo: shutdown all harts on k210; program halt. Type: 0, reason: 0
关机以后也可以按Reset按钮重启内核,关机以后串⼝通信程序仍在运⾏,需要⼿动退出。退出的快捷键是Ctrl+](⼀开始没看到程序启动时的提⽰,以为只能⽤kill命令强制退出 )。
第⼆部分
第⼆部分开始有⽤户程序了,不过⽤户程序的链接脚本不需要修改,反正这个从哪执⾏都是⾃⼰指定的。值得注意的是⽤于导⼊⽤户程序的汇编代码link_app.S,之前是参考的rCore教程,前三⾏为:
.align 3
.section .data
.global _num_app
但是上了板之后会报内存读取不对齐的错误:需改成:
.section .data
.global _num_app
.align 3
按照rCore教程的解释,K210⽐qemu多的⼀个限制是读取多少字节类型的数据就要求这个数据的地址按多少字节对齐。⽐如读取int就要求这个int按4字节对齐。这段汇编代码下⾯就声明了⼀个.quad(64位,8字节)类型的_num_app变量,所以C程序在读取这个变量的时候要求其地址按8字节对齐。但令我疑惑的是,rCore⾥不管是qemu和K210⽤的都是上⾯的版本,两者都可以正常运⾏;但是我如果使⽤上⾯的版本,K210上就会报错。使⽤objdump查看符号表发现gcc编译前⼀个版本_num_app的地址是不按8字节对齐的,⽽rustc编译前⼀个版本则有对齐,感觉这是两个编译器的⾏为差别:在汇编代码中把变量放在段声明的正下⽅,rustc就直接把变量放在段的最前⾯了,⽽gcc在变量前⾯仍有可能插⼊其他的变量,所以必须在需要声明的变量前指定对齐才保险。
第三部分
第三部分需要修改timer.c⾥的时间频率常数:
#ifdef K210
#define CLOCK_FREQ (403000000 / 62)
#else
#define CLOCK_FREQ 12500000
#endif
第四部分
第四部分也没有什么特别的,就是原程序⾥task.c最后没有初始化current_task这个变量,这会导致内核在K210板上⽆法运⾏。这就是虚拟机和硬件的区别了,虚拟机可能会将未初始化的内存置为0,⽽硬件不会,未初始化的内存的值是随机的。如果要保险的话,还是应该像教程那样在程序的⼀开始就⼿动将bss段全部置为0,保证所有未初始化的全局变量值都为0。
第五部分
第五部分是坑了我最久的,因为这个bug实在是太难发现了,虽然实际原因可能对许多熟悉嵌⼊式开发的⼈来说是基本常识,但我这样的新⼿却完全不了解,这⾥总结⼀下调试的经过:
第⼀阶段:运⾏以后报panic,但是有时候是内核态的panic,有时候是机器态的panic,每次panic的原因和错误代码地址还不⼀样,时⽽是读缺页,时⽽是写缺页,时⽽是未知的系统调⽤,时⽽是⾮法代码。⽽错误代码地址对应的代码有时候甚⾄还和错误原因对不上,⽐如明明是store指令却报个读异常。经过观察,我发现发现异常的程序总是在主程序调⽤两次exec后出现,⽽且在内核添加打印函数还会改变错误类型。根据以往的经验,打印函数能够改变错误的基本只有两种情况:初始化和数据竞
争。但是把bss段全部置为0和关闭时钟中断后仍未解决问题,关键是在qemu下从未报错,K210上没法⽤gdb(开发板单步调试需要专门的硬件,我没有),看错误的代码反⽽让我更摸不着头脑。
第⼆阶段:决定使⽤笨⽅法,就是对正常运⾏的第四部分⼀步步往第五部分修改,直⾄复现该错误。这⼀阶段也是改变了调试的思路,从错误结果反推原因转变为从代码反推原因,因此不再关注于错误的类型,⽽是只关注“是否出错”这⼀结果。在加⼊exec之后,错误⼜出现了,说明这个bug和fork什么都没有关系,仅和exec有关。我⼜尝试在执⾏第⼀个⽤户程序前先执⾏两次user_init函数(exec调⽤的处理⽤户任务块的函数),发现不会报错,说明必须进⼊⽤户态,然后我⼜令程序在第⼀次exec进⼊内核态后调⽤两次user_init函数,⼜出错了,只调⽤⼀次就不出错。说明错误发⽣的条件⼀个是需要进⼊⼀次⽤户态,⼀个是需要调⽤两次user_init,那么bug就只能在第⼆次user_init之中。
第三阶段:第⼆次user_init包括解映射第⼀次user_init映射的⽤户物理内存,再重映射新的⽤户物理内存。按照xv6实验的经验,出错往往在删除再更新的时候,于是我设置解映射的时候不释放物理内存,程序不报错了。那么为什么不能释放内存呢?我发现不释放内存时新申请的物理页号是连续的,于是猜想⽤户内存对应的物理页号必须连续。因此我修改了⼀下物理页帧的分配代码,改成在释放页帧的时候,如果被释放的页号和当前使⽤过的最⾼页号相邻,则不将该页号放⼊分配池,⽽是直接令最⾼页号⾃减⼀。分配页帧时也不经过分配池,⽽是直接分配最⾼页号,这样就可以保证多次分配的页帧是连续的。经此⼀改,程序能够正常运⾏了,bug貌似得到了解决,但是⽤户物理页号必须相邻,这个
要求怎么想都很奇怪……
第四阶段:我⼜尝试分配页号的时候每分配⼀个就跳过⼀个页号(即只分配偶数页号或奇数页号),程序却没有报错,前⾯得出的结论不攻⾃破了,真正的凶⼿另有其⼈。我决定再细化错误条件,⽤来测试的⽤户程序内存占三个页,我就看看哪个页释放会报错,最后锁定了⼀个物理页号,当⽤户程序的第⼀个虚拟页映射到该页号时就会出错,⽽其他虚拟页映射到这⾥则不会。仔细观察这个物理页号的分配历史,发现它第⼀次被分配时刚好被映射到了第⼀个执⾏的⽤户程序的第⼀个虚拟页。设第⼀个执⾏的⽤户程序为A,经过两次user_init重置后的⽤户程序为B,经过实验⼜发现,当A和B的其他虚拟页映射到该物理页号不会出错,⽽A和B的第⼀个虚拟页映射到其他相同的物理页号亦会出错。这样,问题就被锁定在了两个⽤户程序的第⼀个虚拟页了,发现都是代码段所在的页。结合虚拟机上不出错,硬件上才会出错的现象,bug终于露出了它的真⾯⽬:缓存未刷新!
在exec函数末尾加上⼀⾏代码:
asm volatile ("fence.i");
问题解决,fence.i指令⽤于刷新指令缓存。现在来解释为什么bug会引发上⾯的这些情况:
虚拟机由于所有的易失性存储器件都是由内存模拟的,所以通常没有实现缓存机制。qemu我没看过源
码,但我看过spike(riscv-isa-sim)的源码,确实没有缓存机制,想必qemu也是如此,⾃然没有缓存刷新问题,⽽硬件不⼀样,因此虚拟机上能正常运⾏,K210上就会报错。
指令缓存和数据缓存是独⽴的,⽽重映射过程中对数据的读写只改变了数据缓存。同时指令缓存是通过指令的物理地址进⾏寻址的,所以如果前⼀个程序代码段的物理页帧被映射给了另⼀个程序代码段,且这两个程序的代码段实际内容不⼀致,指令缓存⾥的数据就会和实际内存内容不⼀致。
我物理页号分配池⽤的是栈,假设第⼀个程序接受的物理页号是1、2、3,重映射回收后进栈顺序是1、2、3,出栈顺序就变成3、2、1了,所以第⼀次user_init映射的顺序是3、2、1,之后重映射回收后进栈顺序是3、2、1,第⼆次user_init的顺序⼜变成1、2、3了,满⾜了出错条件,这就是上⾯需要经过两次user_init才会出错的原因。
因为是缓存,必须先写数据才会发⽣脏数据问题,所以必须进⼊⼀次⽤户态。
指令缓存没有刷新,那么CPU读取到的指令就完全是不可预测的,当然什么异常都有可能发⽣,也因此会出现错误和代码对不上的问题,毕竟我看的代码和缓存⾥的代码不⼀样。值得注意的是,我在第⼀阶段有尝试把代码段的内容以⼗六进制形式输出,没发现问题,这说明输出过程⾛的也是数据缓存,指令缓存只有CPU执⾏的时候才知道⾥⾯的值是多少,这也是这次调试过程中最⼤的难点。
第六部分
没有什么新的问题。
总结
这⼏个部分的移植给我的启发还是挺⼤的。⾸先是开发板的使⽤,这个应该和其他开发板是共通的,然后是硬件要注意的两个问题:初始化和缓存刷新(数据缓存基本不⽤怎么处理,tlb在切换页表时刷新过了,主要就这个指令缓存)。虽然是很简单的⼏个东西,但是长达数天的调试过程还是有很多值得复盘思考的地⽅。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。