实验一 & 实验二
实验问题
练习 1:
main 函数中,固定设置 app_size = 32,这个显然是不合理甚至危险的。
请为 image 设计一个头结构,包含应用的长度信息,loader 在加载应用时获取它的实际大小。
执行通过。
练习 2:
在练习 1 的基础上,扩展 image 头结构,让 image 可以包含两个应用。
第二个应用包含唯一的汇编代码是 ebreak 。
如实验 1 的方式,打印出每一个应用的二进制代码。
解题思路
- 确定镜像头的内容
因为实验一二要将多个应用的二进制代码加载到 pflash 区域,所以为了更好的兼容和抽象,镜像的头应该包括:
- 应用程序的个数
- 每个应用程序的大小
- 每个应用程序的起始地址
因此,最终确定的应用程序头如下:1
2
3
4
5struct AppHeader {
apps_num: usize,
app_size: Vec<usize>,
app_start: Vec<*const u8>,
}
- 确定镜像的加载办法
关于镜像的加载实际上在老师的示例代码中已经给出了,需要特别注意的是:pflash 要求的镜像大小必须是 32M,所以需要先生成一个 32M 的全零文件,再将二进制文件 dd 进去。
解题方案
- 首先确定镜像头的
在外部形成 pflash.img 镜像,包含应用的个数,每个应用的大小,每个应用的二进制文件
这使用一个脚本文件生成,镜像的生成办法在代码仓库:https://github.com/Gege-Wang/hello_app。
在 loader.rs 中建立一个 Appheader 结构体,并且从 pflash.img 中将 AppHeader 还原出来。
根据AppHeader 的信息加载每个应用。
pflash |
---|
apps_num |
app1_size |
app2_size |
… |
appn_size |
app1.bin |
app2.bin |
… |
appn_bin |
需要解决的问题
在脚本文件中并没有指定二进制文件的顺序,这不合理,需要后期重构。
关于 rust 中各种类型的转换需要更加熟悉一些。
实验三
实验问题
批处理方式执行两个单行代码应用,第一个应用的单行代码是 nop ,第二个的是 wfi 。
解题思路
这个问题的关键是:如何执行第一个应用并且在第一个应用执行完成的时候开始执行第二个应用。 我在编译 nop 的时候发现应用大小是 4 ,然后内容是 [1, 0, 0, 0],但是我瞄到群里先做出来的人的截图,都不是这个二进制,我感到很困惑。后来我开始想第一个应用结束之后怎么返回到 load.rs 然后继续执行。jalr t2
之后会保存下一条指令的地址,所以在应用一执行完之后返回就能回到 load.rs 这条指令。虽然我用这个方法能够做出来这个问题,但是却为后来的问题埋下了巨大的麻烦,事实证明,从应用返回这个方法需要非常多需要考量的地方,甚至可以说直接从应用返回是非常不可取的。
解题方案
使用一个循环依次执行每个应用程序,在程序执行完之后加上 ret
指令返回。
需要解决的问题
或许要在外部实现一个批处理的程序,而不是由应用程序自己返回。
实验四
实验问题
本实验已经实现了1 号调用 - SYS_HELLO,2 号调用 - SYS_PUTCHAR,请实现 3 号调用 - SYS_TERMINATE 功能调用,作用是让 ArceOS 退出,相当于 OS 关机。
解题思路
虽然说这个好像是里面最简单的一个,但是我还是找了好几个来完成。我不太清楚 exit 和 terminate 是什么区别,其实我刚开始是用了 axtask 里面的 exit()来完成的,不知道退出号是什么,就那么随随便便退出了。感觉没什么底气,又开始搜索 terminate,还真的是有一个 axhal/misc 里面有一个terminate(),打印log 看起来比较正常了。
解题方案
仿照 abi_hello 和 abi_putchar 实现就可以了。
实验五
特别感谢 @PeizhongQiu 的技术支持。
实验问题
按照如下要求改造应用 hello_app:
- 把三个功能调用的汇编实现封装为函数,以普通函数方式调用。例如,SYS_PUTCHAR 封装为 fn putchar(c: char) 。
- 基于打印字符函数 putchar 实现一个高级函数 fn puts(s: &str) ,可以支持输出字符串。
- 应用 hello_app 的执行顺序是:Hello 功能、打印字符串功能、退出功能。
解题思路
封装成函数不困难,打印字符串也不困难,最困难的是第三个要求,完成这三项功能。
这里有一个重要的逻辑需要搞清楚。 ABI 是 Arceos 实现的,封装的函数是 应用 实现的。调用封装函数也是 应用 实现的。 整个的处理逻辑却是在 Arceos 的 load.rs 中实现的
从每一次 ABI 调用返回到 应用 的时候都需要返回到应用上一条指令的状态,这里有两个目的:一个是保存一些固定寄存器的值,比如 a7,另一个是保存封装函数的返回地址 ra。就像宏内核中系统调用需要保存上下文一样,ABI 调用的时候依旧需要保存上下文,这貌似是因为不是一个普通的函数调用,普通函数调用编译器会保存栈帧,不过这里需要自己保存,而按照怎样的规则保存是应用和操作系统约定的规范。
解题方案
我意识到要保存上下文,却没有想到一个好的办法, @PeizhongQiu 告诉我使用 clobber_abi(“C”),能够保存上下文,并且在返回的时候恢复,这是一个非常好的消息,能够不费吹灰之力之力的完成后面的工作。不过这条命令做的事情也是需要进一步掌握的。
需要解决的问题
要清楚保存上下文发生的细节,才能完成应用程序二进制兼容的事情,这个问题很重要。
实验六
实验问题
- 仿照 hello_app 再实现一个应用,唯一功能是打印字符 ‘D’。
- 现在有两个应用,让它们分别有自己的地址空间。
- 让 loader 顺序加载、执行这两个应用。这里有个问题,第一个应用打印后,不能进行无限循环之类的阻塞,想办法让控制权回到 loader,再由 loader 执行下一个应用。
解题思路
这里做完基础练习就没做什么改动就过了,为 APP 建立独立的页表,实现初始化和切换函数。在练习五的基础上进行加上独立的页表就可以了。
特别说明
所有的代码实现都在仓库https://github.com/Gege-Wang/arceos,欢迎交流并提出指导意见。
实验总结
非常开心参加 ArceOS unikernel 第三阶段的训练,在基础学习部分的训练大部分都是老师在指导书里给了详细的说明,石磊老师非常有耐心,助教老师也很给力,训练群里每个同学都很积极,让我感觉充满了动力。