-
Notifications
You must be signed in to change notification settings - Fork 387
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #198 from epochess/master
Create 2023开源操作系统训练营第三阶段项目一基本任务总结报告-epochess
- Loading branch information
Showing
1 changed file
with
148 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
--- | ||
title: 2023开源操作系统训练营第三阶段项目一基本任务总结报告 | ||
date: 2023-11-20 14:05:55 | ||
categories: | ||
- oscamp 2023fall arceos unikernel | ||
tags: | ||
- author: Epoche | ||
- 2023秋冬季开源操作系统训练营 | ||
- 第三阶段总结报告 | ||
--- | ||
|
||
# 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 的数据。 | ||
|
||
```python | ||
import struct | ||
|
||
# 定义头部结构 | ||
header_format = '>Q' | ||
header_size = struct.calcsize(header_format) | ||
|
||
# 读取原始应用程序二进制文件 | ||
with open('test.bin', 'rb') as f: | ||
app_data = f.read() | ||
app_size = len(app_data) | ||
|
||
# 计算应用程序大小 | ||
print(app_size) | ||
|
||
# 创建新的二进制文件 | ||
with open('test_app.bin', 'wb') as f: | ||
# 写入头部(包含应用程序大小) | ||
f.write(struct.pack(header_format, app_size)) | ||
|
||
# 附加原始应用程序数据 | ||
f.write(app_data) | ||
``` | ||
|
||
这是一个简单的 py 镜像处理脚本,对于一个单应用而言,头结构只需要存储一个整数,注意这里的头结构的数据存储的大小端顺序要和 loader 中解析 头结构 的代码要一致。这里选择大端序存储的 8 字节无符号整数,这样生成的镜像开头就会先存储一个 64 位的数据作为这个 app 的长度,可以用 xxd -ps 命令查看: | ||
|
||
```bash | ||
root@08e03dc057f5:~/phease3/test_app/test4# xxd -ps test_app.bin | ||
000000000000000e411106e422e00008730050100000 | ||
``` | ||
|
||
可以看到前 8 字节保存的数据为 app 的大小:14,在 loader 里面首先根据大端序读取镜像头,就可以知道 app 的实际大小了。 | ||
|
||
### Exercise 2 | ||
|
||
练习二需要在镜像中存在两个应用,完成练习一后思路便明朗了起来,直接在原先的 py 脚本上处理即可: | ||
|
||
```python | ||
- header_format = '>Q' | ||
+ header_format = '>QQ' #表示头结构存储了两个64位无符号整数(大端序) | ||
|
||
+ with open('nop.bin', 'rb') as f: | ||
+ app0_data = f.read() | ||
+ app0_size = len(app0_data) | ||
|
||
- f.write(struct.pack(header_format, app_size)) | ||
+ f.write(struct.pack(header_format, app_size, app0_size)) | ||
|
||
f.write(app_data) | ||
+ f.write(app0_data) | ||
``` | ||
|
||
在 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 的框架设计 | ||
|
||
![arceos arch](https://rcore-os.cn/arceos-tutorial-book/assets/ArceOS.svg) | ||
|
||
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 函数: | ||
|
||
```rust | ||
fn putchar(c: char) { | ||
let arg0 = c as u8; | ||
|
||
unsafe { core::arch::asm!(" | ||
li t0, {abi_num} | ||
slli t0, t0, 3 | ||
add t1, t0, a7 | ||
ld t1, (t1) | ||
jalr t1", | ||
abi_num = const SYS_PUTCHAR, | ||
in("a0") arg0, | ||
) } | ||
} | ||
``` | ||
|
||
接着在 main 函数中多次调用 putchar 函数,制作成镜像后在 arceos 中运行,会报 LoadPageFault 错误。在进行了艰难的 debug 后,发现 a7 寄存器在第一次调用后,其值就失效了,猜测也许是在 print 调用中等过程导致这个寄存器的值发生了更改,而内联汇编编译过后 app 也并没有自动保存这个调用者寄存器,最终在尝试下,发现可以先将 abi_table 的地址先取出来作为一个全局的静态变量 ABI_TABLE_ADDRESS 中,然后在 putchar 函数中,先 ld 这个值的符号, 将这个值存储在一个临时寄存器中,再进行后续计算: | ||
|
||
```rust | ||
fn putchar(c: char) { | ||
let arg0 = c as u8; | ||
|
||
unsafe { core::arch::asm!(" | ||
li t0, {abi_num} | ||
slli t0, t0, 3 | ||
ld t2, {abi_table} | ||
add t1, t0, t2 | ||
ld t1, (t1) | ||
jalr t1", | ||
abi_num = const SYS_PUTCHAR, | ||
abi_table = sym ABI_TABLE_ADDRESS, | ||
in("a0") arg0, | ||
) } | ||
} | ||
``` | ||
|
||
通过反汇编命令: | ||
|
||
```shell | ||
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](https://doc.rust-lang.org/reference/inline-assembly.html). | ||
|
||
在测试完多次 abi 调用没有问题后,封装了一个 puts 函数: | ||
|
||
```rust | ||
fn puts(s: &str) { | ||
s.bytes().for_each(|c| putchar(c as char)) | ||
} | ||
``` | ||
|
||
### Exercise 6 | ||
|
||
我们看一下练习六的要求 | ||
|
||
1. 仿照 hello_app 再实现一个应用,唯一功能是打印字符 'D'。 | ||
2. 现在有两个应用,让它们分别有自己的地址空间。 | ||
3. 让 loader 顺序加载、执行这两个应用。这里有个问题,第一个应用打印后,不能进行无限循环之类的阻塞,想办法让控制权回到 loader,再由 loader 执行下一个应用。 | ||
|
||
对于第一个要求:我们可以在练习二的基础上制作一个包含两个 app 的镜像,并可以在 loader 中读取 pfalsh 中的 app data。对于第三个要求顺序加载执行,我们的 parse 模块会解析每个 app 的起始地址和长度,按照练习三中提到的执行流程即可,且我们删掉了汇编中的 `j .` ,以及在练习六种我们加上了 `clobber_abi("C")` 来保证寄存器在调用前后都有效,以保证程序正常返回到 loader,对于第二条要求,在第三条顺序执行加载的情况下可以得到保证。 |