diff --git "a/source/_posts/2023\345\274\200\346\272\220\346\223\215\344\275\234\347\263\273\347\273\237\350\256\255\347\273\203\350\220\245\347\254\254\344\270\211\351\230\266\346\256\265\351\241\271\347\233\256\344\270\200\345\237\272\346\234\254\344\273\273\345\212\241\346\200\273\347\273\223\346\212\245\345\221\212" "b/source/_posts/2023\345\274\200\346\272\220\346\223\215\344\275\234\347\263\273\347\273\237\350\256\255\347\273\203\350\220\245\347\254\254\344\270\211\351\230\266\346\256\265\351\241\271\347\233\256\344\270\200\345\237\272\346\234\254\344\273\273\345\212\241\346\200\273\347\273\223\346\212\245\345\221\212" new file mode 100644 index 00000000000..70a5eb6a866 --- /dev/null +++ "b/source/_posts/2023\345\274\200\346\272\220\346\223\215\344\275\234\347\263\273\347\273\237\350\256\255\347\273\203\350\220\245\347\254\254\344\270\211\351\230\266\346\256\265\351\241\271\347\233\256\344\270\200\345\237\272\346\234\254\344\273\273\345\212\241\346\200\273\347\273\223\346\212\245\345\221\212" @@ -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,对于第二条要求,在第三条顺序执行加载的情况下可以得到保证。