0%

2024春开源操作系统训练营第三阶段项目一总结报告--符俊杰

第三阶段总结

前期实验

第三阶段前期老师带领我们基于Arceos做了两周的实验

  • 练习1:支持彩色打印 println!
    较为简单,且有多种解法,可以在项目不同的层次进行修改来支持彩色打印
  • 练习2:支持HashMap数据类型
    在官网找到HashMap的源码然后进行一定增删就可以实现,因为初学rust,有些高级用法很生疏,改起来有点费劲。
  • 练习3:为内存分配器实现新的内存算法
    我的实现比较粗暴,有些内存空隙直接不要了
  • 练习4:解析dtb并打印
    在crate.io中有现成的解析dtb的crate,但是需要进行一定修改才能满足任务要求
  • 练习5:抢占式调度算法
    这个任务比较简单,改一下crates/scheduler/src/fifo.rs就好了

第二周的五个实验如下:

  • 实验一:从外部加载应用
  • 实验2:把应用拷贝到执行区域并执行
  • 实验3:通过 ABI 调用 ArceOS 功能
  • 实验4:正式在 App 中调用 ABI
  • 实验5:支持内核和应用分离的地址空间及切换

对于第二周的五个实验总体来说就是把外部应用APP写入PFLASH中,然后在内核中写一个APP加载器,为APP初始好寄存器后跳转到APP执行,同时支持多地址空间以及给APP传递内核API_TABLE的地址。第二周让我印象深刻的是跳转到APP执行前对寄存器的初始化很重要,如果初始化不到位可能会导致运行APP的时候出现各种错误。同时实验4也让我学到了如何以通过传参的方式支持APP调用内核API。

之后就是进入真正的项目阶段了,项目阶段是在lkmodel上开发,lkmodel相比Arceos我个人感觉难度还是有一定提升,且前两周刚熟悉完Arceos的代码和模块(T_T),一开始还是挺畏难的。不过最后在做的过程中慢慢熟悉lkmodel后也就不那么害怕了。

进入真正的项目阶段

musl入手

由于musl编译出来的app系统调用要比glibc简单的多且少得多,因此我先尝试支持用musl-gcc编译的hello_world程序。
经历大概如下:

  • 支持加载hello_world.bin程序并执行
  • 支持对elf的解析
  • 支持对hello_world.elf程序的执行

支持glibc

要支持glibc要实现和空实现一些syscall,让我印象比较深的是writev_syscall
原本lkmodel的writev_syscall实现如下:

1
2
3
4
5
6
7
8
9
10
pub fn writev(fd: usize, iov_array: &[iovec]) -> usize {
assert!(fd == 1 || fd == 2);
for iov in iov_array {
debug!("iov: {:#X} {:#X}", iov.iov_base, iov.iov_len);
let bytes = unsafe { core::slice::from_raw_parts(iov.iov_base as *const _, iov.iov_len) };
let s = String::from_utf8(bytes.into());
error!("{}", s.unwrap());
}
iov_array.len()
}

但是运行的时候发现没有输出,于是我简单的在writev里面加了一个early_console::write_bytes(bytes);。但发现输出的时候遇到了奇怪的现象,例如输出一个Hello_world,他会进行多次输出,并且输出的字符个数逐渐减少。如:

1
修改后:

pub fn writev(fd: usize, iov_array: &[iovec]) -> usize {
assert!(fd == 1 || fd == 2);
let mut total_bytes_written = 0;
for iov in iov_array {
//debug!(“iov: {:#X} {:#X}”, iov.iov_base, iov.iov_len);
let bytes = unsafe { core::slice::from_raw_parts(iov.iov_base as *const _, iov.iov_len) };
let s = String::from_utf8(bytes.into());
early_console::write_bytes(bytes);
total_bytes_written += iov.iov_len;
}
total_bytes_written
}

1
2
3
4
5
6

#### 发现的问题及解决思路
1. 在支持glibc的时候由于glibc调用了一些task相关的syscall,需要拿到当前的task_sched_info,但是每次都拿不到(None),就很奇怪,明明也进行了task::init(),为什么拿不到呢?后面一直跟踪拿task_sched_info的源码才发现是通过读gp寄存器拿的,即项目中的```pub fn gen_read_current_raw(symbol: &Ident, ty: &Type) -> proc_macro2::TokenStream ```存也是把task_sched_info的地址存在gp寄存器上```pub fn gen_write_current_raw(symbol: &Ident, val: &Ident, ty: &Type) -> proc_macro2::TokenStream ```。然后我就怀疑gp寄存器在app运行时被改变了,于是查看qemu.log,果然一开始就动了gp寄存器。于是我采用一个全局静态变量来存当前的task_sched_info,这样就可以顺利拿到了。
2. 一开始没有清理.bss,导致运行时各种奇怪的问题。其实一开始没清理.bss是因为我在做payload的时候是创建了一个32M的空文件(全0),然后把app.elf文件写进去,我心想写的时候是全0,我就不用清理了吧,但在debug的时候发现利用lkmodel下的elf crate来对app.elf进行解析的时候竟然把.bss那块儿区域写了一些数据,就因为偷了个懒,导致又浪费一大把时间debug。
3. 在支持vfork时,vfork的子进程由于要copy父进程的进入trap_handler时的寄存器信息以正确返回。在子进程返回时发现其trap上下文的sepc寄存器一直是0,且其他寄存器的值也不正常,经过调试发现是copy父进程的trap上下文时出现了问题,最后发现问题如下:
pt_regs_addr()函数(即拿到trap上下地址的函数)原来是
pub fn pt_regs_addr(&self) -> usize {
    self.kstack.as_ref().unwrap().top() - align_down(TRAPFRAME_SIZE, STACK_ALIGN)
}
1
2
3
但是由于align_down导致减去的值不是TRAPFRAME_SIZE
但在汇编中却是用self.kstack.as_ref().unwrap().top() 直接减去TRAPFRAME_SIZE
如下:

.Ltrap_entry_s:
addi sp, sp, -{trapframe_size}
SAVE_REGS 0
mv a0, sp
auipc a1, 0 # Load the upper 20 bits of the PC into a1
addi a1, a1, 12
call riscv_trap_handler
RESTORE_REGS 0
sret

.Ltrap_entry_u:
addi sp, sp, -{trapframe_size}
SAVE_REGS 1
mv a0, sp
li a1, 1
call riscv_trap_handler
addi t0, sp, {trapframe_size} // put supervisor sp to scratch
csrw sscratch, t0
RESTORE_REGS 1
sret

1
2
3
因此在rust中是用pt_regs_addr()获取trap_frame地址然后进行读写操作是和汇编里面的存储位置不匹配。
4. 不知道是不是因为静态链接的原因,我这边argc,argv,env在栈上的排布和ppt不一样,用ppt的方式排布app拿不到argv
下面是我在其他地方看到的另一种排布方式,用此方式排布app可以正确拿到argv参数

高地址

0

envp[1]
envp[0]
0

argv[1]
argv[0]
argc

低地址

```

  1. 由于要支持多地址空间,因此我需要给每个app重新分配一个页表,并在这个基础上为app分配内存。同时我为了复用lkmodel下的代码,我采用user_mode_thread来创建一个新的进程,因此我需要对user_mode_thread代码进行修改,最重要的是修改地址空间那块,我需要为app准备一个新的地址空间,而不是和内核共享同一个地址空间。于是我考虑更改mm_copy模块,但后面想到不能直接改这里,因为mm_copy这个函数会被多个模块调用,我改了可能其他模块就跑不通了。于是我决定在user_mode_thread外面为app的task.mm重新赋值(即建立新的地址空间),但神奇的事情发生了,赋值后会导致内核的页表(即之前task.mm指向的区域)被回收(Arc指针计数并不为0),之后为了验证是否是我修改了其他模块导致该问题,于是我重新git clone下来了原仓库并在原仓库基础上进行测试,发现还是会出现相同的问题。最后我通过其他手段绕过了该问题。

    总结

    这一个月的项目实习使我debug能力又有了提升同时更加深刻的理解了musl-gcc,glib-gcc的区别,同时也更加深刻的了解了Unikernel。同时也跟同学老师们学到了很多东西。

仓库链接

https://github.com/xhyf77/lkmodel/tree/dev