Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2024春季符俊杰项目一第三阶段总结 #470

Merged
merged 3 commits into from
Jul 2, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Loading