0%

2023开源操作系统训练营第三阶段项目一总结报告

练习1,2:

该练习实现了一个 app 名为 loader 的外部应用加载器,然后实现了两个外部应用,熟悉了编译生成和封装二进制文件的流程.

该练习的重点在于头结构的构造与实现,我将应用的头结构大致分为三部分,第一部分为应用数量,第二部分为各应用长度,第三部分为应用数据.

大致实现如下

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
type BinNumType = u32;
const BIN_NUM_TYPE_BYTES: usize = 4;
type BinSizeType = u32;
const BIN_SIZE_TYPE_BYTES: usize = 4;
let apps_num = unsafe {
(*(PLASH_START as *const BinNumType)).to_be()
};
let mut size_offset = PLASH_START + BIN_NUM_TYPE_BYTES;
let mut start_offset = size_offset + BIN_SIZE_TYPE_BYTES * apps_num as usize;
for i in 0 .. apps_num {
let load_size = unsafe {
(*(size_offset as *const BinSizeType)).to_be() as usize
};
let load_code = unsafe {
core::slice::from_raw_parts(start_offset as *const u8, load_size)
};
println!("load code {:?}; address [{:?}]", load_code, load_code.as_ptr());
let run_code = unsafe {
core::slice::from_raw_parts_mut(RUN_START as *mut u8, load_size)
};
run_code.copy_from_slice(load_code);
println!("run code {:?}; address [{:?}] size [{}]", run_code, run_code.as_ptr(), load_size);
size_offset += BIN_SIZE_TYPE_BYTES;
start_offset += load_size;
}

下面是构造image文件的大致代码

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
#!/bin/bash
# 获取当前目录下所有的 .bin 文件
apps=(hello_app.bin putd_app.bin)
# 计算应用数量
num_apps=${#apps[@]}
# 创建临时文件用于存储应用大小
size_file="temp_sizes.bin"
> "$size_file"
# 创建一个新的文件用于存储合并后的应用内容
content_file="temp_content.bin"
> "$content_file"
# 写入应用数量(4 字节)
printf "%08x" $num_apps | xxd -r -p > "$size_file"
# 循环遍历每个应用并写入其大小到临时文件
for app in "${apps[@]}"; do
# 获取并写入当前应用大小(4 字节)
app_size=$(stat -c%s "$app")
printf "%08x" $app_size | xxd -r -p >> "$size_file"
# 追加应用内容到另一个临时文件
cat "$app" >> "$content_file"
done
combined_file="combined.bin"
# 合并大小信息和应用内容
cat "$size_file" "$content_file" > "$combined_file"
output_file="apps.bin"
dd if=/dev/zero of="$output_file" bs=1M count=32
dd if="$combined_file" of="$output_file" conv=notrunc
mkdir -p ../arceos/payload
mv "$output_file" ../arceos/payload/apps.bin
# 清理临时文件
rm "$size_file" "$content_file" "$combined_file"
echo "合并完成,输出文件:$output_file"

练习3

本实验较为简单,在原来的基础上修改外部应用的汇编指令以及然后在遍历程序时将其执行即可.

练习4

本实验也比较简单,仿照前面的系统调用写一个即可,关机方法:std::process::exit.

练习5

因为多次系统调用a7寄存器的值会被修改,导致报错,所以我们需要将其存入变量中防止abi_table被修改,而且还要声明clobber_abi(“C”)保持Rust代码与内联汇编之间的一致性.然后在我写的过程中,出现了冒用 in(reg) 和 out(reg) 的错误,导致传入的 ABI 接口的地址导致调用函数的时候接口地址丢失,在第二次调用的访问未映射的地址导致错误发生.

练习6

在练习5的基础上,初始化地址空间以后,对于每一个外部应用调用 RUN_START 前切换地址空间即可.但此时应用并无法正确返回,是因为外部引用主动阻塞导致的,外部应用不能进行阻塞,我们需要修改 _start 函数的返回值为 ();

ArceOS Unikernel 总结报告

回顾学习ArceOS Unikernel的这两周,我学到了很多相关知识。通过石磊老师的教导,我先是从第一个星期的组件化教程开始,实现了彩色输出,然后完成了对HashMap的移植,在用early算法实现了内存分配器后又完成了 dtb 文件的解析和输出,最后修改原有的协作式调度算法fifo为抢占式调度。这一步步的任务让我逐渐掌握了 Unikernel 的用法,对 ArceOS 的整体架构、内存分配及调度算法有了更深入的了解。
然后是第二周的基础任务,分别是:从外部加载应用、将应用拷贝到执行区域并执行、通过 ABI 调用 ArceOS 功能、在 App 中正式调用 ABI 以及支持内核和应用分离的地址空间及切换,基本都是更着做就可以了,通过这些练习,我们也更加深入地了解 ArceOS 系统的运行机制和应用开发技巧。下面本别讲解以下各练习的具体做法及思路。

练习题思路总结

练习 1 和 2

本实验实现加载器 loader ,从外部加载 bin 应用到 ArceOS 地址空间,对头结构的设计要让loader可以读取应用信息。
头结构的设计:
App总数 App1大小 App1 内容 App2 大小 App2 内容 … AppN 大小 AppN 内容
这样就可以很好地管理各个应用程序,从而实现应用程序的加载。

练习 3

在之前实验的基础上修改汇编指令以及 loader 改用批处理的方式加载外部应用即可。

练习 4

本实验只需要添加 SYS_TERMINATE 系统调用,然后让 SYS_TERMINATE 直接调用 axstd::process::exit 即可。

练习 5

这个练习的前两个要求都不难,难的是第三个要求,每一次ABI调用返回到loader的时候都需要返回到应用上一条指令的状态,所以ABI调用的时候需要保存上下文。但在实现时发现a7会一直变化,所以需要使用clobber_abi(“C”) 并额外用临时寄存器存放和加载 abi_table 。最终实现上下文的保存。

练习 6

基于之前的 hello_app 应用,仅保留 putchar 函数。先将 _start 函数的返回值为 (),为 APP 建立独立的页表,实现初始化和切换函数,然后将练习5中等待中断的wfi指令去除再加上独立的页表就可以了。

几个核心问题

  1. 目前 ArceOS 已经实现了“线程”组件 axtask,如何利用它转变为进程?
    • 独立的地址空间
  2. 如何保持 unikernel 的特性?
    • 仅保留单一特权级

ArceOS 作为动态链接库

目前成果

在保持 ArceOS 单特权级的情况下,支持一些简单的动态链接程序的二进制兼容:

  • 编写加载器 loader 支持加载动态链接的 ELF
  • 为每个应用创建虚拟地址空间(页表),将其绑定在任务控制块上
  • 每个进程通过任务控制块参与调度,在 switch_to() 时切换地址空间

需要实现的功能

  1. 动态链接的原理:
    • pltgot 如何配合工作:
      1. .rela.plt:存储了加载器在动态链接时需要填写的 .got 表项,以及动态链接函数的符号信息,用于索引 .dynsym中的一项:
        1
        2
        #define ELF64_R_SYM(i) ((i) >> 32)
        ELF64_R_SYM(Elf64_Rela.r_info)
      2. .dynsym:可以查找到和动态链接符号的相关属性,以及符号名称(位于 .dynstr
    • ABI 是如何保持的:函数签名保持一致即可
  2. ArceOS 需要能加载 ELF
  3. 加载时如何填写 .got
  4. ArceOS 的编译脚本需要修改,
    • 编译 axlibc,导出符号表
    • 自身能够读取 libc 的符号表
    • 最好能直接将 libc 映射到一个地址段,所有进程共享

实现路径

  1. mmap():难度较大,因为目前没有进程,也就没有其对应的地址空间
  2. ELF 解析
  3. 动态链接

日志记录

11 月 23 日

  1. ArceOS 的 axstd 和 axlibc 不能同时链接

    • 如果强制链接的话会出现重复的符号

解决方法:将 rust_libc 和 app 链接在一起,c_libc 再和 rust 代码链接

  • rust loader 可以调用 libc 的函数
  • ArceOS 需要实现 mmap()

11 月 27 日

KISS 原则实现了加载时的动态链接:先不实现 mmap 和 ELF 解析,采用硬编码的方式加载 hello

ELF 解析加载过程:

  1. 读取 Program Headers(在 host 上用 readelf),将其中类型为 LOAD 的 segment 从文件中加载到内存。

11 月 28 日

实现了根据 ELF 头获取需要进行动态链接函数的填充地址和填充值,并进行动态链接。

ELF 解析加载过程:

  1. 获取 ELF 头,找到 section header table
  2. 根据 .shstrtab 找到:
    • .rela.plt:找到外部链接符号
    • .rela.dyn:链接内部符号
    • .dynsym:动态链接符号表
    • .dynstr:动态链接符号名的字符串表
  3. 外部动态链接
    • rela.r_sym() 索引 .dynsym,获取符号项 dynsym
    • 根据 dynsym 判断符号属性,符合动态链接的用 dynsym.st_name 索引 dynstr 获取符号名 func_name
    • 根据符号名查找 loader 初始化时填写的链接表,获取链接虚地址 link_vaddr
    • 将虚地址 rela.r_offset 处的 8 字节填写为 link_vaddr
  4. 内部动态链接
    • rela.r_addend 的值即为链接虚地址 link_vaddr
    • 将虚地址 rela.r_offset 处的 8 字节填写为 link_vaddr

11 月 29 日

根据 ELF 头读取 segment 信息,将其中属性为 LOAD 的段加载到内存。此时对 kernel 而言,hello app 不具有单独的进程(或者线程)属性,而是与 loader 为一体。

在 loader 的 Cargo.toml 开启 axstd 的 multitask 属性,可以正常编译,但内核 panic 退出,信息为 current task is uninitialized,定位到 axtask::task::CurrentTask::get() 函数。显然,在初始化时,已经设置了 current task,这里为何报错?

经过进一步阅读源码,发现 current task 的指针通过 axhal::cpu::current_task_ptr() 获取,而 axtask/multitask 开启了 percpu 功能,也就是说此时的 current task 指针是一个 percpu 变量。目前,ArceOS 中通过 gp 实现 percpu,这与 riscv 的 ABI 是不符的。具体体现为 hello app 一开始就重新加载了 gp,把内核里维护 percpu 的值冲掉了。

因此,为了保持 ABI,我将 percpu 改回了使用 tp 维护(前一个修改的 commit 将 tp 改为 gp)。此时在打开 axstd/multitask 后可以正常退出。

11 月 30 日

  • 实现 axtask::spawn_from_ptr(),专门用于创建从 ELF 加载任务的 TCB
  • 在 riscv 的任务上下文中加入 satpcontext_switch() 时同时切换页表
  • 目前实现了动态分配的页表,但未实现进程退出后页的释放

12 月 1 日

通过复用 axhal::paging::PageTable 的接口,实现了完整的页表分配、回收功能,并且修改 TaskInner 结构体,将页表绑定到上面。这样就实现了类似于 rCore 中的 RAII 风格页表。

总结

目前的实现充分利用了 ArceOS 中已经存在的 axlibc 和页表,不足之处:

  • loader 对于 ArceOS 而言也是一个应用程序,但是其中跨层级调用了很多底层接口,不符合 ArceOS 的设计原则
  • 不支持静态链接的 app,即程序中的系统调用指令 目前 ArceOS 无法处理
  • 修改了一些 ArceOS 中的现有模块,使得原有模块的实现发生改变,如利用 gp 保存 percpu 变量,为了验证思路的可行性,将其改为了 tp

第三阶段总结报告

基本任务体验

第三阶段从十一月出开始到12月2号结束,历时1个月。就个人情况来说,由于杂事比较多,从第一周周六(11.11)开始学习Arceos到第三周周三(11.22)完成了ArceOS unikernel项目的基本任务,后面的一周半没能抽出时间探索一下最终任务还是有些遗憾(不过我希望在本次训练营结束后再花点时间实现一下最终任务的若干个Idea)。

就难度来说,个人认为ArceOS比rCore更为复杂,刚打开ArceOS工作目录就感受到了很强的压迫感,但是得益于ArceOS优秀的组件化设计思路(点赞)将内核开发的难度局限再一个模块之内大大降低了实验过程的复杂度。

第一周的early内存分配器和dtb解析比较麻烦,首先实现early内存分配器时没有考虑到需要对分配指针的地址和分配的内存块大小对齐,这个造成了许多bug,对于dtb解析需要额外学习dtb的数据格式和解析库。

第二周的主要工作量在于设计img的头部用于加载多个应用,然后在练习5遇到了一些问题卡了一段时间,主要是内联汇编的某些写法有问题导致编译器直接没有生成这部分代码(一步一步调试找到了相对靠谱的写法)。

总得来说,由于ArceOS的模块化设计,对其其中一个修改造成的影响也尽量地限制在模块内,对于其他的模块可以不用了解其实现细节而使用其暴露的api接口(通过在Cargo.toml中指定依赖路径),这样简化了开发难度。

最后感谢全体rCore开发者的努力,创造了我认为最棒的OS入门教程╰(°▽°)╯。

第三阶段主要分成四周的学习。

第一周

第一周的学习主要围绕 arceos 的架构。在这基础上进行一些小实验。

第一个小实验比较简单,需要有彩色的输出。这只需要在 println 宏的字符串前后添加神秘代码即可。

第二个小实验是要实现一个 hashmap。这个的实现比较复杂,但是由于不是所有的方法都需要实现,而且网上相关的代码有许多,所以只需要从网上找一个 no-std 的实现,然后将其删改即可。另外,还需要实现随机函数,这里主要是根据时间来生成一个伪随机数。

第三个小实验是要实现 early 算法。这个算法的内容和实现比较简单。就是内存的两边一边分配字节,一边分配页,两者相遇即内存满。但是这个算法的回收操作不是很方便。

第四个小实验是解析 dtb。这个原本是一个复杂的工作,但是已经有相关库实现了。所以可以直接拿来使用。大大减少了工作量。

第五个小实验是把 fifo 改成抢占式的操作。这个实验要根据源代码 cfs 和 rr 的相关声明和实现来帮助理解。

总的来说,第一周的实验还是比较简单的,而且帮助我理解了 arceos 的相关架构。

第二周

第二周主要有六个练习,为最终的实习项目做准备。

练习 1 和练习 2 需要为 image 设计一个头结构,包含应用的长度信息,在加载时就能获取应用的实际长度。为了解决这个问题,我使用 python 将多个应用合并为一个 bin 文件,并在 bin 文件首部添加每个应用的长度和偏移地址。这样在读取应用时就能够按需读取。

练习 3 是要批处理方式执行两个单行代码应用,第一个应用的单行代码是 nop ,第二个的是 wfi。这个在前面练习 2 的基础上,循环读取每个 app 的代码,执行结束后就返回即可。

练习 4 是要实现 3 号调用 - SYS_TERMINATE 功能调用,作用是让 ArceOS 退出,相当于 OS 关机。这个的实现跟相对应的 SYS_HELLO 等调用方法一样。依葫芦画瓢即可。

练习 5 把三个功能调用的汇编实现封装为函数, 基于 putchar 实现 puts 输出字符串。这个我认为是整个第二周坑最多的一个练习。这个练习由于 puts 要输出字符串,所以需要一个链接文件将其放到指定的位置。另外,在內联汇编中,每次执行出来寄存器的值就变的不可预知,所以需要加上 clobber_abi(“C”), 来保护寄存器内容。

练习 6 实现一个应用,唯一功能是打印字符 ‘D’。 现在有两个应用,让它们分别有自己的地址空间。 让 loader 顺序加载、执行这两个应用。这个在练习 3 的基础上修改即可。需要修改的就是给每个应用都加一个页表即可。

第三周和第四周

最后的实验是要完成 linux app 的移植。这个我采用的方法是变更 elf 动态链接库,使其连接到自己写的函数中。我自己完成的部分是 elf 的解析,代码能够在没有开启分页的情况下运行 c 代码。由于我对 arceos 的分页有一定的误解,导致当分页开启时,原来的代码就不能正确访问和运行。后面当我看到郝同学的代码时,我发现他的方法跟我具有相似性,都是解析 elf,并将动态链接部分修改为自己的函数地址。所以后面我就跟着他的代码写了一遍,最终实现了功能。其中遇到的坑和解决方法如下:

  • 分页开启时,内核虚拟地址和物理地址之间是线性映射,但是中间有个偏差 axconfig::PHYS_VIRT_OFFSET,所以,之前所有的地址,比如 PLASH_START 就要加上 axconfig::PHYS_VIRT_OFFSET 才能正确访问到。
  • 每个物理地址除了内核的虚拟地址外,还可以根据三级页表建立映射。每个应用都建立一个三级页表,这样每个应用就都会有自己的地址空间,内存分配也会更加灵活。
  • 在 loader 的 Cargo.toml 开启 axstd 的 multitask 属性,可以正常编译,但内核 panic 退出,信息为 current task is uninitialized,定位到 axtask::task::CurrentTask::get() 函数。显然,在初始化时,已经设置了 current task,这里为何报错?经过进一步阅读源码,发现 current task 的指针通过 axhal::cpu::current_task_ptr() 获取,而 axtask/multitask 开启了 percpu 功能,也就是说此时的 current task 指针是一个 percpu 变量。目前,ArceOS 中通过 gp 实现 percpu,这与 riscv 的 ABI 是不符的。具体体现为 hello app 一开始就重新加载了 gp,把内核里维护 percpu 的值冲掉了。因此,为了保持 ABI,我将 percpu 改回了使用 tp 维护(前一个修改的 commit 将 tp 改为 gp)。此时在打开 axstd/multitask 后可以正常退出。(本段来自郝淼大佬的 github 文档)

总结

这一个月的生活还是非常充实的。没有独立完成最后的实习任务有点遗憾。通过这次的学习,让我对 unikernel 有了较为深刻的理解。也跟老师同学们学到了很多。希望这个活动以后越办越好。

练习一&练习二

在我的 build script 中,用 dd 链接多个应用到 apps.bin。偏移 1MB。

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

PLASH_START 开始搜索,每次新的搜索偏移 1024 * 1024,搜索长度 1024。

设计缺陷,32M bin 最多存放 32 个应用,因为假设了每个应用都是 1MB 。

练习三

实现 AppManager,主要记录应用数、应用虚假长度(尾部无效0)和应用真实长度(去除尾部0)。虚假长度用来计算偏移,真实长度用来框定加载程序范围。载入过多无效 0 会导致执行错误。

循环 load_code copy RUN_START+2*(app_num-1)li t2 RUN_START+2*(app_num-1) 执行。

练习四

修复练习三中的 AppManager,使其具备完整、正确的程序搜索、载入、循环执行。

根据本地文件搜索,发现存在 axstd::process::exit 实现 OS 退出。模仿实验再次封装成 fn abi 接口即可。

练习五

感谢unikernel实习交流群小伙伴 dram🎀 提供思路以及小伙伴 一个短篇 转发TA的思路。hello_app 执行多次样例程序会重复操作地址引发 panic。看 LOG 发现 a7 会一直变化,结合两位小伙伴提供的思路,clobber_abi("C") 并额外用临时寄存器存放和加载 abi_table。

练习六

查手册 git@github.com:riscv-non-isa/riscv-asm-manual.git 存在指令能够返回调用地址。所有 app 不再调用 wfi 阻塞, 且 _start() 最后执行 asm!("jalr ra") 返回到 loader 。

?

DDL是第一生产力。—— Tunglies 2023.12.02

前言

发了三周完成第二阶段的前三个实验,并进入到第三阶段,第三阶段一开始一周,在接下来的三周里奋起追赶进度,花了两周半完成了第一周的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, 成功