rCore 从第二章到第三章的实践
上周周末刚刚看完 rCore 第三章,心血来潮,感觉自己可以做到从第二章自己手搓升级到第三章的内容。结果光是第三章的协作式操作系统就已经让我汗流浃背了。因此通过这篇文章记录下,实现过程中接连不断遇到的 bug 和 debug 过程。
一、初始化 TaskManager 时的异常
首先,我先按照自己对第三章的理解(原理部分请移步其它文章),实现了基本的代码(就是 task 初始化和切换的部分)。这个过程就花了几个小时,非常地快。
然后当我试图启动操作系统的时候,失败了,症状是在加载完 apps 后,在第一个程序运行之前操作系统卡住了。
为了解决这个问题,我必须要对我的操作系统内核进行调试。但是要直接使用 GDB 对二进制程序进行调试吗?我不认为我能看懂纯汇编代码。因此我必须要能使用 vscode 在源代码上对我在 qemu 上运行的操作系统内核进行调试。
然后我就花了一天去搞清楚 gdb 的原理,以及如何实现我的目标(具体过程见 vscode+gdb+qemu 原理以及使用心得)。
在初始化 TaskManager 的时候,我发现了一个异常:
rustlazy_static! {
static ref TASK_MANAGER: TaskManager = unsafe {
extern "C" {
fn __restore(cx: *const TrapContext);
}
let app_manager = APP_MANAGER.exclusive_access();
let app_num = app_manager.num_app;
let mut tcbs = [TaskControlBlock::uninit(); MAX_APP_NUM];
for i in 0..app_num {
let kernel_stack = KERNEL_STACKS[i];
info!("kernel_stack: {:#x?}", kernel_stack.data.as_ptr() as usize);
tcbs[i].task_status = TaskStatus::Ready;
tcbs[i].task_cx = TaskContext::new(
__restore as usize,
kernel_stack.push_context(TrapContext::app_init_context(get_base_i(i), USER_STACKS[i].get_sp())) as *const _ as usize,
[0; 12],
);
}
TaskManager {
num: app_num,
inner: UPSafeCell::new(TaskManagerInner {
tcbs: tcbs,
current_task: 0,
}),
}
};
}
怎么给每个应用初始化时用的内核栈都是同一个???
还注意到这每个栈还都是在 boot_stack 栈上面。我不是把 KERNEL_STACKS 声明在了.data 段上面了吗?
搞了很久,甚至怀疑过 rust 的静态变量声明是不是有毒,结果最后一看代码。
let kernel_stack = KERNEL_STACKS[i];
bro 以为 KERNEL_STACKS[i] 和 kernel_stack 的地址是一样的。这里因为 kernel_stack 是局部变量,所以只是一份 KERNEL_STACKS[i] 的拷贝,所以自然是在栈上的,和原本的内核栈不同。
错误的原因是被之前 C 语言的知识误导了,C 语言的数组变量的值是数组首元素的地址,而 rust 的数组的值就是指的整个数组。
归根到底还是没有搞清楚变量和地址是什么关系。这里再说一下:
变量只是一种记号,用来表示一段内存空间的值,这种记号只是给编译器看的,程序在运行的时候只有内存的值和内存的地址。
这个错误改完之后,内存栈终于正常了。再次启动。
发现怎么还是进入不了第一个应用???
二、调试 __switch 函数
没办法,继续调试。
这次我进入到了内嵌的汇编函数 __switch,没法再用 vscode 的图形化调试了,使用 gdb 命令在 __switch 里面进行调试。
gdb-exec break *0x80200010
Breakpoint 9 at 0x80200010
-exec x/30i 0x80200010
0x80200010 <__switch>: sd sp,8(a0)
0x80200014 <__switch+4>: sd ra,0(a0)
0x80200018 <__switch+8>: sd s0,16(a0)
0x8020001a <__switch+10>: sd s1,24(a0)
0x8020001c <__switch+12>: sd s2,32(a0)
0x80200020 <__switch+16>: sd s3,40(a0)
0x80200024 <__switch+20>: sd s4,48(a0)
0x80200028 <__switch+24>: sd s5,56(a0)
0x8020002c <__switch+28>: sd s6,64(a0)
0x80200030 <__switch+32>: sd s7,72(a0)
0x80200034 <__switch+36>: sd s8,80(a0)
0x80200038 <__switch+40>: sd s9,88(a0)
0x8020003c <__switch+44>: sd s10,96(a0)
0x80200040 <__switch+48>: sd s11,104(a0)
0x80200044 <__switch+52>: ld ra,0(a1)
0x80200048 <__switch+56>: ld s0,16(a1)
0x8020004a <__switch+58>: ld s1,24(a1)
0x8020004c <__switch+60>: ld s2,32(a1)
0x80200050 <__switch+64>: ld s3,40(a1)
0x80200054 <__switch+68>: ld s4,48(a1)
0x80200058 <__switch+72>: ld s5,56(a1)
0x8020005c <__switch+76>: ld s6,64(a1)
0x80200060 <__switch+80>: ld s7,72(a1)
0x80200064 <__switch+84>: ld s8,80(a1)
0x80200068 <__switch+88>: ld s9,88(a1)
0x8020006c <__switch+92>: ld s10,96(a1)
0x80200070 <__switch+96>: ld s11,104(a1)
0x80200074 <__switch+100>: ld sp,8(a1)
0x80200078 <__switch+104>: ret
打个断点进入了 __switch 函数里面。
我关心的就只是 0x80200044 处 ra(返回地址寄存器)赋值正确没有,因为如果正确的话应该赋值为 __restore 的地址 0x80200130。
gdb-exec break *0x80200044
Breakpoint 10 at 0x80200044
-exec continue
Continuing.
Breakpoint 9, 0x0000000080200010 in __switch ()
-exec si
0x0000000080200014 in __switch ()
-exec p/x $ra
$1 = <not saved>
-exec x/x $a1
0x8022b050 <_ZN65_$LT$os..app..TASK_MANAGER$u20$as$u20$core..ops..deref..Deref$GT$5deref11__stability4LAZY17h0960d1a7e6007b58E+16>: 0x00000000
结果一看,天塌了,a1 指向的内存怎么是 0 啊。
后面我就去看传入的 TaskContext 参数是否正确,发现是正确的。
rust#[derive(Clone, Copy)]
pub struct TaskContext {
ra: usize,
sp: usize,
s: [usize; 12],
}
正当我一筹莫展的时候,我突然想,传入参数的第一个 usize 真的就是 ra 吗?
我索性就看了下 a1 偏移 12 个 8(一个 usize 8 个字节)指向的内存是什么。
gdb-exec x/x $a1 + 12 * 8
0x8022b0b0 <_ZN65_$LT$os..app..TASK_MANAGER$u20$as$u20$core..ops..deref..Deref$GT$5deref11__stability4LAZY17h0960d1a7e6007b58E+112>: 0x80200130
!!我要的 ra 怎么跑 s: [usize; 12] 后面了??
一番调查我才发现,rust 编译器会对结构体自动优化,会优先把大的元素放前面。如果不想要优化的话需要加上 #[repr©]。
加上了 #[repr©] 后,再次尝试跑代码。
结果还是没有跑通,不过至少有进步,我终于成功进入了 __restore 函数。
三、调试 __restore 函数
接下来是第三个 bug。
按照之前的方法,我继续使用 gdb 在 __restore 函数里面调试,看传入的参数正确与否。
结果发现,完全不对,完全不知道 __restore 是从哪里拿的参数。
仔细重读代码:
assembly__restore:
# case1: start running app by __restore
# case2: back to U after handling trap
mv sp, a0
# now sp->kernel stack(after allocated), sscratch->user stack
# restore sstatus/sepc
ld t0, 32*8(sp)
ld t1, 33*8(sp)
ld t2, 2*8(sp)
csrw sstatus, t0
csrw sepc, t1
csrw sscratch, t2
# restore general-purpuse registers except sp/tp
ld x1, 1*8(sp)
ld x3, 3*8(sp)
.set n, 5
.rept 27
LOAD_GP %n
.set n, n+1
.endr
# release TrapContext on kernel stack
addi sp, sp, 34*8
# now sp->kernel stack, sscratch->user stack
csrrw sp, sscratch, sp
sret
发现,__restore 的参数都是先从 a0 中拿到的,而我是直接从 __switch 跳转到的 __restore,没有向 a0 传入任何东西,所以 a0 自然是不对的。
那需要在 __switch ret 之前给 a0 赋值为下一个 task 的内核栈地址吗?
不!因为 __switch 不是每次都要返回到 __restore,也有可能返回到上一个 task 切换时在系统调用中的返回地址,所以我目前的想法是给初次加载的程序单独设置一个汇编函数 __init,它和 __restore 的区别就是它没有第一行的 mv sp, a0,同样放在 trap.S 文件。
然后将 TaskManager 初始化时指定的 __restore 替换为 __init。
再次运行代码。
好耶,第一个 app 终于开始运行了。
坏消息,第二个和第三个代码加载不正确了。。。
不过感觉离成功只有一步之遥了——只有 task 切换有点问题了。
后面再努把力吧。
续。。
这几天反复思考,反复验证。
因为首先想到的是自己的内核写的有问题,就一直调试去看switch前后的寄存器状态对不对,
结果怎么看怎么对。
今天还想写个测试用的系统调用来方便我看寄存器状态
结果碰巧让我发现一件事
就是怎么我没有使用 python3 build.py 编译用户程序,在os目录下面make run 的时候也会自动编译用户程序啊
然后我一看,好家伙,os里面的 Makefile 本来就有自动调用用户目录 Makefile 的脚本
但是user 下面的 Makefile 还是原来的版本,也就是把所有代码按照 0x80400000 起始为路径进行编译的
mad 原来是 用户脚本那边的问题。
把 user 的 Makefile 更新之后,终于正常了。。
💬 评论区
暂无评论,来发表第一条吧。