Norlit OS —— 自制操作系统 第2章 保护模式

2  保护模式

2.1         何为保护模式

Q:保护模式是神马玩意儿?好吃吗?多少钱一斤?

A:好吃,10元一斤,蓝色品质,预购从速,先到先得!

我们先不开玩笑了,切入正题。电脑加点以后进入的模式是实模式。实模式最高只能访问1MB内存,16位寄存器。而我们现在用的都至少有32位的处理器,甚至64位,内存也上G,可不能浪费了!如何才能利用这些呢?答案是保护模式。

顾名思义,保护模式为系统提供了强有力的保护机制,所有的权力都被中央集权,为操作系统所掌控。最重要的是,我们不再需要段+偏移才能达到1MB,而是一个寄存器就有4GB的寻址能力!你心动了吗,那还就赶快拨打电话订购吧!

不过,电话是多少呢?哦,不,如何进入保护模式呢?我们得先来看看枯燥的概念GDT

 

2.2         全局描述符(GDT

2.2.1 段描述符结构

 

这就是一个段描述符。其中,段基址总长32位,段界限总长20位,还有剩余的12位。我们先来介绍一下这12位。

GGranularity颗粒度标志,如果这位为1,则段界限以4KB为单位,否则以1字节为单位。这个标志位非常重要,我们的4GB寻址能力需要它!

D/B指明这个段中操作的长度。如果这位被设置,那么这个段是32位段,否则是16位段。这个标志也很重要!

L64位标志,只在IA-32e模式中使用,现在我们大可不管它。

AVLAvailable,可用位,可以为操作系统软件使用。

PPresent,存在位,如果为0表示该段不在内存中。

S:为1是代码数据段,否则是系统段。

DPLDescriptor Privilege Level特权级标志

TYPE:最复杂的东西,涉及到段的类型,不过可以读protect.inc里的注释来了解。

段描述符虽然被Intel大叔搞得乱七八糟,但还是非常有用的。通过基址和界限,我们可以确定一个内存区域并使得代码只在这个区域内寻址,达到保护其他内存地址的作用。这就是保护模式的来历。对了,描述全局的短描述符叫做GDT

可是这个东西放在哪里呢?这个64位长的怪东西塞不进16位的段寄存器,所以我们把它放在内存里,用一个指针来访问它。不过,指针也是32位的,仍然塞不进段寄存器。这时候Intel大叔发明了一个寄存器叫Gdtr,存放指针,而段寄存器则作为索引,相当于构造了一个数组。不过,Gdtr里的内容并不完全是指针。它是一个6字节的数据结构,包含了一个基址和一个界限。

BYTE5

BYTE4

BYTE3

BYTE2

BYTE1

BYTE0

32位基地址

16位界限

2.2.2 GDTR

 

BYTE1

BIT7-BIT2

BIT1

BIT0

GDT索引

TI

RPL

2.2.3 段寄存器的新用途

 

TI是用来区分全局和局部的,RPL则规定了访问权限。计算机共有0-3三个Ring,对应三个权限级别,所以RPL的取值也有从0-30是最高权限,3是最低。

好了,我们了解了GDT,现在开始动手吧。最麻烦的是段描述符,我写了个宏来自动化完成这一切。

 

;=====================================================

; SegementDescriptor(u32 base, u20 limit, u12attr);初始化段描述符

;-----------------------------------------------------

; Entry:

;   - arg0 -> 段基址

;   - arg1 -> 段界限

;   - arg2 -> 属性

; Exit:

;   - 填充一个段描述符

%macroSegementDescriptor 3

    dw %2 & 0FFFFh                         ; 段界限第16

    dw %1 & 0FFFFh                         ; 段基址第24

    db (%1 >> 16) & 0FFh                   ; 同上

    dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 扩展字节

    db (%1 >> 24) & 0FFh                   ; 段基址高8

%endmacro

代码 2.2.1 (chapter2/a/boot/include/protect.inc)

 

哈哈,以后我们只需要用“SegmentDescriptor 基址,界限,属性就可以访问了。不过,我们还得定义一些常量。

 

;=====================================================

; 段选择子属性常量Segment Selector Attribute, SSA

;-----------------------------------------------------

SSA_RPL0    EQU 0  ;RPL = 0

SSA_RPL1    EQU 1  ;RPL = 1

SSA_RPL2    EQU 2  ;RPL = 2

SSA_RPL3    EQU 3  ;RPL = 3

 

SSA_TIG     EQU 0  ;TI = GDT

SSA_TIL     EQU 4  ;TI = LDT

;-----------------------------------------------------

 

;=====================================================

; 段描述符属性常量Segment Descriptor AttributeSDA

;-----------------------------------------------------

SDA_D       EQU 4000h  ; D/B 位设置为32

SDA_G       EQU 8000h  ; G位颗粒度设置为 4KB

SDA_P       EQU 80h    ; P位设置为存在

SDA_S       EQU 10h    ; S为设置为代码数据段

;-----------------------------------------------------

; 特权级

;-----------------------------------------------------

SDA_DPL0    EQU 00h   ; DPL = 0

SDA_DPL1    EQU 20h   ; DPL = 1

SDA_DPL2    EQU 40h   ; DPL = 2

SDA_DPL3    EQU 60h   ; DPL = 3

;-----------------------------------------------------

; 存储段描述符类型值说明

;-----------------------------------------------------

SDA_A       EQU 1h ;已访问

SDA_R_W     EQU 2h  ;可写数据段/可读代码段

SDA_CO      EQU 4h ;一致代码段/向下扩展数据段

SDA_C       EQU 8h ;可执行(代码段)

; 组合说明

; 数据段:必须能读,不允许设置SDA_CSDA_R_W控制是否能写,SDA_CO控制是否是向下扩展。

; 代码段:必须能执行,必须设置SDA_CSDA_R_W控制是否能读,SDA_CO控制是否一致。

SDA_FLAT_C  EQU SDA_G|SDA_D|SDA_P|SDA_S|SDA_C|SDA_R_W; 平坦代码段

SDA_FLAT_D EQU SDA_G|SDA_D|SDA_P|SDA_S|SDA_R_W      ; 平坦数据段

代码 2.2.2 要用到的常量(chapter2/a/boot/include/protect.inc)

 

说了这么多,我们也得开始行动了。我们将在LOADER中进入保护模式。

 

;=====================================================

; GDTPointer(u32 base, u16 limit);初始化GDT指针

;-----------------------------------------------------

; Entry:

;   - arg0 -> GDT基址

;   - arg1 -> GDT界限

; Exit:

;   - 填充一个GDT指针

%macro GDTPointer 2

    dw %2 - 1                          ; GDT界限

    dd %1                              ; GDT基址

%endmacro

代码 2.2.3 初始化GDT指针的宏(chapter2/a/boot/include/protect.inc)

 

2.3         保护模式降临

需要进入保护模式,我们需要首先打开A20。在Intel80386之前的电脑上,地址线只有20位,超过的话就会发生卷回,从0开始重新寻址。这严重干扰了我们的32位寻址能力。因此,我们使用键盘控制器上的第20根线来控制,这就是A20。不过键盘控制器又复杂又慢,所以Intel大叔们又创造了另外两种方式来控制它,就是BIOS A20Fast A20

         我们使用Fast A20,这种方式不是万能的,但是对于如今的电脑来说都是管用的。

FastA20使用第0x92号端口的第1位来控制A20地址线。所以我们只要:

 

    in      al, 92h

    or     al, 010b

    out    92h, al

代码 2.3.1 开启A20(chapter2/a/boot/loader.asm)

 

就可以打开A20地址线。接下来要进行的是GDT的初始化操作。我们只初始化5GDT,第0个留空,剩下两个系统用代码数据段和两个用户级别的代码数据段。

        

align 64 ;8字节对齐

 

GDT_EMPTY:     SegmentDescriptor 0,0,0                ;空描述符

GDT_FLAT_C:    SegmentDescriptor 0,0xFFFFF,SDA_FLAT_C;平坦系统代码段

GDT_FLAT_D:    SegmentDescriptor 0,0xFFFFF,SDA_FLAT_D ;平坦系统数据段

GDT_FLAT_C_USR:SegmentDescriptor 0,0xFFFFF,SDA_FLAT_C|SDA_DPL3

;平坦用户级代码段

GDT_FLAT_D_USR:SegmentDescriptor 0,0xFFFFF,SDA_FLAT_D|SDA_DPL3

;平坦用户及数据段

 

GdtPtr:

GDTPointer GDT_EMPTY + LOADER_SEGPHYADDR, $ - GDT_EMPTY

 

SEL_FLAT_C      EQU GDT_FLAT_C - GDT_EMPTY

SEL_FLAT_D      EQU GDT_FLAT_D - GDT_EMPTY

SEL_FLAT_C_USR  EQU GDT_FLAT_C_USR - GDT_EMPTY + SSA_RPL3

SEL_FLAT_D_USR  EQU GDT_FLAT_D_USR - GDT_EMPTY + SSA_RPL3

代码 2.3.2 数据初始化(chapter2/a/boot/loader.asm)

对了,其中SEL_开头的是段寄存器中要填充的内容,我们把它称作选择子。

好了,一切准备工作已经就绪,我们现在就要打开保护模式的开关,进入新大陆。

 

    mov    eax, cr0

    or     eax, 1

mov     cr0, eax

代码 2.3.3扳机(chapter2/a/boot/loader.asm)

 

其中cr0是控制寄存器,它的第0位就是我们的扳机。

CR0的第0位称为CR0.PE,保护模式使能位。

不过,我们现在在32位模式了,我们的段寄存器还未更新。而且CPU的指令预取也需要更新。这时候直接对cs操作很危险,我们还需要一个远跳转来完成这一切。

 

jmp    dword SEL_FLAT_C:(LOADER_SEGPHYADDR+LABEL_START_PM)

代码 2.3.4 Fire!!! (chapter2/a/boot/loader.asm)

 

这里需要注意我们要加上LOADER_SEGPHYADDR因为接下来的地址不再分段,而是平坦的了,这个值在loader.inc下被定义为0x90000。下面,我们就可以加上[BITS 32]然后进入我们的保护模式世界了。

        

2.4         题外话:保护模式下的显示

我们进入保护模式以后,BIOS中断就不能再用了。实际上,在我们初始化IDT(以后会讲)之前,所有中断都会对我们的操作系统致命。所以我们不能再用显示服务来显示文字了。那怎么办的?答案是写显存。在文本模式下,显存被映射到了0xB8000处。我们只需要操作那里就可以了。

在那之前,我们先初始化段寄存器。

 

LABEL_START_PM:

    mov     ax,SEL_FLAT_D_USR

    mov    gs, ax

    mov    ax, SEL_FLAT_D

    mov    ds, ax

    mov    es, ax

    mov    ss, ax

   

    mov    esi,LOADER_SEGPHYADDR + String

    call   DispStrPM

   

hlt

代码 2.4.1 加载段寄存器(chapter2/a/boot/loader.asm)

 

初始化完后,这个DispStrPM就是我们关注的内容。

 

DispPosdd (80*0+0)*2+0xB8000   ;Data中的定义

……

[Section Bit32]

[BITS 32]

……

;=====================================================

; DispStrPM(char* addr);显示字符串

;-----------------------------------------------------

; Entry:

;   - ESI -> 要显示的字符串

; registers changed:

;   - ESI, EAX

DispStrPM:

    push   ebp

 

    mov    ebp, [DispPosPM] ; 获得当前显示位置

    mov    ah, 0Fh           ; 字体颜色

.loop:

    mov    al, [esi]         ; 1字节

    inc    esi                ; 指针移动到下一字节

    or     al, al               ; if(al==0)?

    jz     .end               ; 退出循环

    cmp    al, 13             ; 回车?

    je     .13

    cmp    al, 10             ; 换行?

    je     .10

    mov    [ebp], ax         ; 填充字符

    inc    ebp

    inc    ebp

    jmp    .loop

.13:

    push   bx

   

    mov    eax, ebp

    sub    eax, 0xB8000

    mov    bl, 160

    div    bl

    movzx   eax, ah

    sub    ebp, eax

   

    pop    bx

    mov    ah, 0Fh

    jmp    .loop

.10:

    add    ebp, 160

    jmp    .loop

.end:

    mov    [DispPosPM], ebp

    pop    ebp

    ret

   

; Data

DispPosPMequ DispPos+LOADER_SEGPHYADDR

代码 2.4.2 显示文字(chapter2/a/boot/loader.asm)

 

这其中还特地处理了回车和换行。好了,我们运行一下看看。

2.4.1 成功显示文字!

不仅出现了实模式输出的字样,而且还成功在保护模式下写入显存并成功显示文字!这是我们的新突破!

 

2.5         保护模式的终极法宝

其实刚才我们讲的那么多只是保护模式的分段机制而已,但实际上分段机制在大多数现代操作系统中是不被使用的。这里面有兼容性问题,而且,我们还有终极法宝——分页。

我们在这节中先不会讲分页机制,而我们需要先得到可用内存的分布情况。在动手之前,我们先看一下1MB一下的内存分布情况。由于兼容性原因,这部分内存的分布在各电脑上都是一致的。

2.5.1 内存分布表

 

接下来就涉及到如何获得高端内存分布的问题了。随着电脑的不同,这些分布也完全不同。我们不可能直接写死在代码里。这里,我们的BIOS同志又要登场了。

这次登场的是15H中断。不同于DOS中读取磁带的中断,BIOS15H中断,是杂碎的系统服务以及高级电源管理。

我们这里使用它的E820h号功能,获得内存地址描述符表。

 

BIOS 15h中断 杂项服务 功能E820h

寄存器

取值

描述

EAX

入口参数(0xE820)

获得内存地址描述符表

EBX

后续值

BIOS用于检索后续值,第一次使用时保留0

ECX

ES:DI所指向的结构大小,取20

EDX

‘SMAP’

校验值,’SMAP’=0534D4150h

ES:SI

地址范围描述符结构ARDS

2.5.1 E820h号功能

 

其中,ARDS(AddressRange Descriptor Structure)是个20字节的数据结构。

偏移

名称

描述

0

BaseLow

基地址低32

4

BaseHigh

基地址高32

8

LimitLow

长度低32

12

LimitHigh

长度高32

16

Type

地址范围所属的内存类型

2.5.2 ARDS结构

 

BootParamAddrSeg  equ 0x50

ARDSNum            equ 0x0

ARDSAddrOffset    equARDSNum+0x4

代码 2.5.1 用来保存ARDS地址定义(chapter2/b/boot/include/loader.inc)

 

这个保存ARDS的地址的来源是我们之前偷窥的内存结构图,从0x500开始这边有一大段空白可以用来放我们的ARDS。接下来就开始获取数据了。

 

    mov    ax,BootParamAddrSeg

    mov    es, ax                     ; !!!!!!!ES=ARDSAddrSeg=0x50

    mov    di,ARDSAddrOffset

   

    mov    dword[es:ARDSNum],0

   

    mov    ebx, 0

.loop:

    mov    edx, 0x534D4150

    mov    eax, 0xE820

    mov    ecx, 20

    int    15h

    jc     .fail

    add    di, 20

    inc    dword[es:ARDSNum]

    or     ebx, ebx

    jnz    .loop

    jmp    .ok

.fail:

    mov    si,MemChkFail  ;显示出错信息

    call   DispStr

    cli                    ;停止系统运行

    hlt

.ok:

代码 2.5.2 轻松得到ARDS结构(chapter2/b/boot/loader.asm)

 

Get it! 不过,麻烦来了。我们如何才能看到这些数据呢?不用怕,我们使用带有调试功能的bochsLinux下带有调试功能的Bochs需要自己编译,使用Linux的读者可以自己去网上搜索一下。Windows用户则比较容易,只需要右键单击配置好的bochsrc.bxrc(这个文件可以运行bochs配置好后按save保存),选择Debugger就可以了。

然后我们先输入c让它执行,

<bochs:1>c

等到

00014105176i[CPU0] WARNING: HLT instruction with IF=0!

出现后按下Ctrl+C组合键调出:

<bochs:2>

这时候我们输入:

<bochs:2> x 0x500

[bochs]:

0x0000000000000500<bogus+       0>:   0x00000006

可以看到0x500出的内容为6,即我们有6条记录,即20字节,5个双字。所以再输入:

<bochs:3> x /30 0x504

[bochs]:

0x0000000000000504<bogus+       0>:   0x00000000     0x00000000      0x0009f000      0x00000000

0x0000000000000514<bogus+      16>:   0x00000001     0x0009f000     0x00000000      0x00001000

0x0000000000000524<bogus+      32>:   0x00000000     0x00000002     0x000e8000      0x00000000

0x0000000000000534<bogus+      48>:    0x00018000     0x00000000     0x00000002      0x00100000

0x0000000000000544<bogus+      64>:   0x00000000     0x01ef0000     0x00000000      0x00000001

0x0000000000000554<bogus+      80>:   0x01ff0000     0x00000000     0x00010000      0x00000000

0x0000000000000564<bogus+      96>:   0x00000003     0xfffc0000     0x00000000      0x00040000

0x0000000000000574<bogus+     112>:   0x00000000      0x00000002

我们可以就这些信息来制成一张表:

编号

范围

Type

描述

0

00000000-0009EFFF

1

AddressRangeMemory,可被使用

1

0009F000-0009FFFF

2

AddressRangeReserved,不可被使用

2

000E8000-000FFFFF

2

AddressRangeReserved,不可被使用

3

00100000-01FEFFFF

1

AddressRangeMemory,可被使用

4

01FF0000-01FFFFFF

3

AddressRangeACPI,被保留为ACPI使用

5

FFFC0000-FFFFFFFF

2

AddressRangeReserved,不可被使用

2.5.3 笔者Bochs内存分布表

 

关于ACPI是啥子,笔者在往后几章有可能会将到。目前就将它当做Reserved的不可使用区间吧。对了,Type=4表示预留为睡眠用区间,不可随意使用,Type=5表示这部分内存不存在,都不可以使用。其他的都必须被操作系统认为是AddressRangeReserved

除了被保留的内存和不存在的内存,其他内存的最大可访问大小加上1就是电脑内存大小了。这么看来这么看来这个电脑的内存大小为0x2000000。好吧。不过每天查看内存是不可能的,我们应该还写一下显示数字的代码。不过,在那之前……

 

2.6         调用与约定与模块化

模块化是操作系统编写过程中重要的思想。模块化可以有效的减少bug的产生,又有助于阅读。所以我们可以把那些函数都提取到一个lib.inc文件中去。提取完毕以后,我们得讲一讲调用约定。

一般来说,我们会将参数压栈以后call目标函数。但我们都知道寄存器操作远远快于内存操作。所以前辈们发明了一种快速调用方法fastcallFastcall将部分参数放在寄存器中,额外的参数压栈然后再call。不幸的是,每个编译器对fastcall的处理方式完全不同。所以我们甚至无法利用fastcall加速汇编和C的相互调用(除非编译器使我们自己写的)。

一般地,我们在C语言和汇编链接的时候使用ASMLINKAGE调用约定,即C调用约定,不使用寄存器传参。而内核中C程序的调用使用FASTCALL调用约定,使用寄存器来加速。汇编内部的调用则随便来,只要不要忘记保存以后用到的寄存器即可。这也是我为什么在汇编函数下面写上registers changed的原因。

 

2.7         显示数字

废话不多说了,直接上代码。

 

;=====================================================

; DispInt(int num);十六进制显示数字

;-----------------------------------------------------

; Entry:

;   - EAX -> 要显示的数字

; registers changed:

;   - ESI, EAX

DispInt:

    mov    esi, IntBufEPM-1

    call   .putDigit

    call   .putDigit

    call   .putDigit

    call   .putDigit

    call   .putDigit

    call   .putDigit

    call   .putDigit

    call   .putDigit

    dec    esi

    call   DispStrPM

    ret

.putDigit:

    push   eax

    and    al, 0x0F

    cmp    al, 0xA

    jnl    .hex

    add    al, '0'

    jmp    .ret

.hex:

    add    al, 'A'-0xA

.ret:

    mov    [esi],al

    dec    esi

    pop    eax

    shr    eax, 4

   ret

代码 2.7.1 显示数字的函数(chapter2/c/boot/include/lib.inc)

 

         别忘了定义:

IntBuf  db "0x00000000"

IntBufE db 0

IntBufPM  equ IntBuf+LOADER_SEGPHYADDR

IntBufEPMequ IntBufE+LOADER_SEGPHYADDR

代码 2.7.2 定义变量(chapter2/c/boot/include/loader.inc)

 

         笔者又擅自加上了一个NewLine换行函数,由于过于简单,就不放出来了,读者可以自己看看。有了显示数字的函数,我们就可以打印信息了:

 

    mov    ecx, [BootParamPhyAddr+ARDSNum]

    mov    ebp,BootParamPhyAddr+ARDSAddrOffset

DispMemBlock:

    call   NewLine

    mov    eax, [ebp]

    call   DispInt

    add    ebp, 4

    mov    eax, [ebp]

    call   DispInt

    add    dword[DispPosPM],2

    add    ebp, 4

    mov    eax, [ebp]

    call   DispInt

    add    ebp, 4

    mov    eax, [ebp]

    call   DispInt

    add    dword[DispPosPM],2

    add    ebp, 4

    mov    eax, [ebp]

    call   DispInt

    add    ebp, 4

   loop    DispMemBlock

代码 2.7.3 别看重复,但是快速!(chapter2/c/boot/loader.asm)

 

如果成功的话,应该在别的虚拟机下面也能运行。试试。

哦,我们的judgement.sh发出了警报,超过512字节了。我们把loader.inc里的LOADER_SIZE_SECT改为0x2judgement.sh中的值改为1024Makefile中的dd命令的count改为count=2。好了,再make image一次。

放在Hyper-V下运行试试:

2.7.1 成功显示ARDS

 

内存结构都知道了,我们现在就应该学习一下分页机制。

 

2.8         分页机制

分页机制说好懂好懂,说难懂难懂。所以为了保证大家100%弄懂,我们先来讲述一下三个概念。

逻辑地址(Logical Address是与段相关的偏移地址部分。在编程过程中,所有的地址都是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。在Intel实模式下,逻辑地址和物理地址相等(因为没有分段或分页机制,不进行自动地址转换)。我们由于将GDT设置为平坦的段,因此逻辑地址等于线性地址。

线性地址(Linear Address是逻辑地址到物理地址变换之间的中间层。逻辑地址加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。

物理地址(Physical Address是指出目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。

我之所以到现在才讲这几个概念,是因为我们之前的编写过程中,这些地址是等价的。我们没有启动分页,分段又是走形式,所以不会引起任何问题。不过现在我们要打开分页机制,所以只能把这三个概念补上。

 

CPU的页式内存管理单元,负责把一个线性地址,转换为物理地址。由于内存数量有限,不可能一一对应,所以从管理和效率的角度出发,线性地址被分为以固定长度的块,称为(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,所以一共有220个次方个页。这些页的数量还是很多,而且占用空间很多,于是我们又将这些页继续分成1024组。

这样我们得到了1024个大数组。这个大数组我们称之为页目录表。页目录表里的每一项又指向一个包含1024页的表,这个含有1024页的表我们称为页表。页表和页目录表都要沿4KB对齐,也就是放在一个页里。

一个二级管理模式的机制就是分页机制。文字描述太累,看图省眼:

2.8.1 分页机制描述图

分页单元中,页目录表是唯一的,它的地址放在CPUcr3寄存器中,是进行地址转换的基础。

每一个32位的线性地址被划分为三部份,页目录表索引(10),页表索引(10),偏移(12依据以下步骤进行转换:

(1)     cr3中取出页目录表的地址。

(2)     根据线性地址前十位,在页目录表中,找到对应的索引项,得到页表的地址。

(3)     根据线性地址的中间十位,在页表中找到页的起始地址。

(4)     将页的起始地址与线性地址中最后12位相加,得到最终我们想要的物理地址。

怎么样,说复杂复杂,说简单也简单吧。

对了,还没说这页目录表和页表中的一项是什么呢。这一项被称为页目录项和页表项。每一项都是4字节的。

 

2.8.2 复杂的页表项(CR4.PAE=0

既然这个表出来的,那我也只好解释一下了。

Ignored:表示这些位被CPU忽略,操作系统自行决定用途。

PPresent,第0位表示其存在位,如果不存在的话所有其他位都会无效。

R/WRead/Write,读写权限R0)只读,W1)可写。

U/SUser/System,特权级0为系统级(Ring0-2),1为用户级(Ring3)。如果CR0.WP没有被置位,系统级程序可以写入用户级的只读页。

PCD表示这个页面的缓冲策略。1则禁止对该页面进行缓冲。

PWDPCD位被设置时,1表示写入高速缓存的同时必须写入内存。

AAccessed,访问位,表示该页或页表是否已被访问。

DDirty,脏位,表示该页是否已被写。

PATPage Attribute Table,页表属性,部分处理器不支持,我们留0

GGlobal,全局位,如果G位被设置,而且CR4.PGECR47位)被设置,那么这个也不会在TLBTranslation Lookaside Buffer)中被无效。

PSPage Size,页大小。页目录表的第七位。如果CR4.PSEPageSize Extension页面大小扩展,CR4第四位)置位并且这个位置位表示这个页目录表是4MB[1]的页表,并且不含有下级页表。

         差不多了解了,那么我们可以开始写了。

我们先简单地把整个内存等位值映射吧。我们把页目录表放在0x2000~0x3000处的位置,使用4MB的页面好了(简单方便)。初始化后,将CR0的最高位,即PG分页使能位打开。

 

PagingOn:

    mov    ebx, PDEAddr

    mov    eax, 10000011b ;PS,P,R/W被设置

    mov    ecx, 1024

.loop:

    mov    [ebx], eax

    add    ebx, 4

    add    eax, 0x400000

    loop   .loop

   

    mov    eax, PDEAddr

    mov    cr3, eax

   

    mov    eax, cr4

    or     eax, 10000b

    mov    cr4, eax

   

    mov    eax, cr0

    or     eax, 0x80000000

    mov    cr0, eax

    jmp    .ret

.ret:

    nop

ret

代码 2.8.1 分页启动!(chapter2/c/boot/include/lib.inc)

 

PDEAddr是我定义在loader.inc中的常量,选用了一个空的空间。不过运行以后啥事都没有发生……不用担心,这是正常的,我们还什么都没做呢。那我们就利用保护模式干点什么吧。

 

;add    eax, 0x400000

;注释掉以后就可以模拟回卷,即所有线性页都映射到第一个物理页

……

.ret:

    nop

   

    mov    dword[0x600], 0x1234

    mov    eax, dword[0x400600]

    call   DispInt

   ret

代码 2.8.2 测试分页机制(chapter3/c/boot/include/libtest.inc[2])

 

 

2.8.3 成功了!

 

0x1234! 太好了,我们的分页机制也在正常地运行!我们成功掌握了分页机制!

 待って[3],我们还没有掌握多级页表的技术呢,刚才只是偷了个懒跳过了多级分页而已。

 

;=====================================================

; PagingOn();分页机制使能(2级)只映射了4MB以下内存

;-----------------------------------------------------

; registers changed:

;   - EAX, EBX, ECX

PagingOn:

    mov    ebx, PTEAddr

    mov    eax, 00000011b

    mov    ecx, 1024

.loop:

    mov    [ebx], eax

    add    ebx, 4

    add    eax, 0x1000

    loop   .loop

   

    mov    ebx, PDEAddr

    mov    eax, PTEAddr+00000011b

    mov    ecx, 1024

.loop1:

    mov    [ebx], eax

    add    ebx, 4

    loop   .loop1

   

    mov    eax, PDEAddr

    mov    cr3, eax

   

    mov    eax, cr4

    or     eax, 0x10      ; CR4.PSE=1, 4MB页表使能

    mov    cr4, eax       ; 在这里这三句可以省略

   

    mov    eax, cr0

    or     eax, 0x80000000 ;CR0.PG=1, 分页机制使能

    mov    cr0, eax

    jmp    .ret

.ret:

    nop

   

    mov    dword[0x600], 0x1234

    mov    eax, dword[0x400600]

    call   DispInt

   

ret

代码 2.8.3 二级分页(chapter2/c/boot/include/lib2.inc)

 

运行后没有任何变化,证明我们成功攻破了二级分页机制!

 

2.9         冲刺PAEPhysical Address Extension,物理地址扩展)

我们在上一节中已近学会了2级分页,现在我们就要冲刺3级分页了。

         还是老样子,先学习理论,不多说,直接上图。

2.9.1 PAE模式下的分页机制

现在,每一个32位的线性地址被划分为四部份,页目录指针表索引(2),页目录表索引(9),页表索引(9),偏移(12依据以下步骤进行转换:

(1)       cr3中取出页目录指针表的地址。

(2)       根据线性地址前两位,在页目录指针表中,找到对应的索引项,得到页目录表的地址。

(3)       根据线性地址29-21位,在页目录表中,找到对应的索引项,得到页表的地址。

(4)     根据线性地址20-12位,在页表中找到页的起始地址。

(5)     将页的起始地址与线性地址中最后12位相加,得到最终我们想要的物理地址。

页目录表和页表的其他特性都没有变,只是扩展到了64位以支持更大的物理内存(注意,线性地址只有32位!)

其实这个页目录表也没啥指针表也没啥特殊的,和页目录表和页表很相似,不过只有4项,占用32字节,而且也要在内存中沿32字节对齐。另外,PS置位不再是4MB,而是2MB的略小的大页表了。我们还要将CR4.PAE(第5位)置位。

没啥奥秘的,对吧,我们开始写源代码。

 

;=====================================================

; PagingOn();分页机制使能(3级)只映射了4MB以下内存

;-----------------------------------------------------

; registers changed:

;   - EAX, EBX, ECX

PagingOn:

    mov    ebx, PTEAddr

    mov    eax, 00000011b ; PS,P,R/W被设置

    mov    ecx, 512       ; 注意是512了,变成8字节了。

.loop:

    mov    [ebx], eax

    mov    dword[ebx+4], 0 ; 高位填充0,我们不用

    add    ebx, 8

    add    eax, 0x1000

    loop   .loop

   

    mov    ebx, PDEAddr

    mov    eax, PTEAddr+00000011b

    mov    ecx, 512

.loop1:

    mov    [ebx], eax     ; 所有内存都映射在0-4MB

    mov    dword[ebx+4], 0

    add    ebx, 8

    loop   .loop1

   

    mov    ebx, PDPTEAddr ; 所有内存都映射在0-4MB

    mov    eax, PDEAddr+1

    mov    ecx, 4

.loop2:

    mov    [ebx], eax

    mov    dword[ebx+4], 0

    add    ebx, 8

    loop   .loop2

   

    mov    eax, PDPTEAddr

    mov    cr3, eax

   

    mov    eax, cr4

    or     eax, 0x30      ; CR4.PSE=CR4.PAE=1, 4MB页表使能,PAE使能

    mov    cr4, eax

   

    mov    eax, cr0

    or     eax, 0x80000000 ;CR0.PG=1, 分页机制使能

    mov    cr0, eax

    jmp    .ret

.ret:

    nop

   

    mov    dword[0x600], 0x1234

    mov    eax, dword[0x400600]

    call   DispInt

   

   ret

代码 2.9.1 页表级数越来越多。。。。(chapter2/c/boot/include/lib3.inc)



[1] 事实上,这个页表也有可能是2MB,不过那需要打开PAE,物理地址扩展

[2] 如果要测试这段代码,用libtest.inc替换掉lib.inc

[3]日语中表示麻袋(大雾,其实是“等等”的意思)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值