Arceos unikernel summary
Exercise 1
实验中 payload 中的 app.bin 文件是通过一系列工具组合编译处理打包而来的,在实验一中,只是用 objdump 工具将 elf 文件中的一些调试信息和元信息删除掉,并通过 dd 命令生成一个32M 的空文件并将应用放在空文件的开始位置,在 arceos 中 loader app 运行的时候,将镜像写入给 qemu 的 pflash 供 loader 访问。参考练习提示:“可以为镜像设置一个头结构”,并考虑到后面的练习会有在一个镜像中写入多个 app 的操作,可以想到这个头结构是在 “镜像制作” 过程中被结构的,也就是可以在 dd 命令生成的空 32M 镜像上做文章,而不需要深入到 hello_app 的编译和链接过程。所以可以考虑写一个脚本,计算编译处理过后的 bin 文件的大小,并将其写入到 32M 的空文件头部,作为这个镜像的头结构,并在后续镜像内存中写入 app 的数据。
1 | import struct |
这是一个简单的 py 镜像处理脚本,对于一个单应用而言,头结构只需要存储一个整数,注意这里的头结构的数据存储的大小端顺序要和 loader 中解析 头结构 的代码要一致。这里选择大端序存储的 8 字节无符号整数,这样生成的镜像开头就会先存储一个 64 位的数据作为这个 app 的长度,可以用 xxd -ps 命令查看:
1 | root@08e03dc057f5:~/phease3/test_app/test4# xxd -ps test_app.bin |
可以看到前 8 字节保存的数据为 app 的大小:14,在 loader 里面首先根据大端序读取镜像头,就可以知道 app 的实际大小了。
Exercise 2
练习二需要在镜像中存在两个应用,完成练习一后思路便明朗了起来,直接在原先的 py 脚本上处理即可:
1 | - header_format = '>Q' |
在 loader app 中,解析出两个 app 的长度之后,即可计算出每个 app 的起始地址和长度,并打印相关数据即可。
Exercise 3
按照练习二的思路,制作一个包含两个 app 的镜像,loader 复制解析每个 app 的长度和将 pflash 上的 app data 拷贝到内存。并分批次的将 app 的数据拷贝到内核可执行地址空间,来逐个运行 app。hello_app 通过编译器处理后,在 arceos unikernel 中,运行 app 等价于函数调用,而编译器在编译 app 的时候已经处理好了调用栈以及 ra 寄存器等,所以只需要 jalr 到 app 的开头,等待程序运行无误后便可将控制权转交给 loader ,运行在实验二的基础上,需要修改内联汇编的代码,将最后一行 j .
删掉以保证程序正常运行。大致运行过程为从 loader app 将 hello_app 的内存载入到可读可写可执行的内存区域并跳转到该 app,运行完成后回到 loader app 接着这一流程载入并运行下一应用。
Exercise 4
根据 arceos 的框架设计
ax_terminate 功能在 axhal 模块被定义并通过 arceos_api 的 arceos_api::sys::ax_terminate 函数暴露给 app,loader app 要实现 terminate 调用需要引入 arceos_api crate,定义一个 abi_terminate 函数,在函数内部调用 arceos_api::sys::ax_terminate ,并在 abi_table 注册这个调用即可。这个实验实现较为简单,但是在这一步的时候 loader app 内部的 main 文件涵盖的内容太多了,所以就想到了模块化,新建了 parse 和 abi 文件,将处理镜像头结构的代码转移到 parse,abi 相关调用转移到 abi 文件。
Exercise 5
在做练习五的时候,首先先尝试多次 abi 调用,将实验五的 main 函数的代码独立出来成为 putchar 函数:
1 | fn putchar(c: char) { |
接着在 main 函数中多次调用 putchar 函数,制作成镜像后在 arceos 中运行,会报 LoadPageFault 错误。在进行了艰难的 debug 后,发现 a7 寄存器在第一次调用后,其值就失效了,猜测也许是在 print 调用中等过程导致这个寄存器的值发生了更改,而内联汇编编译过后 app 也并没有自动保存这个调用者寄存器,最终在尝试下,发现可以先将 abi_table 的地址先取出来作为一个全局的静态变量 ABI_TABLE_ADDRESS 中,然后在 putchar 函数中,先 ld 这个值的符号, 将这个值存储在一个临时寄存器中,再进行后续计算:
1 | fn putchar(c: char) { |
通过反汇编命令:
1 | cargo objdump --release --target riscv64gc-unknown-none-elf --bin hello_app -- -d |
反汇编发现 rust 若按照第一种方式,编译器会将代码编译为两次在一个寄存器中取值,然而这个值会在 abi 调用后失效。在交流群里询问才得知需要在内联汇编上加上一行 clobber_abi(“C”),这时编译器会帮你自动加上某个 abi 的 function call 的 caller-saved 的寄存器会在内联汇编被使用的提示,从而会保证程序上下文寄存器的值在函数调用前后都有效。具体的 ref: https://doc.rust-lang.org/reference/inline-assembly.html.
在测试完多次 abi 调用没有问题后,封装了一个 puts 函数:
1 | fn puts(s: &str) { |
Exercise 6
我们看一下练习六的要求
- 仿照 hello_app 再实现一个应用,唯一功能是打印字符 ‘D’。
- 现在有两个应用,让它们分别有自己的地址空间。
- 让 loader 顺序加载、执行这两个应用。这里有个问题,第一个应用打印后,不能进行无限循环之类的阻塞,想办法让控制权回到 loader,再由 loader 执行下一个应用。
对于第一个要求:我们可以在练习二的基础上制作一个包含两个 app 的镜像,并可以在 loader 中读取 pfalsh 中的 app data。对于第三个要求顺序加载执行,我们的 parse 模块会解析每个 app 的起始地址和长度,按照练习三中提到的执行流程即可,且我们删掉了汇编中的 j .
,以及在练习六种我们加上了 clobber_abi("C")
来保证寄存器在调用前后都有效,以保证程序正常返回到 loader,对于第二条要求,在第三条顺序执行加载的情况下可以得到保证。