基于mykernel 2.0编写一个操作系统内核
Posted 刹那很好
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于mykernel 2.0编写一个操作系统内核相关的知识,希望对你有一定的参考价值。
实验基于https://github.com/mengning/mykernel完成
一.配置虚拟机QEMU
安装过程不再阐述,参考上方链接即可
为了使得qemu能够正常进行debug,需要设置相关内核选项
# 关闭KASLR,否则会导致打断点失败 Processor type and features ---->
之后终端输入make,编译。
接下来配置内存根文件系统,需要用到busybox。
首先,取消busybox的动态链接,然后编译,将编译后_install下的文件,以及dev目录下的文件打包制作根文件系统。
由于默认的内核命令行上有 init=/linuxrc, 因此,在文件系统被挂载后,运行的第一个程序是根目录下的 linuxrc。 这是一个指向/bin/busybox 的链接,也就是说,系统起来后运行的
第一个程序也就是 busybox 本身。
启动qemu,
二,代码具体分析
首先定义进程控制块PCB
PCB主要包含下面几部分的内容:
1. 进程的描述信息,比如进程的名称,pid,
2. 处理机的状态信息,当程序中断是保留此时的信息,以便CPU返回时能从断点执行
3. 进程调度信息,比如进程状态,优先级等等
4. 进程控制和资源占用,同步通信机制,链接指针(指向队列中下一个进程的PCB地址)
1 typedef struct PCB{ 2 int pid;//进程id 3 //进程状态 4 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ 5 char stack[KERNEL_STACK_SIZE];//每个进程都有自己独立的栈空间 6 /* CPU-specific state of this task */ 7 struct Thread thread;//线程 8 unsigned long task_entry;//函数入口地址 9 struct PCB *next;//下一个进程控制块 10 }tPCB;
接下来定义线程
struct Thread { unsigned long ip;//指向的是函数地址 unsigned long sp;//指向栈底 };
由线程的定义可见,线程自己不拥有自己的地址空间,它使用的是进程的栈,也就是线程和进程共享数据。
接下来初始化所有的pcb,每个进程的pcb的入口地址,以及线程的ip,其值都是my_process函数的地址。
int pid = 0;//0号进程 int i; /* Initialize process 0*/ task[pid].pid = pid; task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */ //任务入口,即my_process函数的地址 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;//把my_process函数的地址赋给了ip task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; task[pid].next = &task[pid];//自己指向自己 /*fork more process */ for(i=1;i<MAX_TASK_NUM;i++) { memcpy(&task[i],&task[0],sizeof(tPCB));// task[i].pid = i; task[i].state = -1; task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];//sp指向栈底 task[i].next = task[i-1].next;//当前的next指向上一个,上一个的next指向当前。最后的pcb的next指向第0个pcb,形成环形 task[i-1].next = &task[i]; }
接下来,准备进程切换/调用
1.将调用者的ebp压栈处理,保存指向栈底的ebp的地址(方便函数返回之后的现场恢复),此时esp指向新的栈顶位置;
push ebp
2.将当前栈帧切换到新栈帧(将ebp值装入esp,更新栈帧底部), 这时ebp指向栈顶,而此时栈顶就是old esp ,
mov esp, ebp
3.之后将my_process的地址入栈,ret执行后rip保存my_process的地址,之后就会进入这个函数
asm volatile( "movq %1,%%rsp\\n\\t" /* set task[pid].thread.sp to rsp */ "pushq %1\\n\\t" /* push rbp */ "pushq %0\\n\\t" /* push task[pid].thread.ip */ "ret\\n\\t" /* pop task[pid].thread.ip to rip */ : : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ );
进程切换的过程与上面的过程类似,都是先保存当前栈底rbp,这是为了之后的返回,
接着调整把当前线程thread的ip,和sp保存到pcb中
然后下一个pcb的ip,即函数地址,压栈,因为rip无法直接操作
然后rsp指向了新的堆栈的栈顶
asm volatile( "pushq %%rbp\\n\\t" /* save rbp of prev */ "movq %%rsp,%0\\n\\t" /* save rsp of prev */ "movq %2,%%rsp\\n\\t" /* restore rsp of next */ "movq $1f,%1\\n\\t" /* save rip of prev */ "pushq %3\\n\\t" "ret\\n\\t" /* restore rip of next */ "1:\\t" /* next process start here */ "popq %%rbp\\n\\t" : "=m" (prev->thread.sp),"=m" (prev->thread.ip) : "m" (next->thread.sp),"m" (next->thread.ip) );
为了清楚知道rsp,rbp的变化情况,根据vscode的调试,方便起见,把stack的size调整到8*8
一开始,4个pcb的stack全都是空,那么在运行上述代码的时候,当rsp指向next的stack栈顶时,stack全空,如图
那么当执行popq %%rbp的时候, rsp-8的位置,也就是本该存放rbp的位置,全0,那么在弹栈的时候,rbp的值应该也为全0
为了验证rbp的值到底是多少,修改popq %2,也就是弹栈到next->thread.sp
不知道什么原因,therad.sp的值在前后并未发生改变。。。
本来以为popq可能没有执行,但是将popq %%rbp删除后,第一次循环正常运行,当再次循环到0号pcb的时候就发生错误了
但是可以看到新堆栈的值已经改变
后来查询,在vscode调试控制台输入-exec info registers可以直接查询寄存器的值
在进入进程切换前
rbp=0xffffffff82b57b00,rsp=0xffffffff82b5bb3f
切成切换后:
rsp的值确确实实被改变了,但是,rbp的值并没有变化(疑惑
以上是关于基于mykernel 2.0编写一个操作系统内核的主要内容,如果未能解决你的问题,请参考以下文章