oscamp 暑期 proj 周纪要

(六) 7.13:

回顾会议内容, 总结任务目标:

  • arceos/tour/* 添加对其他架构 (aarch64/x86_64/loongarch) 的支持
  • arceos/tour 下新增一些例子, 体现 ArceOS 特定功能, 如图形显示功能/文件系统功能/新调度算法功能

把工具链换到最新的:

  • #[naked] 更改为 #[unsafe(naked)]. 裸函数使得编译器不会为函数生成序言和尾声代码(比如保存/恢复寄存器, 设置栈帧等), 操作系统开发的部分场景要求完全控制寄存器细节

  • 同时将 asm! 改为 naked_asm!. 裸函数内一般没有 Rust 代码, 因为会隐含地依赖序言和尾声, 所以几乎都是内联汇编代码

  • naked_asm! 不支持伪指令和宏(虽然我不知道为什么原来这里要写伪指令, 可能是和 arm 统一?), 所以要把那一段全部改写成 RISC-V 汇编, 好在代码量比较少, 如果多了我还真想不出什么方便的方法

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
naked_asm!(
/*
"
// save old context (callee-saved registers)
STR ra, a0, 0
STR sp, a0, 1
STR s0, a0, 2
STR s1, a0, 3
STR s2, a0, 4
STR s3, a0, 5
STR s4, a0, 6
STR s5, a0, 7
STR s6, a0, 8
STR s7, a0, 9
STR s8, a0, 10
STR s9, a0, 11
STR s10, a0, 12
STR s11, a0, 13

// restore new context
LDR s11, a1, 13
LDR s10, a1, 12
LDR s9, a1, 11
LDR s8, a1, 10
LDR s7, a1, 9
LDR s6, a1, 8
@@ -318,6 +319,46 @@
LDR ra, a1, 0

ret",
*/

// --- Save old context (callee-saved registers) ---
// The address of `_current_task` is in a0.
// Offsets are index * 8 bytes (for 64-bit registers).

"sd ra, 0(a0)",
"sd sp, 8(a0)",
"sd s0, 16(a0)",
"sd s1, 24(a0)",
"sd s2, 32(a0)",
"sd s3, 40(a0)",
"sd s4, 48(a0)",
"sd s5, 56(a0)",
"sd s6, 64(a0)",
"sd s7, 72(a0)",
"sd s8, 80(a0)",
"sd s9, 88(a0)",
"sd s10, 96(a0)",
"sd s11, 104(a0)",

// --- Restore new context ---
// The address of `_next_task` is in a1.
"ld ra, 0(a1)",
"ld sp, 8(a1)",
"ld s0, 16(a1)",
"ld s1, 24(a1)",
"ld s2, 32(a1)",
"ld s3, 40(a1)",
"ld s4, 48(a1)",
"ld s5, 56(a1)",
"ld s6, 64(a1)",
"ld s7, 72(a1)",
"ld s8, 80(a1)",
"ld s9, 88(a1)",
"ld s10, 96(a1)",
"ld s11, 104(a1)",

// Return to the new task's execution flow.
// `ret` is a pseudo-instruction for `jalr zero, 0(ra)`.
"ret",
)

由于在裸函数汇编中必须完全手动控制行为, 所以也要删除 options(no_return)

试图迁移到其他架构:

不同的架构在 make run 时通过不同的 SBI 启动 (OpenSBI for RISC-V, SeaSBI for x86_64);

在 qemu 中模拟 x86_64 架构是模拟 q35 机器而不是像模拟 RISC-V 时那样模拟 virt 虚拟化通用机器;

不同架构对 pflash 的要求不同

  • aarch64 下要求 64M 的 pflash, 所以

    1
    $ dd if=/dev/zero of=/home/amiriox/oscamp/arceos/pflash.img bs=1M count=32 seek=32 conv=notrunc

另外 pflash 的起始地址也不一样, 所以必须重新调整

  • x86_64 下较为复杂, 查阅资料发现在 q35 机器中 pflash 被严格用于固件而不能作为通用闪存设备, 且要求两块 Flash 芯片: 只读的 unit0 包含了 UEFI/BIOS 代码, 可读写的 unit1 包含一些固件变量, 后者严格依赖前者.

    所以为 x86_64 多写一行

1
-drive if=pflash,readonly=on,file=$(CURDIR)/dummy_code.img,unit=0

​ 其中 dummy_code.img 是空镜像. 启动后没有任何输出,gdb 调试发现甚至都没进 _start,一直在执行 add %al,(%rax) (一个全 0 的机器码)

​ 我好好想了下, unit=0 里应当是 BIOS 固件代码, 我传了个空镜像进去, 那自然最开始执行的就应该是全 0 代表的机器码了. 换句话说, unit=0 的 pflash 里的 BIOS 优先级更高, 如果有则会优先执行 (此时 QEMU 负责启动的 SeaBIOS 就相当于被忽略的板载 BIOS), 而 unit=1 的 “NVRAM_SLOT” 是不能单独使用的, 根本矛盾在于这个例子需要将 pflash 作为通用闪存使用而 q35 机型禁止这样操作. (相反对于统一 virtio 接口的 virt 机型, pflash 则仅仅是通用存储设备, 可以单纯地作为块设备读入)

​ 如果试图使用 unit=0, 会导致直接进入空的 SBI 反复执行 add %al,(%rax); 使用 unit=1 要求 unit=0 存在 (连续定义), 所以也行不通; 而 q35 不支持更多的 pflash 了.

(一) 7.15:

  1. riscv64-qemu-virt 的 pflash 在 MMIO 段中, ArceOS 作为 Unikernel 启动会 map 内核镜像+MMIO段+空余内存, 所以可以正常访存; 而且此时是还没有处理 PAGE_FAULT 的 Trap Handler 的, 由于 ああrch64-qemu-virt 规定的 pflash 并不在 MMIO 段, 而是 0x0400_0000, 所以并不在 new_kernel_space 的映射范围内, 会触发无法处理的 Trap 的 panic. 所以 new_kernel_space 要额外处理一下这段的映射
  2. u_1_0u_8_0 均能在 aarch64 下正常运行, aarch64 的迁移宣告成功 f91e0b0.
  3. TODO: 需要把额外生成的镜像写进 Makefile (已完成)

(二) 7.16:

使用 ramdisk 代替 pflash. 先创建个空的 ramdisk.img (TODO: 写入 makefile)

1
$ dd if=/dev/zero of=ramdisk.img bs=1M count=32

根据 Multiboot 规范规定, multiboot 相关的信息 (包括 ramdisk 信息) 由 Bootloader 放在 %rbx 寄存器 ( magic 在 %rax ),

multiboot.S 已经把这 magic 和 multiboot 信息传递给 rust_entry 的两个参数了, 所以只需要处理一下即可

1
let mbi_info = *(mbi as *const MultibootInfo);

关于 MultibootInfo:

ArceOS 没有 Multiboot 信息的处理, 所以要按照规范手写结构体:

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
/// Multiboot 1 info struct
/// See:https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct MultibootInfo {
/// `flags[0]`: validity of `mem_lower` `mem_upper`
/// `flags[3]`: validity of `mods_addr` `mods_count`
pub flags: u32,
pub mem_lower: u32,
pub mem_upper: u32,
pub boot_device: u32,
pub cmdline: u32,

pub mods_count: u32,

pub mods_addr: u32,
pub syms: [u32; 4],
pub mmap_length: u32,
pub mmap_addr: u32,
pub drives_length: u32,
pub drives_addr: u32,
pub config_table: u32,
pub boot_loader_name: u32,
pub apm_table: u32,
pub vbe_control_info: u32,
pub vbe_mode_info: u32,
pub vbe_mode: u16,
pub vbe_interface_seg: u16,
pub vbe_interface_off: u16,
pub vbe_interface_len: u16,
}

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ModuleEntry {
pub mod_start: u32,
pub mod_end: u32,
pub cmdline: u32,
_reserved: u32,
}

考虑到这个 info 是一个只能被初始化一次的静态变量, 所以考虑使用 OnceCell

1
once_cell = { version = "^1.5.0", default-features = false, features = ["critical-section"] }

(default-features = falseno_std 环境下必要的, 而必须开了临界区的 feature 才能用 OnceCell.)

由于是内核极早期, 此时在全局分配器初始化之前, 所以用不了 alloc::boxed::Box, 所以 once_cell 引以为傲的 no_std 下的 once_cell::race::OnceBox 也用不了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use once_cell::sync::OnceCell;
pub static BOOT_INFO: OnceCell<MultibootInfo> = OnceCell::new();

pub fn boot_info() -> &'static MultibootInfo {
BOOT_INFO
.get()
.expect("Boot information has not been initialized!")
}

pub unsafe fn set_boot_info(info: MultibootInfo) {
BOOT_INFO
.try_insert(info)
.expect("Boot information can only be initialized once!");
}

但这样还不够, 还需要手写一个临界区的 acquire 和 release 逻辑才行, 否则链接器会抱怨找不到符号

1
2
3
4
5
6
7
8
9
10
unsafe impl critical_section::Impl for MyCriticalSection {
// TODO
unsafe fn acquire() -> () {
//asm!("cli", options(nomem, nostack));
}

unsafe fn release(_restore_state: ()) {
//asm!("sti", options(nomem, nostack));
}
}

获取 multiboot info:

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
#[cfg(target_arch = "x86_64")] {
use axhal::ModuleEntry;
let multiboot_info = axhal::boot_info();
println!("{:?}", &multiboot_info);
let mods_vaddr = phys_to_virt(PhysAddr::from(multiboot_info.mods_addr as usize)).as_usize();

let mods_cnt: usize = multiboot_info.mods_count as usize;
let modules = unsafe {
core::slice::from_raw_parts(mods_vaddr as *const ModuleEntry, mods_cnt)
};
for (i, md) in modules.iter().enumerate() {
let st = md.mod_start as usize;
let mut size = 0;
let cmd_vaddr = phys_to_virt(PhysAddr::from(multiboot_info.cmdline as usize)).as_usize();
let cmd = unsafe { core::ffi::CStr::from_ptr(cmd_vaddr as *const i8).to_str().unwrap() };
for part in cmd.split_whitespace() {
if let Some(val_str) = part.strip_prefix("ramdisk_size=") {
size = val_str.parse::<usize>().unwrap_or(0);
}
}
let ed = st + size;
println!(
"Ramdisk {}: addr = [{:#x}, {:#x}), size = {} KB",
i, st, ed, size / 1024
);
}
}

(qemu 的 bootloader 似乎不怎么好好填结尾地址, 所以得根据传入参数算 size)

1
2
3
4
5
ifeq ($(ARCH), x86_64) 
qemu_args-$(PFLASH) += \
-initrd $(CURDIR)/ramdisk.img \
-append "ramdisk_size=33554432"
...

(五) 7.18:

完成了 virtio-blk 的新 tour/u_9_0. u_7_0 只展示了 block 的大小等信息, u_9_0 展示了 block 的读写

TODO: 添加多块读写和随机访问测试

首先获取信息的初始化和 7 差不多都是 axdriver::init_drivers() (这部分在 ArceOS 的获取设备相关解析过, 之前写的那篇博客也会更新); 对其中一个块 all_devices.block.take_one() 打印设备名 (virtio-blk) 和设备类型 (Block)

测试读写: 构造字符串通过 copy_from_slice 拷贝到缓冲区, 然后通过 AxBlockDevicewrite_block/flush/read_block 测试功能, 最后比较读入与写入是否一致.


(六, 日) 7.10~7.20:

试着根据已有的 ArceOS 代码直接写图形界面:

ax_framebuffer_info 获取 fb_info, 得到显示资源的长宽和一个显存缓冲区, 根据二维到一维的映射关系向其中写入十六进制 RGBA 颜色值, 然后 ax_framebuffer_flush 刷新屏幕.

但是不知道为什么 ax_framebuffer_flush 会始终阻塞, 并且无法显示颜色.

(一..五) 7.21~7.25:

阅读 rCore Tutorial 原版的第九章, 关于 I/O 设备和驱动程序相关的内容

并整理复习: (草稿)从设备到操作系统: 如何编写驱动 | Amiriox’s Storage

(六) 7.26:

阅读 ArceOS 关于驱动部分的源代码:

axdisplay -> axdriver_crates (axdriver_virtio -> axdriver_display) -> virtio-drivers

其中 virio-drivers 这个 crate 就是 rCore Tutorial 第九章的例子

然后用 Bresenham 算法画两点之间直线/矩形, 用中点画圆法绘制圆形, 实现了简单几何图形的绘制


进一步计划:

  1. 整理几何图形绘制算法的博客
  2. 整理一下 Makefile
  3. 考虑为 axdriver_gpu 新增光标位置的接口
  4. 并在操作系统引入 virtio-input 设备实现光标操作
  5. GUI 基础控件
  6. 音频设备驱动和相关的 tour

(日..四) 7.27-7.31:

读 OSTEP

(五..六) 8.1-8.2:

  1. 完成 lvgl 到 ArceOS 的 FFI 调用和交叉编译
  2. 基于 lvgl_sys 实现了一个简单的图形显示demo

(日..三) 8.3-8.6:

阅读 OSTEP 到第 33 章

(四..六) 8.7-8.9:

基于 virtio-drivers crate 编写对于 VirtIO Input 设备的驱动:

  • 定义 VirtIOInputDev<H, T> 类型, 实现 try_new(transport)BaseDriverOps, 维护统一的设备接口
  • InputDeviceOps 主要有 poll_eventhash_events 两个函数, 后者判断是否有新事件, 前者 poll 出新事件
  • has_events 的实现: 由于 virtio-drivers 似乎没有提供对底层 VirtQueue 的访问, 所以只能先设置一个时间缓冲区, 每次 has_eventspoll 一下如果没有事件就返回 false, 否则放入事件缓冲区; 同理 poll 就可以先查一查缓冲区有没有东西.
  • 将难以理解的事件编号等封装为可读性强的枚举

编写符合 ArceOS 设计架构的 axinput 模块:

  • 全局静态变量 LazyInit<Mutex<AxInputDevice>> 表示 MAIN_INPUT
  • 封装 poll_event, has_event 等接口为更 Rusty 的形式
  • 最后再通过 arceos_api 封装为全局的函数, 类似 ax_framebuffer_info

编写 u_12_0 展示对鼠标/平板设备的支持:

  • 通过 arceos_api 暴露出的 ax_input_* 轮询检查并获取事件, 如果没有就 (TODO) yield
  • 对事件类型进行模式匹配, 分类处理 (同时进行绘图):
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
while loop_count < max_loops {
if ax_input_has_events() {
//print!("Input events available: ");

if let Some(event) = ax_input_poll_event() {
match event.get_type() {
Some(InputEventType::Relative) => {
if event.code == RelativeAxis::X as u16 {
panic!();
let mouse_dx = event.value; // as i32;
x += mouse_dx;
// println!("Quick mouse X movement: {}", mouse_dx);
} else if event.code == RelativeAxis::Y as u16 {
let mouse_dy = event.value; // as i32;
y += mouse_dy;
// println!("Quick mouse Y movement: {}", mouse_dy);
}
}
Some(InputEventType::Absolute) => {
if event.code == AbsoluteAxis::X as u16 {
let tablet_x = event.value;
x = tablet_x;
// println!("Quick tablet X position: {}", tablet_x);
} else if event.code == AbsoluteAxis::Y as u16 {
let tablet_y = event.value;
y = tablet_y;
// println!("Quick tablet Y position: {}", tablet_y);
}
}
Some(InputEventType::Key) => {
pressed = event.value == 1;
}
Some(InputEventType::Sync) => {
if let Some(ref mut screen) = screen {
let (h, w) = (screen.height, screen.width);
let ax = (x as u64 * (w as u64 - 1) + 16383) / 32767;
let ay = (y as u64 * (h as u64 - 1) + 16383) / 32767;

if pressed {
screen.draw_line(
0x006400,
(0, 0),
(ax.try_into().unwrap(), ay.try_into().unwrap()),
);
ax_framebuffer_flush();
println!("Moved to: ({}, {})", ax, ay);
}
} else {
if pressed {
println!("Moved to: ({}, {})", x, y);
}
}
}
_ => {}
}
}
} else {
// println!("No fucking events.");
// yield?
std::thread::sleep(std::time::Duration::from_millis(10));
}

loop_count += 1;
}