代码实现仓库(包含实验五的复现方式):https://github.com/yumu20030130/arceos
通过这个实验,主要学习了ArceOS的Unikernel模式下如何加载用户程序并执行。之前是内核直接启动一个应用(该应用不可被加载,是和内核一起编译的),现在把这个应用变成一个loader程序,然后通过这个程序再去加载一个或多个外部的用户程序,且加载的是bin文件而非elf文件,直接省去了elf加载的细节,只需把整个文件都load到内存中,并设置正确的执行开始入口,即可开始运行。
这样是最基础且最简单的加载方式,通过这些实验,可以很好地理解,Unikernel下加载一个最简单应用需要做什么。
Debug方式
查看应用的elf文件
1 | riscv64-linux-musl-readelf -lw ./payload/hello_c/hello |
主要看text段,看该程序的汇编执行代码,以及都有哪些段,是怎么布局的
查看应用的bin文件
1 | xxd -l 6 ./payload/apps.bin |
和elf文件对照着看,大致了解elf文件到bin文件是怎么转换的。(可以发现,在有了字符串字面量之后,除了text段还会有数据段,放在bin文件的最开始,此时程序开始执行位置就不是bin文件的起始了,loader开始执行应用的起始地址需要修改)
查看qemu.log
可以看到实际运行了哪些汇编命令,具体设置看https://raw.githubusercontent.com/arceos-org/oscamp/refs/heads/main/tour_books/prepare_unikernel_for_linux_apps/README.md
练习1
练习一:之前是分配一个固定大小的虚拟地址空间(32字节)来load应用,现在要求在load应用时动态获取应用大小;
简单做法:设计一个Image头,在执行APP编译及入磁盘脚本时,把各个应用的大小放在PFLASH的最前面(在所有应用之前),loader程序一开始就先加载这部分内容,初始化一个ImageHeader对象。
1 | pub struct ImageHeader { |
练习2
练习二:支持连续加载两个应用(每个应用只有一条指令)并拼接起来运行。
简单做法:可以观察到,最简单的应用(仅含一条指令),编译出来的bin文件也就只有一条指令,理论上加载到连续的虚拟地址空间即可拼接运行,但是由于应用中有option(noreturn),会导致多两个00字节在bin文件最后,所以实际上,最后两个字节不要加载。
练习3
练习三:批处理方式执行两个单行代码应用,第一个应用的单行代码是nop
,第二个的是wfi
。
简单做法:在最外面套一层for循环就行了,每次只加载一个应用
练习4
练习四:支持一个新的系统调用
简单做法:仿照示例拓展一下就行了
练习5
练习五:应用hello_app通过ABI获取loader测(OS侧)提供的服务。
简单做法:
loader程序在跳转到用户程序之前,将ABI_TABLE(一个函数数组地址,数组元素是对应索引的服务函数地址)放在a7寄存器中,此处可以在一开始将a7寄存器保存下来,之后通过直接写rust代码来让编译器去规范寄存器使用,以逃避一些难以理解的寄存器误改。(注释处是之前存在问题的汇编代码)
效果:
可以多次调用hello、puts、terminal函数,即多次通过ABI方式调用OS提供的服务。此处,调用OS提供的服务实际上就是普通的function_call,而不是syscall,这也正是Unikernel需要的。
1 |
|
在上面的代码中,如果使用汇编代码,会发现,puts("puts")
在输出第二个字符时会出错,而如果换成多个单字符输出,而不是使用for循环,就不会有问题。
如果直接使用多次putchar,实际没有需要维护的寄存器上下文,直接写这种调用函数前没有保存寄存器的汇编是没问题的,而for循环使用了a1寄存器,此时没有保存a1寄存器,而callee不负责保存a1寄存器,可能直接更改(观察qemu.log,可以发现确实如此),从而导致之后for循环出问题。
在riscv标准中,caller负责保存及恢复a0a7、t0t6、ra寄存器,之后才是将参数放到对应的a0等寄存器。callee负责保存及恢复s0 ~ s11寄存器,即其他寄存器,callee都是默认已经保存,可以直接改的。
所以,如果使用rust编写的函数调用,编译器会根据当前的上下文,决定哪些寄存器需要保存,所以直接使用rust调用就没问题。
如果硬要自己写汇编,跳转到对应的ABI函数,最好就是按照riscv标准,在跳转之前保存好所有负责保存的寄存器,这样就也不会出错。
此外,可以将a7寄存器传递ABI_TABLE改为a0寄存器传递,这样给_start增加一个参数,就可以直接免去所有直接汇编书写,完全避免寄存器操作不当。