一、init进程
osh 和 init应该属于用户程序,但是目前我们直接编译进内核了。到目前我们的系统已经可以执行程序了,因此需要将这两个程序独立出来,作为用户程序放到用户空间去执行。
1.1、独立osh
osh作为shell程序,首先需要修改他的主函数为main,因为它要作为一个独立的用户程序来运行。其次我们是在init进程中调用了osh,在内核中编译时可以直接调用,但是作为用户程序之后需要使用系统调用来执行。修改如下:
static void user_init_thread()
{
while (true)
{
u32 status;
pid_t pid = fork();
if (pid)
{
pid_t child = waitpid(pid, &status);
printf("wait pid %d status %d %d\n", child, status, time());
}
else
{
int err = execve("/bin/osh.out", NULL, NULL);
printf("execve /bin/osh.out error %d\n", err);
exit(err);
}
}
}
如上所示,我们应该通过execve系统调用来执行osh程序。调整到用户空间后,就可以多次执行osh了。
1.2、独立init进程
init进程也需要运行在用户内存空间,因此首先需要编写init程序,如下:
int main()
{
if (getpid() != 1)
{
printf("init already running...\n");
return 0;
}
while (true)
{
u32 status;
pid_t pid = fork();
if (pid)
{
pid_t child = waitpid(pid, &status);
printf("wait pid %d status %d %d\n", child, status, time());
}
else
{
int err = execve("/bin/osh.out", NULL, NULL);
printf("execve /bin/osh.out error %d\n", err);
exit(err);
}
}
return 0;
}
init进程的逻辑基本没有变化,只是独立成了一个程序文件。接下来在从内核态进入用户态时,就不需要需要内联汇编来通过jump跳转了,直接使用内核函数sys_execve调用即可
void task_to_user_mode()
{
task_t *task = running_task();
....
int err = sys_execve("/bin/init.out", NULL, NULL);
panic("exec /bin/init.out failure");
}
到此,用户态和内核态就已经完全隔离了。接下来就可以对内核的内存进行保护,防止用户程序使用内核内存。
二、内核内存保护
目前我们定义全局描述符时,定义了用户特权级也是完整访问整个4G的内存。现在用户程序也可以访问内核的内存,这样极不安全,所以需要一种机制来防止用户程序,无意或有意的访问和修改内核内存。我们先写一个用户程序来访问内核内存:
#include <onix/stdio.h>
int main()
{
char *video = (char *)0xB8000;
printf("char in 0x%X is %c\n", video, *video);
*video = 'E';
printf("changed to E\n");
return 0;
}
如上所示,我们给显存的第一个位置改写为字符E,这里因内核的映射是虚拟地址映射到相同的物理地址,因此实际访问的物理内存就是0xB8000。 当shell中执行err命令,控制台的第一个字符被修改为了E。说明用户程序是可以操作系统内存的。
将程序映射参数 user 置为 0,表示该页只能由内核访问。因此在内核内存映射时设置页表想的user元素为0,代表仅超级用户可以使用。
typedef struct page_entry_t
{
u8 present : 1; // 在内存中
u8 write : 1; // 0 只读 1 可读可写
u8 user : 1; // 1 所有人 0 超级用户 DPL < 3
u8 pwt : 1; // page write through 1 直写模式,0 回写模式
u8 pcd : 1; // page cache disable 禁止该页缓冲
u8 accessed : 1; // 被访问过,用于统计使用频率
u8 dirty : 1; // 脏页,表示该页缓冲被写过
u8 pat : 1; // page attribute table 页大小 4K/4M
u8 global : 1; // 全局,所有进程都用到了,该页不刷新缓冲
u8 shared : 1; // 共享内存页,与 CPU 无关
u8 privat : 1; // 私有内存页,与 CPU 无关
u8 readonly : 1; // 只读内存页,与 CPU 无关
u32 index : 20; // 页索引
} _packed page_entry_t;
当用户尝试访问内核内存时,就会触发页错误,我们在页错误里进行判断访问的内存,如果访问时内核内存或者超过栈顶的内存,直接打印段错误。
除此以外,由于用到了平坦模型,所以内核必须检测用户程序系统调用传入的 内存空间,一定在用户的内存空间,否则也需要报错。但是这样在开发阶段有一个缺点,就是不方便调试。可以添加 ONIX_DEBUG 宏,判断是否是调试状态,然后分别对不同的状态进行处理。如果开启了DEBUG,则将init和osh编译进内核。
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付