自制玩具操作系统--week9

DAY 0x18

分页前的准备

Linux并没有非常依赖于段表,而是采用的分页结构,将内核和用户程序的段设置为覆盖整个内存区域以此来绕过分段结构,但是显然这个30天操作系统的作者并不打算这样做,它把内核数据段设置为整个内存区域,但是却把代码段设置成0x00280000开始的空间。这对于之后加入类似于Linux的分页模式造成了不便,首先我们想办法把内核数据段改为覆盖整个内存区。

先看一下内存区域图:

1
2
3
4
5
6
7
8
9
10
11
|-------------------------------------------------------|0x00280000
| 512KB 0x00080000Byte |
| bootpack.hrb内容被asmhead.nas整体加载到此区域执行 |0x002fffff
|-------------------------------------------------------|0x00300000
| 64KB 0x00010000Byte |
| 内核栈 |0x0030ffff
|-------------------------------------------------------|0x00310000
| 960KB 0x000f0000Byte |
| bootpack.hrb中的数据区域 |
| 0x003c0000存放了MEMMAN(大小大约0x8000Byte) |0x003fffff
|-------------------------------------------------------|0x00400000(4M)

作者将bootpack.hrb放到了0x00280000处,然后通过farjmp指令JMP DWORD 2*8:0x0000001b(注意作者的第二个段的base是0x00280000),跳到bootpack.hrb的头部的一个写有jmp指令的跳板(一个根据相对pc的偏移量跳转的jmp指令),然后继续跳入bootpack.hrb的代码段。

那么我们首先要解决逻辑地址的问题,原先bootpack.hrb逻辑地址是从0开始的,但是现在我们把第二个段的base从0x00280000改到0,相应的逻辑地址应该加上0x00280000,结合之前分析作者的obj2bim和bim2hrb的逻辑知道,在.rul文件中可以修改链接时代码段的逻辑基地址,我们这里在原来的基础上加上0x00280000,asmhead.nas中的临时段表和dsctbl.cpp根据段表的结构相应的改成覆盖全部内存区域

1
2
set_segmdesc(gdt + ((unsigned int)__KERNEL_DS >> 3), 0xffffffff, 0x00000000, AR_DATA32_RW);
set_segmdesc(gdt + ((unsigned int)__KERNEL_CS >> 3), 0xffffffff, 0x00000000, AR_CODE32_ER);

1
2
3
4
; limit 0xffffffff base:0x00000000 ar:0xc092 
DW 0xffff,0x0000,0x9200,0x00cf
; limit 0xffffffff base:0x00000000 ar:0x409a
DW 0xffff,0x0000,0x9a00,0x00cf

然后这里要将bootpack.hrb的头部的跳板jmp指令进行修改,在内存中先将jmp指令的偏移量减去0x00280000再set回去

1
2
3
4
5
MOV		EBX,0x0028001c
MOV EAX,[EBX]
SUB EAX,0x00280000
MOV [EBX],EAX
JMP DWORD 2*8:0x0028001b

效果如下,能够成功启动,但是执行用户程序hello的时候有一点问题,显然是因为我们动了.rul文件的原因,得把用户程序链接用的.rul文件和和内核程序的分开来

内核代码二级分页

准备

这是网上找的Linux的线性地址到物理地址映射的图

我们这里准备在内核中先做两个映射

  • 这部分512MB空间直接通过偏移量的方式给内核程序用
    0xc0000000-0xdfffffff -> 0x000000000000-0x00001fffffff
  • 这部分最高的512MB空间由于开启了VBE画面模式,图像缓冲存在这里,因此避免麻烦我们直接映射
    0xe0000000-0xffffffff -> 0x0000e0000000-0x0000ffffffff
调逻辑地址

首先,我们要再次调整逻辑地址,这次调整的是bootpack数据段(包括栈区域)和bootpack代码段的地址,在.rul文件中改成这样

1
2
code(align:1, logic:0xC0280024,      file:0x24);
data(align:4, logic:0xC0310000, file:code_end);

asmhead中加载bootpack的地址要做一些修改使它保持在原来的地方,之后我们分页开启后用高逻辑地址来进行访问这部分低物理地址空间
然后就是在jmp到bootpack之前,用汇编来填充我们的页目录和页表了

写入页表

页目录项的结构

页表项的结构

要注意的是,页目录项中存储的地址应该是页表的物理地址而不是页表的线性地址。

线性地址中10位对应页目录项索引,10位对应页表项索引,一个页目录项和一个页表项的大小都是4字节,所以一张页目录大小和一张页表大小都是4*2^10B = 4KB,也就是要占用掉0x1000大小的地址空间。
我们将0x00400000作为页目录起始位置,页目录之后放置页表
这些表一共耗费了1GB/4KB*4+4KB=1MB+4KB的空间,分配了高1G的线性地址,低位的地址在进入bootpack后再进行动态分配。

启用页表

让CR3存储页目录的物理地址,再在R0中设置pg标志位为1。由于x86默认是二级页表,如果想启用其他类型的页表需要先给CR4寄存器的某些位赋值。
开启页表后出现的一个问题是,由于CPU取指也依赖于段表和页表这两级的地址转换。
在执行MOV CR0,EAX后,我们的代码只能用线性地址来访问了,但是现在我们的EIP寄存器中的依然是物理地址(之所以是物理地址是因为我们已经绕过了段表),在我们的设计中,bootpack的线性地址和物理地址是不同的。

为解决这个问题,我们将计就计,在内存的高物理地址区(和bootpack的线性地址数值上相同的那里,写入MOV CR0,EAX这条开启分页的指令的机器码(仅3个字节),然后从asmhead里面jmp到这个地方执行。

1
(0) [0x0000c0280000] 0010:00000000c0280000 (unk. ctxt): mov cr0, eax              ; 0f22c0

执行完这条指令后分页已经开启,EIP采用线性地址,经过页表的转换,实际上执行的代码落在了内存的低物理地址区域里,也就是我们加载的bootpack代码入口0x00280003处。

1
2
3
<bochs:4> n
Next at t=34870451
(0) [0x000000280003] 0010:00000000c0280003 (unk. ctxt): nop ; 90

为了方便编写,加载bootpack后,把前头部前0x1b个字节全部改写成nop指令,这样在高地址开启分页后,下一条指令会落回在一堆nop指令组成的“滑板”上,一直滑到bootpack开头0x1b处的那个jmp指令那里,最终会成功舶入bootpack.cpp的HariMain函数。

通过在HariMain函数的开始加上一条bochsdbg()语句断点来进行验证

修改c层代码

进入bootpack只是开启分页之后的第一步,现在的c层代码是不能使用的,直接continue会立刻崩掉,原因有好几个:

  • 代码中硬编码了太多的低地址,像binfo什么的,还有作者随便拿来存储变量的一部分低地址,都要加上一个偏移量,可以用宏的方式来实现
  • memtest函数的范围问题
  • load_gdtr和load_idtr的地址问题(一开始以为得加载物理地址),后面总是莫名其妙页错误,然后发现开启分页以后去load的话应该load线性地址
  • 代码不规范存在取用null指针的bug,我们还没有映射低地址区域所以会直接页错误PE
  • TASK结构体里面的cr3字段要处理

注意几点:

  • 页错误的中断号是0x0e
  • 改完bug前先不要允许任何中断,否则可能在发生中断时突然crash,导致在调试时会把正确的代码误以为是错误的
  • 结合.map文件和静态分析工具分析错误位置

用户态分页表需要为每一个task建立一个页目录,在task切换的时候赋值cr3,时间关系我们下周再做这个了。