0%

前言

发了三周完成第二阶段的前三个实验,并进入到第三阶段,第三阶段一开始一周,在接下来的三周里奋起追赶进度,花了两周半完成了第一周的5个实验。

从接触arceos的unikernel整体架构设计,有以下感悟:

组件化设计,内核的所有依赖根据需要引入,包括组件内部的依赖,这个控制是到代码块级,粒度非常小,rust包管理和语法提供很好的支持

平台相关和平台无关的模块相互隔离,这一块解耦合有助于写平台无关模块的代码,提升代码的复用性

练习1和2

需要给应用设计一个头部,包含应用大小,根据大小来加载应用,而不是固定加载32字节。在实验过程中,单纯的添加头部,会导致qemu读取pflash读取失败,经过一番

尝试后发现,pflash一定要32M大小,大一点小一点都不行,然后就是修改应用端构建的命令。先添加头部,然后添加数据段,再加上32M空数据,

最后通过dd命令将整个文件的前32M拷贝到另一个文件,这个文件即为我们的最终文件。

头部设计:
应用数量|第一个应用大小|第二应用大小|….\n
第一个应用数据+第二个应用数据

头部通过\n和数据端区分,两个应用的数据无间隔,因为已经从头部得知应用大小了

练习3

练习3的难点在于如何执行玩应用程序,再返回到内核,这里有个细节,就是无返回 即 -> !,生成的汇编是没有ret的,我们需要ret指令会到内核,只需要改下

用户代码即可。

练习4

实现terminate,通过查看arceos的代码发现,arceos_api中有这块代码,问题就在于引入arceos_api,并调用对应的ax_terminate方法

练习5

比较难,发现只会输出hello world并报load page fault的错误或其他错误,通过排查发现连续调用两次hello world也会报错。通过qemu.log的汇编,

将a7寄存器保存到一个全局变量后,依旧存在错误。第二步,发现连hello_world也没有输出,怀疑应用内存布局的原因,导致start并没有放置在程序开头

位置,通过静态链接文件ld固定内存布局,解决问题。第三步,通过禁止内联消除寄存器被覆盖的现象,发现依旧报错。第四步,发现和编译器生成的调用函数汇编

有差异,无保存恢复寄存器的操作,仿照这部分代码,保存了ra寄存器。实验成功了!

实验6

实验6比较简单,两个用户根页表切换一下,可以一模一样,反正是顺序执行。把会陷入无限等待的代码改一下即可

总结

第三阶段更多的是实验,相比于阶段二,需要更多的探索和debugger的能力。通过第三阶段,我发现了前面学习中遗漏的知识,比如函数调用规范,外设的dbt文件和

cargo的包管理的细节等等。

发时间最多的是第一周的实验2,在不停的开启注释代码修改代码中,了解到看似无比复杂的依赖,其实可以被梳理清楚,到能运行时,依赖并不是很多,也就实现了

最小化原则。和arceos的设计有异曲同工的感觉,如果只用其中一部分功能,很多方法及依赖都不是必须的,且是可以替代的。

2023秋冬季开源操作系统训练营第三阶段总结报告

前言

在训练营第三阶段, 我选择了Unikernel项目, 不觉间四周已过, 训练营也步入尾声, 遂做个总结.

Unikernel 项目学习总结

week1

输出有颜色的字符

查阅资料得知, 只要在字符串两侧包裹\u001b[<color>m\u001b[0m就可以了

  • <color>为颜色数字, 比如红色为31
  • \u001b[31mhello world!\u001b[0m即可输出红色的hello world!

支持 HashMap 数据类型

思路

  • 一开始无从下手, 感谢老师在群里提示, 去看了标准库实现
  • 将标准库代码复制过来, 全部注释
  • 一点点放开注释, 缺啥补啥

Rust标准库的哈希表的一些具体内容

  • 底层: 对GoogleC++哈希表的包装
  • new: 用参数hashbuilder生成一个哈希表, 默认为RandomState
  • RandomState: 结构体, 保存两个随机数, 实现了BuildHasher Trait
  • BuildHasher: 可以根据key创建Hasher, 要求实现方法build_hasher() -> Hasher
  • Hasher: 一个Trait, 代表一种哈希算法, 可以根据key(字节流)返回哈希值, 要求实现方法write()finish()
    • write(): 往Hasher里写key
    • finish(): 结束写, 返回哈希值
  • build_hasher(): 生成Hasher, 默认的DefaultHasher是调用SipHasher13::new_with_key()生成的
    • DefaultHasher: 结构体, 保存了一个SipHasher13
    • SipHasher13::new_with_key(): 新建一个SipHasher13
    • SipHasher13: 一个Hasher, 即一种哈希算法的实现

内存管理

实现内存分配算法Early

  • 参考TLSF的代码, 在其基础上修改
  • 初始化页和字节分配器, 共用一块连续空间
  • 字节分配指针从前往后, 页从后往前
  • 指针相遇了就意味着内存耗尽
  • 页分配器不回收释放的内存, 指针一直往前
  • 字节分配器仅当所有分配的内存都释放了才回收
    • 初始化一个计数器为 0
    • 每次分配 +1, 释放 -1
    • 如果计数为 0, 就把指针移回起点

dtb

解析dtb

  • 群友有推荐使用hermit-dtb进行解析
  • 最后选择了名为dtbcrate

解析后获取相应信息

  • 遍历每个节点
  • 每个节点的reg属性都有4个值
  • 其中第一个和第三个是起始地址和大小
    • 目前还未找到相关规定, 姑且先当成结论

调度

fifo改造成抢占式, 最简单的实现:

  • task_tick()的返回值从false改成true即可
  • 类似于一个时片极短的RR

week2

练习 1 & 2

请为 image 设计一个头结构,包含应用的长度信息,loader 在加载应用时获取它的实际大小。

扩展 image 头结构,让 image 可以包含两个应用。打印出每一个应用的二进制代码。

实现

  • 给每个app的文件头都加上24字节的元信息, 如下
    • 魔数
    • app起始地址
    • app大小
  • 循环: 解析前24字节, 如果发现是app, 就将其内容打印出来, 否则退出循环

反思

  • 或许这样会更好: 学习inode, 将每个文件的元信息统一放在image开头, 而不是每个文件的开头

练习 3

批处理方式执行两个单行代码应用,第一个应用的单行代码是 nop ,第二个的是 wfi

思路

  • 从练习2继续, 循环读取app的代码
  • 每读取完一个app, 就跳转到该app指令处执行
  • 执行完毕后返回, 继续循环, 即读取下一个app的代码

实现

  • 尝试用jalr跳到app的指令处运行, 但是失败
  • debug发现问题和jalr无关, 而是app执行完毕后, 没有返回
  • 观察实验代码, 发现app的函数有noreturn的标志, 删去即可

练习 4

请实现 3 号调用 - SYS_TERMINATE 功能调用,作用是让 ArceOS 退出,相当于 OS 关机

照猫画虎, 模仿前几号系统调用实现3号即可

  • 使用的退出函数为arceos_apiax_exit()
  • axstd也有相关函数, 当时对arceos的结构了解尚浅

练习 5

把三个功能调用的汇编实现封装为函数, 基于 putchar 实现 puts 输出字符串。

思路

  • 在实验中已经将abi_table基址放在a7中传给了应用
  • 应用用一个全局变量保存基址
  • 每个函数根据该基址, 用汇编jalr跳转到系统调用函数执行
  • puts即循环调用putchar

遇到的问题: 调用puts时死循环

  • 观察gdb, 发现在puts的最后, 执行ret无法返回, 而是停留在原地
  • 检查发现, 原因:
    • riscv使用ra保存返回值, 但ra不是被调用者保存寄存器
    • 使用内联汇编跳转时, 编译器不会自动帮你加上保存ra的指令
    • puts循环调用putchar的过程中, ra被修改, 所以无法正常返回
  • 解决: 调用前将ra入栈, 调用后再出栈

练习 6

实现一个应用,唯一功能是打印字符 ‘D’。 现在有两个应用,让它们分别有自己的地址空间。 让 loader 顺序加载、执行这两个应用。

顺序加载和执行

  • 在练习3已实现

地址空间的分配: 最简单的实现思路

  • 实验过程中, 新建了一张页表, 映射虚拟地址0x4000_0000开始的1G空间
  • 应用起始地址为0x4010_0000
  • 既然应用是顺序执行的, 完全可以共用该页表
  • 将第一个应用加载到0x4010_0000处, 运行完毕后, 第二个应用直接覆盖上去即可

地址空间的分配: 加载全部应用后再执行的思路

  • 额外建立一张页表给第二个应用
  • 第一个应用映射0x4000_00000x8000_0000, 大小1G
  • 第二个应用映射0x4000_00000x8000_1000, 大小1G
  • 这要求第一个应用大小不能超过1页, 不然会和第二个应用重合

遇到的问题

  • 第二个应用无法映射成功, 每当访问该虚拟地址时均会产生pagefault
  • 感谢老师和同学@王瑞康的帮助, 告诉我这是riscv的规定
    • sv39的页表机制要求, 如果使用一级页表, 那么地址必须1G对齐
    • 如果想实现上述的映射, 必须用到三级页表
  • 将第二个应用的页表改成三级页表, 即可完成映射

感想与收获

对于arceos各个组件说的非常清晰

  • 初看arceos的代码时一团乱麻
  • 看过ppt之后才逐渐梳理清楚, 一图胜千言

第一次分析几千行的标准库代码

  • 起初打算自己实现哈希表, 要是真这么做肯定费力不讨好
  • 学会了如何借鉴和复用他人的代码

第一周的内容给我补了很多急缺的知识, 也加深了很多知识的理解

  • dtb的作用和结构
  • 现代内存分配算法
  • 调度算法
  • 页表机制

总之, 十分感谢能有这次机会参加训练营, 此间经历, 此生难忘.

2023秋冬季开源操作系统训练营第三阶段总结报告

前言

此为训练营第三阶段Unikernel项目最终任务的报告

  • 描述了我对arceos支持Linux原始应用以及支持多应用的实现思路以及验证过程

思路

支持 Linux 原始应用

在思路一的基础上支持思路二

思路一

  • 需要获得应用源码
  • 将应用和arceos一起静态编译

思路二

  • 应用不需要重新编译, 但必须动态编译
  • 实现一个和libc接口一致的动态库
  • 运行时, 使用一个动态链接器将应用重定位到该动态库
  • 该动态库必须知道arceos系统调用函数的地址
  • 动态库不使用中断, 而是用事先获得的地址, 直接跳转过去进行处理
  • 细节参考第二周的练习4

本思路

  • 应用不需要重新编译, 但必须动态编译
  • 实现一个和libc接口一致的库, 和一个动态链接器, 两者和arceos一起静态编译
  • 如此一来, 库的内部实现可以直接调用arceos函数
  • 应用装入内存时, 用动态链接器重定位到库函数

支持多应用

每个应用都有一个arceosTCB, 在其中保存一些关键信息

  • 应用有自己的内存空间, 所以需要保存自己的页表
  • 应用可以创建线程, 所以需要保存子线程的TCB
  • 多应用情况下, 必须进行内存的管理, 所以还需要保存应用申请的物理页

原理和实现

支持 Linux 原始应用

实现接口一致的库

这部分很简单

  • 对外接口和libc一致即可, 目的是让应用感知不到动态库被替换
  • 内部实现随意, 只要达到对应功能即可
  • 因为和arceos静态链接, 调用系统调用十分方便
  • 具体实现可以参考或者直接使用axlibc
  • 也可以”翻译”glibcmusl-libc

实现的库函数

参考 musl-libc 1.2.4 源码

对于一个C程序, 其启动流程如下

  • 内核将一些信息放入栈中, 随后跳转到程序入口_start
  • _start, 将当前sp作为参数, 跳转到_start_c
  • _start_csp里解析出argcargv, 跳转到__libc_start_main
  • __libc_start_main进行初始化工作, 完成后跳转到应用的main
  • main执行完毕, 回到__libc_start_main
  • __libc_start_main完成收尾工作

对于动态编译的程序

  • 上述提高的函数里, 仅有_start, _start_c, main
  • 当然可能还有其他函数比如_init, _fini等, 这些往往作为参数交给__libc_start_main进行处理
  • __libc_start_main本身是不包括在程序里的, 也就是说, 需要由我们实现

__libc_start_main的实现

  • 对于一个hello world程序而言, __libc_start_main的实现十分简单
  • 不需要进行初始化工作, 直接跳转到main即可
  • 也不需要进行收尾工作, 直接exit即可

动态链接器

动态链接器自举

  • 动态链接器装入内存的地址不确定, 需要自举, 即对自己重定位, 这是动态链接器最复杂的部分
  • 自举时无法使用标准库功能, 甚至不能进行函数调用
  • 但因为该动态链接器和arceos静态编译, 所以不需要自举

动态重定位

对于一个动态链接的应用

  • 函数调用的地址在编译期无法确定, 需要在运行时查询GOT来确定
  • GOT可以确定一个函数名到函数地址的映射
  • 装入时重定位: 应用在装入时, 动态链接器修改GOT, 将函数地址改成正确对应地址
  • 运行时重定位: 动态链接器将函数地址改成dl_runtime_resolve()的地址, 仅当应用调用函数的时候才修改GOT为正确地址

本实现里, 使用装入时重定位

  • 应用装入内存时, 修改GOT, 改成上文的”实现接口一致的库”的对应函数的地址
  • 如此一来, 应用调用库函数便会跳转到期望的地址执行

支持多应用

内存管理

arceos的设计

  • 只支持单应用, 内核以库的形式存在, 即libos
  • 应用拥有所有内存空间, 随意使用

为了支持多应用, 需要对应用进行内存管理

  • TCB里使用一个数组保存应用使用的物理内存
  • 当应用退出时, 回收所申请的内存

应用独立地址空间

arceos的设计

  • 只支持单应用, 因此不需要进行页表切换
  • 内核初始化时初始一张页表, 映射内核的数据
  • 将该页表作为根页表写入页表寄存器, 之后不再改动

为了支持多应用

  • 每个应用在建立的时候, 都为之新建一张页表, 保存在TCB
  • 页表初始化时, 也需要映射内核的数据
    • 对于sv39, 地址空间分为高256G和低256G
    • arceos在完成初始化工作之后, 将自己的运行地址设置在了256G
    • 因此应用运行在低256G即可, 高256G留给内核运行
  • 设计应用二进制数据的起始地址为0x10000, 重定位完毕之后, 将数据写入该地址
    • 申请应用和数据大小一致的物理内存
    • 将应用数据写入该物理内存
    • 在页表上将0x10000映射到该内存地址

支持页表切换

  • 调度时, 需要将切换页表, 将地址空间从上一个应用切换到下一个
  • 系统初始化创建main进程运行, 但是此时没有发生调度, 因此需要手动将页表切换成main的页表

将来的改进

完善库函数

  • 完善__libc_start_main, 初始化和收尾工作必须符合应用期望
  • 可以直接用成熟的__libc_start_main实现
  • 也可以直接”翻译”一个libc中的相关实现

支持运行时重定位

  • 目前支持装入时重定位, 在面对复杂应用时, 重定位工作很耗时间
  • 可以进一步改为运行时重定位, 减少应用启动时间

支持应用创建子线程

  • 可以在tcb中保存一个pid
  • 创建子线程时, 将pid设置为一致, 表示属于同一个进程

验证

过程

  • 下载riscv版本的Ubuntu, 使用qemu安装并运行
  • 在其中下载musl-libc源码, 编译
  • 创建两个hello world应用, 使用musl-gcc编译
  • 复制到arceos项目文件中, 运行arceos, 成功

练习 1

main 函数中,固定设置 app_size = 32,这个显然是不合理甚至危险的。

请为 image 设计一个头结构,包含应用的长度信息,loader 在加载应用时获取它的实际大小。执行通过。

思路:

  • 在制作bin文件的时候,先将应用的长度信息存入,在存入应用数据;从bin文件中读取的过程中,也按照同样的约定即可。
1
2
3
4
5
6
7
8
app_size=$(stat -c %s ./hello_app.bin)

printf "$(printf '%04x' $app_size)" | xxd -r -p | dd of=./apps.bin conv=notrunc bs
=1 seek=0

dd if=./hello_app.bin of=./apps.bin conv=notrunc bs=1 seek=2

mv apps.bin ../arceos-unikernel/payload/

练习 2

在练习 1 的基础上,扩展 image 头结构,让 image 可以包含两个应用。

第二个应用包含唯一的汇编代码是 ebreak

如实验 1 的方式,打印出每一个应用的二进制代码。

思路:

  • 使用1个字节记录应用数量,2个字节记录应用大小
  • app_num和app_size在烧录进bin文件时,先转换为十六进制,再转换为二进制写入bin文件,1个字节使用2个十六进制位表示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 创建二进制bin文件
dd if=/dev/zero of=./apps.bin bs=1M count=32

# 通用编译命令
cargo build --target riscv64gc-unknown-none-elf --release
rust-objcopy --binary-architecture=riscv64 --strip-all -O binary target/riscv64gc-unknown-none-elf/release/hello_app ./hello_app.bin

# 应用数量
app_num=2
printf "$(printf '%02x' $app_num)" | xxd -r -p | dd of=apps.bin conv=notrunc bs=1 seek=0

# 第一个应用
app_size=$(stat -c %s ./hello_app/hello_app.bin)
printf "$(printf '%04x' $app_size)" | xxd -r -p | dd of=./apps.bin conv=notrunc bs=1 seek=1
dd if=./hello_app/hello_app.bin of=./apps.bin conv=notrunc bs=1 seek=3

# 第二个应用
app_size2=$(stat -c %s ./hello_app2/hello_app2.bin)
printf "$(printf '%04x' $app_size2)" | xxd -r -p | dd of=./apps.bin conv=notrunc bs=1 seek=9
dd if=./hello_app/hello_app.bin of=./apps.bin conv=notrunc bs=1 seek=11

mv ./apps.bin ./arceos-unikernel/payload/apps.bin

练习 3

批处理方式执行两个单行代码应用,第一个应用的单行代码是 nop,第二个的是 wfi

思路

  • 制作bin文件的过程同练习二
  • 在代码中通过for来遍历二进制文件中的应用程序即可

练习 4

本实验已经实现了1 号调用 - SYS_HELLO,2 号调用 - SYS_PUTCHAR,请实现 3 号调用 - SYS_TERMINATE 功能调用,作用是让 ArceOS 退出,相当于 OS 关机。

思路:

  • 仿造已实现的两个系统调用,通过调用std::process:exit方法来退出ArceOS

练习 5

按照如下要求改造应用 hello_app:

  1. 把三个功能调用的汇编实现封装为函数,以普通函数方式调用。例如,SYS_PUTCHAR 封装为 fn putchar(c: char)
  2. 基于打印字符函数 putchar 实现一个高级函数 fn puts(s: &str),可以支持输出字符串。
  3. 应用 hello_app 的执行顺序是:Hello 功能、打印字符串功能、退出功能。

思路:

  • 需要通过linker.ld定义hello_app的内存布局,ABI_TABLE的地址由操作系统通过a0寄存器传递给应用,之后再存入a7寄存器
  • 其中的一个系统调用如下,clobber_abi("C")表示按照C调用约定来修改寄存器,用于保持Rust代码与内联汇编之间的一致性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn putchar(c: u8) {
unsafe {
core::arch::asm!("
li t0, {abi_num}
slli t0, t0, 3
mv a7, {abi_table}
add t1, a7, t0
ld t1, (t1)
jalr t1",
abi_num = const SYS_PUTCHAR,
abi_table = in(reg) ABI_ADDR,
in("a0") c,
clobber_abi("C"),
)
}
}

练习 6

  1. 仿照 hello_app 再实现一个应用,唯一功能是打印字符 ‘D’。
  2. 现在有两个应用,让它们分别有自己的地址空间。
  3. 让 loader 顺序加载、执行这两个应用。这里有个问题,第一个应用打印后,不能进行无限循环之类的阻塞,想办法让控制权回到 loader,再由 loader 执行下一个应用。

思路:

  • 参考练习5的思路,新增一个应用,通过for循环来执行两个应用程序

总结报告

第一周练习

练习1是输出彩色字体,实现并不难。

练习2是移植hashmap,我将标准库里的 hashmap 拷贝过来,先全部注释掉,再根据测试结果逐步的去解除注释

练习三和四是实现内存分配算法和解析 fdt,解析 fdt 使用了群友推荐的库。

练习5是抢占式的 fifo,只需要将 rr 里面的抢占的代码移植过来

第二周练习

练习1 && 2

镜像格式:

|app_num|app_size|app_data|app_size|app_data|…|

一开始使用dd写入的时候一直无法读取第二个app,后来通过 hexdump 去解析镜像文件发现了问题在哪,进行了相应的修改。

练习 3

需要去掉 noreturn,让函数返回

练习 4

练习3使用 axstd 里面提供的 exit 方法就可以完成这个实验

练习 5

练习4需要注意寄存器 a7 被 rust 修改,每次调用完后需要重新设置 a7 的值,或者第一次返回的时候就保存好入口地址,以后就使用这个入口地址,还需要加上clobber_abi("C")。遍历字符串的时候使用 bytes 来迭代。

练习 6

需要先读取 app_num 和 app_size。其他就移植练习5过来就行。

代码仓库:https://github.com/uran0sH/arceos

总结报告

很高兴参加第三阶段ArceOS Unikernel的课程学习与训练,
经过第三阶段的学习,让我对rust、unikernel、linux等都有了更深入的理解。经过这些实验与练习,提升了我对rust语言的理解,感受到rust的条件编译的强大。学习了ArceOS的层次结构、模块化设计理念,让我对Unikernel的概念有了更具象化的理解。十分感谢石磊老师的细心讲解、助教老师孙应娥的热心帮忙以及群里的同学们的讨论指导。

下面是逐个练习的总结与反思。

第一周练习

练习 1 &2

练习1

支持彩色打印println!。以apps/helloworld为测试应用。
要求:不能在helloworld程序本身改,要在下面的库或更深层次的组件修改

练习2(附加题)

支持HashMap数据类型。以apps/memtest为测试应用。
要求:在ulib/axstd中支持HashMap类型

预期输出:执行 make A=apps/memtest ARCH=riscv64 run

思路:练习1就是在println!宏的实现上添加带颜色的格式控制就可以了。
练习2根据提示是参考rust官方的hash表实现,但是官方实现内容太多了,错综复杂的依赖不好解决。根据老师在群里的提示,只需要实现测试用到的函数(new、iter、insert等)就好了,然后就顺利解决了。

我在参考官方库的时候,发现官方库实际是在 用hashbrown::hash_map,进行二次封装的。所以这个练习有一个偷懒的做法

1
use hashbrown::hash_map as HashMap;

练习3

练习3

为内存分配器实现新的内存算法early,禁用其它算法。early分配算法以apps/memtest为测试应用。

这题是我卡的最久的一个练习,我一开始以为需要涉及底层内存的分配(实际上是分配好底层的内存了),实际上为了简化练习,底层内存已经分配固定了,只是需要给应用分配地址。
因此我一开始走了很多弯路,我的原本的想法是将byteAllocator和pageAllocator进行结合,或者是使用byteAllocator和BitAlloc结合。导致这样的想法是为以为BitAlloc是会控制底层的内存分配(不得不说实在太蠢了)。

尝试了各种方法后,请教了王格格同学,知道了正确的做法:维护byte_pos和page_pos来进行内存分配就好了。

实际的细节中要注意内存地址的对齐,为在练习5中发现了在练习3中的问题。即内存地址的大小和地址都要进行对齐。

练习4

解析dtb(FDT的二进制格式),打印物理内存范围和所有的virtio_mmio范围。以apps/memtest为测试应
用。
当ArceOS启动时,上一级SBI向我们传递了dtb的指针,一直会传递到axruntime,我们就在
axruntime中执行解析并打印。

思路:需要查阅相关资料,了解DTB,了解FDT相关的内容。感谢群里的郝淼同学分享的hermit-dtb库,解析dtb很方便。在解析的过程中需要注意的点是内存地址和大小的字段在reg字段中,需要将reg一分为二。

练习5

练习5

把协作式调度算法fifo改造为抢占式调度算法。让测试应用通过

难点:主要是要理解arcos中的调度模块的代码,启用preemt的feature。

第二周练习

练习1&2

练习 1:

main 函数中,固定设置 app_size = 32,这个显然是不合理甚至危险的。
请为 image 设计一个头结构,包含应用的长度信息,loader 在加载应用时获取它的实际大小。执行通过。

练习 2:

在练习 1 的基础上,扩展 image 头结构,让 image 可以包含两个应用。
第二个应用包含唯一的汇编代码是 ebreak 。
如实验 1 的方式,打印出每一个应用的二进制代码

练习1和练习2是关系紧密的,都是要给image添加文件头,可以直接做练习2。

我们需要知道应用的数量,以及每个应用的字节长度,所以很自然地可以想到,首先文件头第一个字段应该是 应用数量,然后接着是每个应用的应用大小,最后是每个应用的应用二进制内容

因此我设计的文件格式如下
| field| length|
| ———– | ———– |
| app_amount| 2 bytes|
| app0_size | 2 bytes|
| app1_size | 2 bytes|
| …… | 2 bytes|
| appn_size | 2 bytes|
| app0_bin | app0_size bytes|
| app1_bin | app1_size bytes|
| ……. | …….|
| appn_bin | appn_size bytes|

应用数量和应用大小字段长度都是2个字节,因此我用rust写了个小程序将文件头写入文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//参数是 需要合并的app文件名称
let mut args =env::args();
args.next();
let files_number:u16=args.len() as u16;

let mut output_file: File =File::create("new_apps.bin").expect("open fails");
output_file.write(&files_number.to_be_bytes()).expect("write file numbers fails");

for file in args{
let length:u16= fs::metadata(file.clone()).expect("open file fails").len() as u16;
println!("APP:{}:{}",file,length);
output_file.write(&length.to_be_bytes()).expect("writing file length fails");

}
args=env::args();
args.next();
for file in args{
let mut source_file=File::open(file).expect("Opening file fails");
let mut buffer=Vec::new();
source_file.read_to_end(&mut buffer).expect("reading file fails.");
output_file.write_all(&buffer).expect("writing file fails.");
}

相应的在arcos中的loader程序就要先读取前两个字节,获取app数量,然后再依次读取app长度和app的内容

要注意的细节:要维持文件大小在32M,否则qemu无法加载,文件大小在Makefile文件中写死了。

练习3

练习 3:

批处理方式执行两个单行代码应用,第一个应用的单行代码是 nop ,第二个的是 wfi

思路:如何把控制权重新回到arceos中,重新返回到loader中执行

练习4

练习 4:

本实验已经实现了1 号调用 - SYS_HELLO,2 号调用 - SYS_PUTCHAR,请实现 3 号调用 -
SYS_TERMINATE 功能调用,作用是让 ArceOS 退出,相当于 OS 关机。

思路:熟悉arceOS的代码,寻找相关的模块代码。我的想法是关机的调用实际上是应该控制硬件来进行操作的,可能跟sbi相关的,应该在硬件抽象层,所以为主要是看了axhal模块的代码。然后发现了相关的调用代码

1
2
3
4
5
6
7
8
9
/// Shutdown the whole system, including all CPUs.
pub fn terminate() -> ! {
info!("Shutting down...");
sbi_rt::system_reset(sbi_rt::Shutdown, sbi_rt::NoReason);
warn!("It should shutdown!");
loop {
crate::arch::halt();
}
}

所以只需要在loader中调用这个terminate函数就可以实现关机操作。

练习5

练习5

照如下要求改造应用 hello_app:

  1. 把三个功能调用的汇编实现封装为函数,以普通函数方式调用。例如,SYS_PUTCHAR 封装为
    fn putchar(c: char) 。
  2. 基于打印字符函数 putchar 实现一个高级函数 fn puts(s: &str) ,可以支持输出字符串。
  3. 应用 hello_app 的执行顺序是:Hello 功能、打印字符串功能、退出功能。

思路:将三部分调用的内联汇编代码都封装在函数中,在入口函数中顺序调用即可。

看样子不难的程序我又卡了很久很久,跟编译器斗智斗勇了很久。

1
2
3
4
5
6
unsafe extern "C" fn _start(_entry:usize){
ENTRY=_entry;
hello();
puts("ArceOS exercise 5.");
terminate();
}

理想的执行过程是依次调用hello、puts、terminate函数后退出。但是无论怎么尝试,程序只会执行第一个调用的函数后退出。我就很疑惑:能够执行第一个调用的函数,那说明我的函数封装应该没有问题,可是为什么无论调用多少次,只会执行一次。而且调用多少次都不会改变编译生成的文件大小。然后我又生成了汇编代码进行查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
_start:
addi sp, sp, -32
sd ra, 24(sp)
sd s0, 16(sp)
sd s1, 8(sp)
sd s2, 0(sp)
addi s0, sp, 32
mv s1, a0
.Lpcrel_hi0:
auipc s2, %pcrel_hi(_ZN9hello_app5ENTRY17hc4afe4ceb535dcc3E.0)
sd a0, %pcrel_lo(.Lpcrel_hi0)(s2)
mv t2, a0
#APP

li t0, 1
mv a7, t2
slli t0, t0, 3
add t1, a7, t0
ld t1, 0(t1)
jalr t1

#NO_APP
ld a0, %pcrel_lo(.Lpcrel_hi0)(s2)
bne a0, s1, .LBB0_2
ld ra, 24(sp)
ld s0, 16(sp)
ld s1, 8(sp)
ld s2, 0(sp)
addi sp, sp, 32
ret

然后发现start内确实只有我第一个调用函数的汇编代码。难道是为我的代码被编译器给优化了?
然后尝试了各种方法,改成debug模式,修改优化等级,修改使用的寄存器等。比如在Cargo.toml中修改优化等级为0。

1
2
[profile.dev]
opt-level = 0

均没有任何效果。
然后为又重新审视了每个汇编代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsafe fn hello() {
core::arch::asm!("
li t0, {abi_num}
mv a7,t2
slli t0, t0, 3
add t1, a7, t0
ld t1, (t1)
jalr t1
",
abi_num = const SYS_HELLO,
in("t2") ENTRY,
clobber_abi("C"), // 告诉编译器 clobber 了通用寄存器
options(noreturn)
)
}

然后发现了问题所在:options(noreturn)是告诉编译器这段汇编代码没有返回,也就是说后面的代码永远也不会执行,因此编译器会把后面调用的函数代码全都丢掉。因此只会执行第一次函数调用。注释掉即可解决问题。

练习6

练习 6:

  1. 仿照 hello_app 再实现一个应用,唯一功能是打印字符 ‘D’。
  2. 现在有两个应用,让它们分别有自己的地址空间。
  3. 让 loader 顺序加载、执行这两个应用。这里有个问题,第一个应用打印后,不能进行无限循环
    之类的阻塞,想办法让控制权回到 loader,再由 loader 执行下一个应用。

思路:需要为每个应用分配独立的地址空间,并在跳转到app之前切换到app的地址空间,返回时再重新切换回内核空间。

因此在切换到app的地址空间之前,需要先保存当前的satp寄存器的值,执行完app后重新加载。

2023秋冬季开源操作系统训练营第三阶段总结报告

练习一、二

练习要求

练习 1:
main 函数中,固定设置 app_size = 32,这个显然是不合理甚至危险的。
请为 image 设计一个头结构,包含应用的长度信息,loader 在加载应用时获取它的实际大小。
执行通过。
练习 2:
在练习 1 的基础上,扩展 image 头结构,让 image 可以包含两个应用。
第二个应用包含唯一的汇编代码是 ebreak 。
如实验 1 的方式,打印出每一个应用的二进制代码。

实现过程

根据练习一、二的要求设计 image 结构如下图所示。

Alt text

image 文件中的首 0-7 的字节用于存放 ‘app 数量’,用以告诉加载器整个 image 中包含有多少个 app。后续的内容以 ‘app 数量’ 个的 app 结构块组成,每个 app 结构块 的首个 32 字节存放当前 app 的块数量,第二个 32 字节存放不满足块大小的 ‘余量’。
‘app 块大小’ 默认定义为 1024 ,即一个块大小为 1024 字节。app 的 ‘块数量’ 则为 ‘app 二进制文件大小除以 app 块大小’。’余量’定义为不满足一个块大小的剩余长度,表示为 ‘app 二进制文件大小除以 app 块大小的余数’。
跟据上述的定义,将 app 二进制文件修改为以 ‘.app’ 后缀结尾的文件,依托于 rust 的构建脚本 build.rs 查找指定目录下的 app 文件,生成上述格式的 image 文件,同时使用 dd 命令完成文件合并,需要保证 * pflash 文件必须为 32MB 大小*。
后续在 loader 中完成对 image 的解析,并在 app 需要执行时将 app 字节码内容拷贝到指定地址后,跳转到相应地址执行 app 内容。 app 结构定义 如下:

1
2
3
4
struct App<'a> {
size: u32, size: usize,
code: &'a [u8], code: &'a [u8],
}

练习三

练习要求

练习 3:
批处理方式执行两个单行代码应用,第一个应用的单行代码是 nop,第二个的是 wfi。

实现过程

根据上一节的练习添加第二个应用,在第一个应用执行完成后并没有返回到 loader 执行下一个下一个应用,根据调试分析,在第一个应用中并没有返回到 loader 的相关代码,需要添加添加相关代码才能返回到 loader 执行下一个 app。在每个 app 的代码中添加 ‘ret’ 指令,并删除 ‘options(noreturn)’ 以及函数的 ‘永不返回’ 标识 ‘!’,以完成在 app 执行结束后跳转回 loader。同时在构建 image 的时候需要注意 app 的顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#![no_std]
#![no_main]

use core::panic::PanicInfo;

#[no_mangle]
unsafe extern "C" fn _start() {
core::arch::asm!(
"
nop
ret
"
)
}

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}

练习四

练习要求

练习 4:
本实验已经实现了1 号调用 - SYS_HELLO,2 号调用 - SYS_PUTCHAR,请实现 3 号调用 - SYS_TERMINATE 功能调用,作用是让 ArceOS 退出,相当于 OS 关机。

实现过程

仿照 SYS_PUTCHAR 的实现,调用 axhstd 提供的 exit 函数完成相关功能个即可。

练习五

练习要求

练习 5:
按照如下要求改造应用 hello_app:

1.把三个功能调用的汇编实现封装为函数,以普通函数方式调用。例如,SYS_PUTCHAR 封装为 fn putchar(c: char)。
2.基于打印字符函数 putchar 实现一个高级函数 fn puts(s: &str),可以支持输出字符串。
3.应用 hello_app 的执行顺序是:Hello 功能、打印字符串功能、退出功能。

实现过程

根据实验文档,ABI_TABLE 的基地址保存在 a7 寄存器中,在进入 app 时将 a7 寄存器的内容保。同时将 ABI 函数的调用逻辑封装为函数 ‘abi_call’,需要注意使用 ‘clobber_abi(“C”)’ 完成对寄存器的保存,同时指定 app 的 链接文件,以指定字符串存放位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// abi 调用函数
unsafe fn abi_call(addr: usize, abi_num: usize, arg0: usize) {
core::arch::asm!("
mv a7, {abi_addr}
mv t0, {abi_num}
slli t0, t0, 3
add t1, a7, t0
ld t1, (t1)
jalr t1",
abi_addr = in(reg) addr,
abi_num = in(reg) abi_num,
in("a0") arg0,
clobber_abi("C")
)
}

练习六

练习要求

练习 6:
1.仿照 hello_app 再实现一个应用,唯一功能是打印字符 ‘D’
2.现在有两个应用,让它们分别有自己的地址空间。
3.让 loader 顺序加载、执行这两个应用。这里有个问题,第一个应用打印后,不能进行无限循环之类的阻塞,想办法让控制权回到 loader,再由 loader 执行下一个应用。

实现过程

在练习五的基础上,仿照 ‘hell_app’ 实现另一个 app,同时需要避免 app 中的 ‘wif’ 和无限循环等阻塞操作,避免不能返回 ‘loader’ 执行下一个 app。

总结

本次实验和练习加深了我对操作系统的理解,同时也学习了组件化操作系统 unikernel 的相关知识、对 qemu 的使用更加熟练的同时也学习了 pflash 相关的知识。对 rust 的嵌入式汇编及相关知识的掌握更加深入。同时也感谢各位老师的对本次课程的知识讲解,也感谢各位同学在群里提出的的问题以及解决方案。

总结报告

第一周实验总结

exercise 1&2: 改变println!宏以及引入haspmap

这里我是直接查询chatgpt知道可以通过在println!加入特殊符号来改变颜色,于是改变了底层的println!实现了。

后者我则是有点找不着头脑,因为本身的rust基础就是训练营的第一周和第二周,而这种程度根本无法跟得上后续的项目级别的开发,二阶段也只是调用api,到了第三阶段才发现自己连cargo管理项目和lib.rs文件都不知道是什么作用,导致自己花了很多时间去找bug,后来通过在项目群里询问大佬才发现自己的问题,当然对于引入hashmap的操作,也是借鉴了石磊老师的注释大法,通过报错信息和一步步释放自己想要的代码来逐步达到目的,这让我节省了很多时间。

exercise 3&4 引入early算法和解析dtb文件

前者我是直接通过将buddy算法和bitmap算法两个内存分配和页分配的算法结合起来拼凑起来实现的。

后者我是通过使用群里大佬分享的库,以及询问邝劲强大佬关于库的使用方法,他给予了我一些提示,于是成功实现了这个练习。

exercise 5 将协作式调度算法fifo改造为抢占式调度算法

这里我是直接参考了round_robin.算法的实现进行修改,然后比较顺利的通过。

第二周实验总结

exercise 1&2

这里我使用了非常规的方法,直接使用了脚本来获取文件大小等信息

后者通过脚本将两个文件合并到一起,后来才发现这种做法是不可取的,少了在loader的一环,这也导致我后续练习5的bug找了很久。

exercise 3&4

这两个实验则相对于简单很多,第一个实验就是改一下汇编代码’nop’和’wfi’

后者就是仿照puthello和putchar然后加一个axstd::process::exit函数的调用就行了。

exercise 5&6

其实在我心目中,反而是第一个和第二个实验最难,我对文件那块不太熟悉,练习5和练习6的思路和策略我觉得想得很快,无非是因为要写内联汇编而已,这里我也是首先犯了没有保存a7寄存器的错误,后续发现不再是loadfault的错误而是IllegalInstruction的错误信息,一直找不到错误的重点在哪里,在此过程中,在助教和群友的帮助下,尝试了通过其他寄存器如s11来保存a7,通过全局静态变量来保存a7寄存器,通过在loader传入a0而非a7寄存器到helloapp的_start函数的第一个参数来保存abi表地址一共三个办法,得出一定不是

helloapp本身的问题,而是loader的文件,然后通过排查,发现是load_size的大小太小了,一直写得指导书的32,后面改大一点通过了

练习6意识到需要改正练习1和练习2没有在loader加入头结构和相关函数的问题,于是添加上去,实现了分别调用两个地址空间的应用。

第一周练习总结

第一周的 5 个练习的主要目的是了解 ArceOS 的基本代码组织结构,为后面的任务打下基础。

练习 1、2

练习 1 要求实现彩色打印 println!,这个任务我采用了修改 axstd::println! 来实现,这是出于架构兼容性的考虑,不应修改太过底层的代码。具体实现是利用了 ANSI 转义序列。

练习 2 要求支持 HashMap 并通过测试。之所以 Rust 核心库中没有 HashMap,是因为 HashMap 需要调用产生随机数的接口,而这个接口是架构相关的。因此,练习 2 的核心是实现 axstd 可调用的 fn random() -> u128 接口。

自底向上实现 random 的过程:

  • axhal::random::random():在 axhal 层实现架构相关的 random() 接口,它调用了 axhal::time::current_ticks()
  • arceos_api::random::ax_random():定义 ax_random 接口,通过 pub use axhal::random::random as ax_random; 将其直接实现为 axhal::random::random()
  • axstd 中可调用 arceos_api::random::ax_random()

练习 3、4

练习 3 要求实现内存分配器 early。ArceOS 中包含两个内存分配器 pallocballocpalloc 用于以页单位的内存分配,balloc 被指定为 Rust 的 #[global_allocator],这样我们就可以使用 Rust 中如 VecString 等这些堆上可变长的数据类型。这两个内存分配器都包含在一个全局内存分配器 GLOBAL_ALLOCATOR 中。

early 分配器既是 palloc 又是 balloc,因此这里遇到一个问题:GLOBAL_ALLOCATOR 中使用了一对智能指针分别指向了 pallocballoc 实例,但由于 early 分配器的特点,early 在全局只有一个实例,并且其中的 pallocballoc 还需要共享数据。

因此,我将 GLOBAL_ALLOCATOR 可能的类型分为两种:

  • SeparateAllocator:分离式内存分配器,pallocballoc 是两个,当 balloc 可用内存不足时,向 palloc 请求内存
  • AIOAllocator:一体式内存分配器,pallocballoc 是同一个,两个内存分配器中共享剩余可用内存

此外,我定义了 trait GlobalAllocator,与原来的 GlobalAllocator 类中实现的方法保持一致。而后,为 SeparateAllocatorAIOAllocator 分别实现 GlobalAllocator,然后在 toml 中增加关于 early 的选项,即可达成练习目标。

练习 4 要求通过 rust_main() 的参数 dtb 解析 dtb。这一练习的关键是找到一个合适的 crate,我使用的是 hermit-dtb = "0.1.1"

练习 5

练习 5 要求将 FIFO 算法改变为抢占式的。FIFO 算法原本维护了一个队列,靠进程主动放弃 CPU 触发调度。在抢占式中,需要将原本的 List 修改为双端队列 VecDequeue,这样在 remove_task(&mut self, task: &Self::SchedItem) 中,修改为先找到 task 的下标,然后根据下标将其从队列中删除;在 put_prev_task(&mut self, prev: Self::SchedItem, preempt: bool) 如果此时是可以抢占的,就将 prev 放入队尾,否则将其放在队头,相当于调度器选择的还是当前的任务;最后,将 task_tick 的返回值恒置为 true 这样每次时钟中断发生时都能进行调度。

第二周练习总结

第二周在一个个小练习后逐渐实现了一个可以加载二进制文件的 loader,并且加载的应用有独立的地址空间。

练习 1、2

实际上练习1、2的任务是实现一个简易的文件系统。我的设计如下:

1
2
3
4
5
6
7
struct ImgHeader {
app_num: usize,
}

struct AppHeader {
app_size: usize,
}

将 1 个 ImgHeader 放在 pflash 头部,而后紧接着app_numAppHeader 用于指示每个应用的大小。

生成二进制镜像,添加这写头部信息我是用 C 语言实现的,在此不再赘述。

练习 3

批处理的方式下,先加载第一个应用,然后运行;而后加载第二个应用,然后运行。这两个应用均被加载到一个固定的地址。

在 loader 中,使用 jalr 指令通过函数调用执行一个应用,因此,在进入应用程序的主函数时,ra 寄存器已经被加载为返回地址,想要从应用程序返回,只需要将函数签名返回值部分的 -> ! 改为 -> () 或者直接删除即可。这样,应用程序生成的汇编代码最后是一条 ret 指令,恰好能返回到 loader。

练习 4、5

练习 4 将 sys_terminate 封装为对 axstd::process::exit 的调用即可。

练习 5 我将所有的 abi 调用和为一个分发函数:

1
2
3
4
5
6
7
8
fn abi_entry(abi_num: usize, arg0: usize) {
match abi_num {
SYS_HELLO => abi_hello(),
SYS_PUTCHAR => abi_putchar(arg0 as u8 as char),
SYS_TERMINATE => abi_terminate(arg0 as i32),
_ => panic!("unsupport abi call!"),
}
}

然后将这个函数的地址作为第一个参数传入应用,即通过 a0 传参:

1
unsafe extern "C" fn _start(abi_entry: usize) -> !

在应用中,封装 putchar 等函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn putchar(c: u8) {
unsafe {
core::arch::asm!("
li a0, {abi_num}
la t0, {abi_entry_addr}
ld t0, (t0)
jalr t0",
abi_num = const SYS_PUTCHAR,
abi_entry_addr = sym ABI_ENTRY,
in("a1") c,
clobber_abi("C"),
)
}
}

实际上,所有 abi 函数都是跳转到 abi_entry 执行,不同之处在与参数的传递,我规定第一个参数 a0 是 abi 号,后面的是 abi 函数的参数。在 putchar 中,a0 被设置为 SYS_PUTCHARa1 被设置为 c 的值。注意,在内联汇编的结尾要加上 clobber_abi("C"),保持寄存器在执行这段汇编代码前后的一致,否则将出现混乱。

练习 6

目前,在批处理且每个应用地址空间相同的情况下,练习 6 实现起来比较简单,保持每个应用复用全局的应用页表 APP_PT_SV39 即可。

总结报告

练习1:为 image 设计一个头结构,包含应用的长度信息,loader 在加载应用时获取它的实际大小

这个实验刚开始的时候一头误水,以为是生成镜像后,让loader自己判断应用长度和个数,发现很难实现,后面才发现练习1的要求是在image开头加入应用长度信息,这样loader只需要获取相应的信息就可以获取实际大小和个数,实现非常轻松,后面才了解到例如elf等文件,都会有相应的头部信息给loader进行判断。

应用头

1
2
3
4
5
struct AppHeader {
start: usize,
size: usize,
content: &'static [u8],
}

后面感谢邝劲强同学提供了一个生成镜像的脚本思路,以及dd中seek选着字节跳过的用法。

练习 2:在练习 1 的基础上,扩展 image 头结构,让 image 可以包含两个应用

在完成练习1后,练习2就变得很轻松了,顺着练习1的思路只需要在镜像开头加入应用个数的信息,然后loader获取后,循环调用每一个app即可。

镜像头:

1
2
3
struct ImageHeader{
ptr_len: usize
}

循环调用:

1
2
3
4
5
6
7
8
9
let mut app_start = PLASH_START + ptr_len;
for _ in 0..app_nums {
let app_header = image_header.load_app(app_start);
println!("start:{:#}",app_header.start);
println!("size:{}", app_header.size);
println!("context:{:?}",app_header.content);
println!("......................................");
app_start += app_header.size + ptr_len;
}

练习3:批处理方式执行两个单行代码应用,第一个应用的单行代码是 nop ,第二个的是 wfi

刚开始没注意到改文档了,写的noop,然后直接报错了,调试了一下,最后看群里面才发现是nop。然后好像就没什么了,把实验2的实现放在练习3就行了。

练习4 请实现 3 号调用 -SYS_TERMINATE 功能调用,作用是让 ArceOS 退出,相当于 OS关机

这个在前面的学习中已经发现axstd中已经实现了axstd::process::exit,只需要封装下调用即可,还是比较简单的。

练习5: 按照要求改造应用 hello_app

这个应该是6个练习中最难的一个,刚开始按照实验4的思路把汇编指令全部移动到了外部的镜像中,但是和实验4不同的是,这里会执行3个系统调用(应该是大于三个的,因为打印字符串,吊调用了多次putchar),然后我执行完第一个后就发生panic,然后为对riscv汇编指令不熟,也不知道如何查看寄存器状态,这里感谢刘逸珑同学大半夜了教我如何使用gdb,最后发现是a7寄存器的值发生了改变,即原本传入的abi_table,在调用第一个函数后a7的值发生了改变,后面发现是在执行后axhal调用了a7,存了一下值,于是问题就简单了,用一个t2寄存器存一下a7的值即可。
实现的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
fn hello() {
unsafe {
core::arch::asm!("
li t0, {abi_num}
slli t0, t0, 3
add t1, a7, t0
mv t2,a7
ld t1, (t1)
jalr t1
mv a7,t2",
abi_num = const SYS_HELLO,
clobber_abi("C")
)
}
}

fn putchar(c: u8) {
unsafe {
core::arch::asm!("
li t0, {abi_num}
slli t0, t0, 3
add t1, a7, t0
mv t2,a7
ld t1, (t1)
jalr t1
mv a7,t2",
clobber_abi("C"),
abi_num = const SYS_PUTCHAR,
in("a0") c,

)}
}

fn terminate(exit_code: i32) {
unsafe {
core::arch::asm!("
li t0, {abi_num}
slli t0, t0, 3
add t1, a7, t0
mv t2,a7
ld t1, (t1)
jalr t1
mv a7,t2",
abi_num = const SYS_TERMINATE,
in("a0") exit_code as u8,
clobber_abi("C")
)
}

}

练习6:

  1. 仿照 hello_app 再实现一个应用,唯一功能是打印字符 ‘D’。
  2. 现在有两个应用,让它们分别有自己的地址空间。
  3. 让 loader 顺序加载、执行这两个应用。这里有个问题,第一个应用打印后,不能进行无限循环
    之类的阻塞,想办法让控制权回到 loader,再由 loader 执行下一个应用。

这个比较简单只需要在练习5的基础上使用putchar函数,并且删除wfi,让程序不要等待中断即可。

个人总结

刚开始我每个项目都进去听了一下,最后被arceos的组件化思想给吸引了,加上老师上课任务都很用心,因为平时还有课,就专注于项目1了,收获不小,rust和unikernel,甚至如何编写makefile 都是在从这次开源活动中学习到的,尤其是项目1。最让我深刻的就是自己编写makefile然后成功让镜像加入到了loader里面并运行,和练习5调试那个汇编代码和寄存器,这两个地方,解决困难后的兴奋感。