diff --git "a/source/_posts/2024\346\230\245\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\346\200\273\347\273\223\346\212\245\345\221\212--\347\254\246\344\277\212\346\235\260.md" "b/source/_posts/2024\346\230\245\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\346\200\273\347\273\223\346\212\245\345\221\212--\347\254\246\344\277\212\346\235\260.md" new file mode 100644 index 0000000000..7953fb7c57 --- /dev/null +++ "b/source/_posts/2024\346\230\245\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\346\200\273\347\273\223\346\212\245\345\221\212--\347\254\246\344\277\212\346\235\260.md" @@ -0,0 +1,138 @@ +--- +title: '2024春开源操作系统训练营第三阶段项目一总结报告--符俊杰' +date: 2024-06-29 4:17:48 +categories: + - report +tags: + - author: xhyf77 + - Spring Camp 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实现如下: +``` +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```,他会进行多次输出,并且输出的字符个数逐渐减少。如: +```"Hello_world"```,```"llo_world"```,```"o_world"```....最后到空字符,然后就开始仔细查看writev的代码,发现代码的返回值出了问题,修改后就可以正常输出了。 +修改后: +``` +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. 在支持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) + } +``` +但是由于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 +``` +因此在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 +-- +低地址 +``` + +5. 由于要支持多地址空间,因此我需要给每个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 \ No newline at end of file