本节分支:data_limit_3gb

实现用户进程一文的文尾,笔者留下了一个思考题:既然要求用户不能直接访问内核,那为什么不将用户代码段的界限设置为 3GB 呢?正如之前所演示的那样,如果用户代码段的界限为 4GB,则用户就可以随意修改内核,包括直接访问显存:

其实笔者也不知道准确的答案,我粗略参考了 Linux 0.11 的代码,发现 Linux 0.11 似乎也是直接将段界限设置为 4GB 。至于如何规范的防止用户修改内核的方式,咋们以后遇上了再说,现在我们来看看到底能不能通过将数据段界限改为 3GB 来防止用户直接修改内核数据。

首先将用户数据段的段界限改为 0xbfffffff

//文件说明:tss.c
//函数说明:tss_init()
*((struct gdt_desc*)0xc0000938) = make_gdt_desc((uint32_t*)0, 0xbffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH);

还要修改 syscall:

//.....
uint32_t _syscall1(uint32_t no, uint32_t _arg1)
{
    int retval;
    asm volatile
    ("int 0x80"
    : "=a" (retval)
    : "a" (no), "b" (_arg1)
    : "memory"
    );
    return retval;
}
//.....其他syscall也需要改为内联汇编

为什么要将之前的静态变量方式改成扩展内联汇编呢?说到这,笔者不禁流下了悔恨的泪水…当初我也是为了少学这点看起来复杂的扩展汇编,所以使用了全局静态变量这样的“巧计”来代替这种复杂的汇编。没办法,未来某些场景必须使用到它,所以,该来的早晚会来。话说回来,目前而言,由于我们还没有真正的加载用户进程(真正的用户进程是从文件中读取,进而载入内存的,而当前我们使用函数代替用户进程的),所以一切代码和数据(的标号,即地址),不论用户进程还是内核,都被链接成了 3GB 以上,这都是下面这行 makefile 指令造成的:

#文件说明:makefile
$(BUILD)/kernel.bin: $(KERNEL)
	ld  -m elf_i386 $^ -o $@ -Ttext 0xc0001500

这条指令将目前的所有文件全部链接成 kernel.bin,并指定入口的虚拟地址为 0xc0001500 ,因此,所有的函数和全局、静态变量的地址实际上都在 0xc0000000 以上!所以,我们将用户段的界限限制在 3GB 以下时,在用户态中就不能再使用任何全局变量和静态变量!所以这里的 _syscallX 就不能使用静态变量来中转啦!于是只有改成扩展内联汇编喽。
最后,还得修改系统调用入口函数,因为进入中断并不会自动切换 ds、es、fs(只会切换cs),所以咋们必须手动将这几个寄存器修改为内核态的数据段选择子,如下:

;.....
;3 调用子功能处理函数
   push eax
   mov  ax,0x10   ;内核的数据段选择子
   mov  ds,ax
   mov  es,ax
   mov  fs,ax
   pop  eax
   call [syscall_table + eax*4]	    ; 编译器会在栈中根据C函数声明匹配正确数量的参数
   add esp, 12                      ; 跨过上面的三个参数
;.....

ok,来试试看这下能不能直接在用户态访问显存:

image-20230114134607206

显然,发生了 GP 异常,禁止访问。说明这样是能够起到一定的保护作用的。来看看正常运行的效果:

上图中用户只打印了一次,我就使用 Bochs 断点指令将其打断了,因为后面任务切换时引发了程序崩溃,原因就不细说了。这说明通过限制用户数据段的界限来禁止访问内核的这种方式是不可行的。实际上,现代操作系统的内存保护机制并非在段上,而是在页上 ,关于这点,笔者也没有更深的理解,以后再说吧。

文章作者: 极简
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 后端技术分享
自制操作系统
喜欢就支持一下吧