一个操作系统的实现(8)-进一步体会分页机制

上面的两篇文章中,我们对可用内存进行了统计,并且合理的分配了页表的大小。这节中,我们来看看分页的好处

在此之前不知道你有没有注意过一个细节,如果你写一个程序(在Linux或Windows下均可),并改个名复制一份,然后同时调试,你会发现,从变量地址到寄存器的值,几乎全部都是一样的!而这些“一样的”地址之间完全不会混淆起来,而是各自完成着自己的职责。这就是分页机制的功劳,下面我们就来模拟一下这个效果。

线性地址到物理地址的映射

先执行某个线性地址处的模块,然后通过改变cr3来转换地址映射关系,再执行同一个线性地址处的模块,由于地址映射已经改变,所以两次得到的应该是不同的输出。

映射关系转换前的情形如下图所示:

开始,我们让ProcPagingDemo中的代码实现向LinearAddrDemo这个线性地址的转移,而LinearAddrDemo映射到物理地址空间中

的ProcFoo处。我们让ProcFoo打印出红色的字符串Foo,所以执行时我们应该可以看到红色的Foo。随后我们改变地址映射关系,变

化成下图所示的情形。

页目录表和页表的切换让LinearAddrDemo映射到ProcBar(物理地址空间)处,所以当我们再一次调用过程ProcPagingDemo时,程序将转移到ProcBar处执行,我们将看到红色的字符串Bar。

接下来看看新增的代码:

改变映射关系的代码实现

首先,我们用到了另外一套页目录表和页表,所以原先的页目录段和页表段已经不再够用了。事实上,前面的程序中我们用两个段分别存放页目录表和页表,是为了让我们阅读时更加直观和形象。在接下来的程序中,我们把它们放到同一个段中,同时把增加的一套页目录和页表也放到这个段中。

为了操作方便,我们新增加一个段,其线性地址空间为0~4GB。由于分页机制启动之前线性地址等同于物理地址,所以通过这个段可以方便地存取特定的物理地址。此段的代码定义如下:

 26 LABEL_DESC_FLAT_C:  Descriptor 0,        0fffffh, DA_CR|DA_32|DA_LIMIT_4K; 0~4G
 27 LABEL_DESC_FLAT_RW: Descriptor 0,        0fffffh, DA_DRW|DA_LIMIT_4K     ; 0~4G
...

 41 SelectorFlatC           equ     LABEL_DESC_FLAT_C       - LABEL_GDT
 42 SelectorFlatRW          equ     LABEL_DESC_FLAT_RW      - LABEL_GDT

之所以用了两个描述符来描述这个段,是因为我们不仅仅要读写这段内存,而且要执行其中的代码,而这对描述符的属性要求是不一样的。这两个段的段基址都是0,长度都是4GB。

下面我们就将启动分页的代码做相应的修改,如下所示:

257 ; 启动分页机制 --------------------------------------------------------------
258 SetupPaging:
259         ; 根据内存大小计算应初始化多少PDE以及多少页表
260         xor     edx, edx
261         mov     eax, [dwMemSize]
262         mov     ebx, 400000h    ; 400000h = 4M = 4096 * 1024, 一个页表对应的内存大小
263         div     ebx
264         mov     ecx, eax        ; 此时 ecx 为页表的个数,也即 PDE 应该的个数
265         test    edx, edx
266         jz      .no_remainder
267         inc     ecx             ; 如果余数不为 0 就需增加一个页表
268 .no_remainder:
269         mov     [PageTableNumber], ecx  ; 暂存页表个数
270
271         ; 为简化处理, 所有线性地址对应相等的物理地址. 并且不考虑内存空洞.
272
273         ; 首先初始化页目录
274         mov     ax, SelectorFlatRW
275         mov     es, ax
276         mov     edi, PageDirBase0       ; 此段首地址为 PageDirBase0
277         xor     eax, eax
278         mov     eax, PageTblBase0 | PG_P  | PG_USU | PG_RWW
279 .1:
280         stosd
281         add     eax, 4096               ; 为了简化, 所有页表在内存中是连续的.
282         loop    .1
283
284         ; 再初始化所有页表
285         mov     eax, [PageTableNumber]  ; 页表个数
286         mov     ebx, 1024               ; 每个页表 1024 个 PTE
287         mul     ebx
288         mov     ecx, eax                ; PTE个数 = 页表个数 * 1024
289         mov     edi, PageTblBase0       ; 此段首地址为 PageTblBase0
290         xor     eax, eax
291         mov     eax, PG_P  | PG_USU | PG_RWW
292 .2:
293         stosd
294         add     eax, 4096               ; 每一页指向 4K 的空间
295         loop    .2
296
297         mov     eax, PageDirBase0
298         mov     cr3, eax
299         mov     eax, cr0
300         or      eax, 80000000h
301         mov     cr0, eax
302         jmp     short .3
303 .3:
304         nop
305
306         ret
307 ; 分页机制启动完毕 ----------------------------------------------------------

我们原来并没有把页表个数保存起来,而现在情况发生了变化,我们不只有一个页目录和页表,为了初始化另外的页表时方便起见,在这里增加了一个变量PageTableNumber,页表的个数就存在里面。

在整个初始化页目录和页表的过程中,es始终为SelectorFlatRW。这样,想存取物理地址的时候,只需将地址赋值给edi,那么es:edi指向的就是相应物理地址。比如页目录物理地址为PageDirBase0,第276行将edi赋值为PageDirBase0,es:edi于是指向地址PageDirBase0处,赋值通过指令stosd来实现。初始化页表也是同样的道理。

这样,页目录和页表的准备工作就完成了。不过我们不再在原来的位置调用它,而是新建一个函数PagingDemo,把所有与分页有关的内容全都放进里面,这样,程序看起来结构清晰一些。

根据上面两幅图,我们可以认为在这个程序的实现中有4个要关注的要素,分别是ProcPagingDemo、LinearAddrDemo、ProcFoo和ProcBar,我们把它们称为F4。因为程序开始时LinearAddrDemo指向ProcFoo并且线性地址和物理地址是对等的,所以LinearAddrDemo应该等于ProcFoo。而ProcFoo和ProcBar应该是指定的物理地址,所以LinearAddrDemo也应该是指定的物理地址。也正因为如此,我们使用它们时应该确保使用的是FLAT段,即段选择子应该是SelectorFlatC或者SelectorFlatRW。

为了将我们的代码放置在ProcFoo和ProcBar这两处地方,我们先写两个函数,在程序运行时将这两个函数的执行码复制过去就可以了。

ProcPagingDemo要调用FLAT段中的LinearAddrDemo,所以如果不想使用段间转移,我们需要把ProcPagingDemo也放进FLAT段中。我们需要写一个函数,然后把代码复制到ProcPagingDemo处。

这样看来,F4虽然都是当做函数来使用,但实际上却都是内存中指定的地址。我们把它们定义为常量。如下:

 13 LinearAddrDemo  equ     00401000h
 14 ProcFoo         equ     00401000h
 15 ProcBar         equ     00501000h
 16 ProcPagingDemo  equ     00301000h

将代码填充进这些内存地址的代码就在上文我们提到的PagingDemo中,如下:

310 ; 测试分页机制 --------------------------------------------------------------
311 PagingDemo:
312         mov     ax, cs
313         mov     ds, ax
314         mov     ax, SelectorFlatRW
315         mov     es, ax
316
317         push    LenFoo
318         push    OffsetFoo
319         push    ProcFoo
320         call    MemCpy
321         add     esp, 12
322
323         push    LenBar
324         push    OffsetBar
325         push    ProcBar
326         call    MemCpy
327         add     esp, 12
328
329         push    LenPagingDemoAll
330         push    OffsetPagingDemoProc
331         push    ProcPagingDemo
332         call    MemCpy
333         add     esp, 12
334
335         mov     ax, SelectorData
336         mov     ds, ax                  ; 数据段选择子
337         mov     es, ax
338
339         call    SetupPaging             ; 启动分页
340
341         call    SelectorFlatC:ProcPagingDemo
342         call    PSwitch                 ; 切换页目录,改变地址映射关系
343         call    SelectorFlatC:ProcPagingDemo
344
345         ret
346 ; ---------------------------------------------------------------------------

其中用到了名为MemCpy的函数,它复制三个过程到指定的内存地址,类似于C语言中的memcpy。但有一点不同,它假设源数据放在ds段中,而目的在es段中。所以在函数的开头,你可以找到分别为ds和es赋值的语句。MemCpy代码如下:

131 ; ------------------------------------------------------------------------
132 ; 内存拷贝,仿 memcpy
133 ; ------------------------------------------------------------------------
134 ; void* MemCpy(void* es:pDest, void* ds:pSrc, int iSize);
135 ; ------------------------------------------------------------------------
136 MemCpy:
137         push    ebp
138         mov     ebp, esp
139
140         push    esi
141         push    edi
142         push    ecx
143
144         mov     edi, [ebp + 8]  ; Destination
145         mov     esi, [ebp + 12] ; Source
146         mov     ecx, [ebp + 16] ; Counter
147 .1:
148         cmp     ecx, 0          ; 判断计数器
149         jz      .2              ; 计数器为零时跳出
150
151         mov     al, [ds:esi]            ; ┓
152         inc     esi                     ; ┃
153                                         ; ┣ 逐字节移动
154         mov     byte [es:edi], al       ; ┃
155         inc     edi                     ; ┛
156
157         dec     ecx             ; 计数器减一
158         jmp     .1              ; 循环
159 .2:
160         mov     eax, [ebp + 8]  ; 返回值
161
162         pop     ecx
163         pop     edi
164         pop     esi
165         mov     esp, ebp
166         pop     ebp
167
168         ret                     ; 函数结束,返回
169 ; MemCpy 结束-------------------------------------------------------------

被复制的三个过程如下:

402 PagingDemoProc:
403 OffsetPagingDemoProc    equ     PagingDemoProc - $$
404         mov     eax, LinearAddrDemo
405         call    eax
406         retf
407 LenPagingDemoAll        equ     $ - PagingDemoProc
408
409 foo:
410 OffsetFoo               equ     foo - $$
411         mov     ah, 0Ch                 ; 0000: 黑底    1100: 红字
412         mov     al, ‘F‘
413         mov     [gs:((80 * 17 + 0) * 2)], ax    ; 屏幕第 17 行, 第 0 列。
414         mov     al, ‘o‘
415         mov     [gs:((80 * 17 + 1) * 2)], ax    ; 屏幕第 17 行, 第 1 列。
416         mov     [gs:((80 * 17 + 2) * 2)], ax    ; 屏幕第 17 行, 第 2 列。
417         ret
418 LenFoo                  equ     $ - foo
419
420 bar:
421 OffsetBar               equ     bar - $$
422         mov     ah, 0Ch                 ; 0000: 黑底    1100: 红字
423         mov     al, ‘B‘
424         mov     [gs:((80 * 18 + 0) * 2)], ax    ; 屏幕第 18 行, 第 0 列。
425         mov     al, ‘a‘
426         mov     [gs:((80 * 18 + 1) * 2)], ax    ; 屏幕第 18 行, 第 1 列。
427         mov     al, ‘r‘
428         mov     [gs:((80 * 18 + 2) * 2)], ax    ; 屏幕第 18 行, 第 2 列。
429         ret
430 LenBar                  equ     $ - bar

接下来继续看PagingDemo中的代码,这部分代码最重要的部分是四个call语句,如下:

339         call    SetupPaging             ; 启动分页
340
341         call    SelectorFlatC:ProcPagingDemo
342         call    PSwitch                 ; 切换页目录,改变地址映射关系
343         call    SelectorFlatC:ProcPagingDemo

首先启动分页机制,然后调用ProcPagingDemo,再切换页目录,最后又调用一遍ProcPagingDemo。

现在ProcPagingDemo、ProcFoo以及ProcBar的内容我们都已经知道了,由于LinearAddrDemo和ProcFoo相等,并且函数SetupPaging建立起来的是对等的映射关系,所以第一次对ProcPagingDemo的调用反映的就是开始时的内存映射关系图。

接下来看看调用的PSwitch:

349 ; 切换页表 ------------------------------------------------------------------
350 PSwitch:
351         ; 初始化页目录
352         mov     ax, SelectorFlatRW
353         mov     es, ax
354         mov     edi, PageDirBase1       ; 此段首地址为 PageDirBase1
355         xor     eax, eax
356         mov     eax, PageTblBase1 | PG_P  | PG_USU | PG_RWW
357         mov     ecx, [PageTableNumber]
358 .1:
359         stosd
360         add     eax, 4096               ; 为了简化, 所有页表在内存中是连续的.
361         loop    .1
362
363         ; 再初始化所有页表
364         mov     eax, [PageTableNumber]  ; 页表个数
365         mov     ebx, 1024               ; 每个页表 1024 个 PTE
366         mul     ebx
367         mov     ecx, eax                ; PTE个数 = 页表个数 * 1024
368         mov     edi, PageTblBase1       ; 此段首地址为 PageTblBase1
369         xor     eax, eax
370         mov     eax, PG_P  | PG_USU | PG_RWW
371 .2:
372         stosd
373         add     eax, 4096               ; 每一页指向 4K 的空间
374         loop    .2
375
376         ; 在此假设内存是大于 8M 的
377         mov     eax, LinearAddrDemo
378         shr     eax, 22
379         mov     ebx, 4096
380         mul     ebx
381         mov     ecx, eax
382         mov     eax, LinearAddrDemo
383         shr     eax, 12
384         and     eax, 03FFh      ; 1111111111b (10 bits)
385         mov     ebx, 4
386         mul     ebx
387         add     eax, ecx
388         add     eax, PageTblBase1
389         mov     dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW
390
391         mov     eax, PageDirBase1
392         mov     cr3, eax
393         jmp     short .3
394 .3:
395         nop
396
397         ret
398 ; ---------------------------------------------------------------------------

这个函数前面初始化页目录表和页表的过程与SetupPaging是差不多的,只是紧接着程序增加了改变线性地址LinearAddrDemo对应的物理地址的语句。改变后,LinearAddrDemo将不再对应ProcFoo,而是对应ProcBar。<font color=”red>具体是如何改变的,不是很懂

所以,此函数调用完成之后,对ProcPagingDemo的调用就变成了后来的映射关系图。

在代码PSwitch的后半部分,我们把cr3的值改成了PageDirBase1,这个切换过程宣告完成。

程序的运行情况如下:

我们看到红色的Foo和Bar,这说明我们的页表切换起作用了。其实,我们先前提到的不同进程有相同的地址,原理跟本例是类似的,也是在任务切换时通过改变cr3的值来切换页目录,从而改变地址映射关系。

这就是分页的妙处。其实,妙处还不仅仅如此。由于分页机制的存在,程序使用的都是线性地址空间,而不再直接是物理地址。这好像操作系统为应用程序提供了一个不依赖于硬件(物理内存)的平台,应用程序不必关心实际上有多少物理内存,也不必关心正在使用的是哪一段内存,甚至不必关心某一个地址是在物理内存里面还是在硬盘中。总之,操作系统全权负责了这其中的转换工作。

源代码

; ==========================================
; pmtest8.asm
; 编译方法:nasm pmtest8.asm -o pmtest8.com
; ==========================================

%include    "pm.inc"    ; 常量, 宏, 以及一些说明

PageDirBase0        equ    200000h    ; 页目录开始地址:    2M
PageTblBase0        equ    201000h    ; 页表开始地址:        2M +  4K
PageDirBase1        equ    210000h    ; 页目录开始地址:    2M + 64K
PageTblBase1        equ    211000h    ; 页表开始地址:        2M + 64K + 4K

LinearAddrDemo    equ    00401000h
ProcFoo        equ    00401000h
ProcBar        equ    00501000h
ProcPagingDemo    equ    00301000h

org    0100h
    jmp    LABEL_BEGIN

[SECTION .gdt]
; GDT
;                           段基址,       段界限, 属性
LABEL_GDT:          Descriptor 0,              0, 0                      ; 空描述符
LABEL_DESC_NORMAL:  Descriptor 0,         0ffffh, DA_DRW                 ; Normal 描述符
LABEL_DESC_FLAT_C:  Descriptor 0,        0fffffh, DA_CR|DA_32|DA_LIMIT_4K; 0~4G
LABEL_DESC_FLAT_RW: Descriptor 0,        0fffffh, DA_DRW|DA_LIMIT_4K     ; 0~4G
LABEL_DESC_CODE32:  Descriptor 0, SegCode32Len-1, DA_CR|DA_32            ; 非一致代码段, 32
LABEL_DESC_CODE16:  Descriptor 0,         0ffffh, DA_C                   ; 非一致代码段, 16
LABEL_DESC_DATA:    Descriptor 0,      DataLen-1, DA_DRW                 ; Data
LABEL_DESC_STACK:   Descriptor 0,     TopOfStack, DA_DRWA|DA_32          ; Stack, 32 位
LABEL_DESC_VIDEO:   Descriptor 0B8000h,   0ffffh, DA_DRW                 ; 显存首地址
; GDT 结束

GdtLen        equ    $ - LABEL_GDT    ; GDT长度
GdtPtr        dw    GdtLen - 1    ; GDT界限
        dd    0        ; GDT基地址

; GDT 选择子
SelectorNormal        equ    LABEL_DESC_NORMAL    - LABEL_GDT
SelectorFlatC        equ    LABEL_DESC_FLAT_C    - LABEL_GDT
SelectorFlatRW        equ    LABEL_DESC_FLAT_RW    - LABEL_GDT
SelectorCode32        equ    LABEL_DESC_CODE32    - LABEL_GDT
SelectorCode16        equ    LABEL_DESC_CODE16    - LABEL_GDT
SelectorData        equ    LABEL_DESC_DATA        - LABEL_GDT
SelectorStack        equ    LABEL_DESC_STACK    - LABEL_GDT
SelectorVideo        equ    LABEL_DESC_VIDEO    - LABEL_GDT
; END of [SECTION .gdt]

[SECTION .data1]     ; 数据段
ALIGN    32
[BITS    32]
LABEL_DATA:
; 实模式下使用这些符号
; 字符串
_szPMMessage:            db    "In Protect Mode now. ^-^", 0Ah, 0Ah, 0    ; 进入保护模式后显示此字符串
_szMemChkTitle:            db    "BaseAddrL BaseAddrH LengthLow LengthHigh   Type", 0Ah, 0    ; 进入保护模式后显示此字符串
_szRAMSize            db    "RAM size:", 0
_szReturn            db    0Ah, 0
; 变量
_wSPValueInRealMode        dw    0
_dwMCRNumber:            dd    0    ; Memory Check Result
_dwDispPos:            dd    (80 * 6 + 0) * 2    ; 屏幕第 6 行, 第 0 列。
_dwMemSize:            dd    0
_ARDStruct:            ; Address Range Descriptor Structure
    _dwBaseAddrLow:        dd    0
    _dwBaseAddrHigh:    dd    0
    _dwLengthLow:        dd    0
    _dwLengthHigh:        dd    0
    _dwType:        dd    0
_PageTableNumber        dd    0

_MemChkBuf:    times    256    db    0

; 保护模式下使用这些符号
szPMMessage        equ    _szPMMessage    - $$
szMemChkTitle        equ    _szMemChkTitle    - $$
szRAMSize        equ    _szRAMSize    - $$
szReturn        equ    _szReturn    - $$
dwDispPos        equ    _dwDispPos    - $$
dwMemSize        equ    _dwMemSize    - $$
dwMCRNumber        equ    _dwMCRNumber    - $$
ARDStruct        equ    _ARDStruct    - $$
    dwBaseAddrLow    equ    _dwBaseAddrLow    - $$
    dwBaseAddrHigh    equ    _dwBaseAddrHigh    - $$
    dwLengthLow    equ    _dwLengthLow    - $$
    dwLengthHigh    equ    _dwLengthHigh    - $$
    dwType        equ    _dwType        - $$
MemChkBuf        equ    _MemChkBuf    - $$
PageTableNumber        equ    _PageTableNumber- $$

DataLen            equ    $ - LABEL_DATA
; END of [SECTION .data1]

; 全局堆栈段
[SECTION .gs]
ALIGN    32
[BITS    32]
LABEL_STACK:
    times 512 db 0

TopOfStack    equ    $ - LABEL_STACK - 1

; END of [SECTION .gs]

[SECTION .s16]
[BITS    16]
LABEL_BEGIN:
    mov    ax, cs
    mov    ds, ax
    mov    es, ax
    mov    ss, ax
    mov    sp, 0100h

    mov    [LABEL_GO_BACK_TO_REAL+3], ax
    mov    [_wSPValueInRealMode], sp

    ; 得到内存数
    mov    ebx, 0
    mov    di, _MemChkBuf
.loop:
    mov    eax, 0E820h
    mov    ecx, 20
    mov    edx, 0534D4150h
    int    15h
    jc    LABEL_MEM_CHK_FAIL
    add    di, 20
    inc    dword [_dwMCRNumber]
    cmp    ebx, 0
    jne    .loop
    jmp    LABEL_MEM_CHK_OK
LABEL_MEM_CHK_FAIL:
    mov    dword [_dwMCRNumber], 0
LABEL_MEM_CHK_OK:

    ; 初始化 16 位代码段描述符
    mov    ax, cs
    movzx    eax, ax
    shl    eax, 4
    add    eax, LABEL_SEG_CODE16
    mov    word [LABEL_DESC_CODE16 + 2], ax
    shr    eax, 16
    mov    byte [LABEL_DESC_CODE16 + 4], al
    mov    byte [LABEL_DESC_CODE16 + 7], ah

    ; 初始化 32 位代码段描述符
    xor    eax, eax
    mov    ax, cs
    shl    eax, 4
    add    eax, LABEL_SEG_CODE32
    mov    word [LABEL_DESC_CODE32 + 2], ax
    shr    eax, 16
    mov    byte [LABEL_DESC_CODE32 + 4], al
    mov    byte [LABEL_DESC_CODE32 + 7], ah

    ; 初始化数据段描述符
    xor    eax, eax
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_DATA
    mov    word [LABEL_DESC_DATA + 2], ax
    shr    eax, 16
    mov    byte [LABEL_DESC_DATA + 4], al
    mov    byte [LABEL_DESC_DATA + 7], ah

    ; 初始化堆栈段描述符
    xor    eax, eax
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_STACK
    mov    word [LABEL_DESC_STACK + 2], ax
    shr    eax, 16
    mov    byte [LABEL_DESC_STACK + 4], al
    mov    byte [LABEL_DESC_STACK + 7], ah

    ; 为加载 GDTR 作准备
    xor    eax, eax
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_GDT        ; eax <- gdt 基地址
    mov    dword [GdtPtr + 2], eax    ; [GdtPtr + 2] <- gdt 基地址

    ; 加载 GDTR
    lgdt    [GdtPtr]

    ; 关中断
    cli

    ; 打开地址线A20
    in    al, 92h
    or    al, 00000010b
    out    92h, al

    ; 准备切换到保护模式
    mov    eax, cr0
    or    eax, 1
    mov    cr0, eax

    ; 真正进入保护模式
    jmp    dword SelectorCode32:0
    ; 执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0  处

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

LABEL_REAL_ENTRY:        ; 从保护模式跳回到实模式就到了这里
    mov    ax, cs
    mov    ds, ax
    mov    es, ax
    mov    ss, ax

    mov    sp, [_wSPValueInRealMode]

    in    al, 92h        ; ┓
    and    al, 11111101b    ; ┣ 关闭 A20 地址线
    out    92h, al        ; ┛

    sti            ; 开中断

    mov    ax, 4c00h    ; ┓
    int    21h        ; ┛回到 DOS
; END of [SECTION .s16]

[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS    32]

LABEL_SEG_CODE32:
    mov    ax, SelectorData
    mov    ds, ax            ; 数据段选择子
    mov    es, ax
    mov    ax, SelectorVideo
    mov    gs, ax            ; 视频段选择子

    mov    ax, SelectorStack
    mov    ss, ax            ; 堆栈段选择子

    mov    esp, TopOfStack

    ; 下面显示一个字符串
    push    szPMMessage
    call    DispStr
    add    esp, 4

    push    szMemChkTitle
    call    DispStr
    add    esp, 4

    call    DispMemSize        ; 显示内存信息

    call    PagingDemo        ; 演示改变页目录的效果

    ; 到此停止
    jmp    SelectorCode16:0

; 启动分页机制 --------------------------------------------------------------
SetupPaging:
    ; 根据内存大小计算应初始化多少PDE以及多少页表
    xor    edx, edx
    mov    eax, [dwMemSize]
    mov    ebx, 400000h    ; 400000h = 4M = 4096 * 1024, 一个页表对应的内存大小
    div    ebx
    mov    ecx, eax    ; 此时 ecx 为页表的个数,也即 PDE 应该的个数
    test    edx, edx
    jz    .no_remainder
    inc    ecx        ; 如果余数不为 0 就需增加一个页表
.no_remainder:
    mov    [PageTableNumber], ecx    ; 暂存页表个数

    ; 为简化处理, 所有线性地址对应相等的物理地址. 并且不考虑内存空洞.

    ; 首先初始化页目录
    mov    ax, SelectorFlatRW
    mov    es, ax
    mov    edi, PageDirBase0    ; 此段首地址为 PageDirBase0
    xor    eax, eax
    mov    eax, PageTblBase0 | PG_P  | PG_USU | PG_RWW
.1:
    stosd
    add    eax, 4096        ; 为了简化, 所有页表在内存中是连续的.
    loop    .1

    ; 再初始化所有页表
    mov    eax, [PageTableNumber]    ; 页表个数
    mov    ebx, 1024        ; 每个页表 1024 个 PTE
    mul    ebx
    mov    ecx, eax        ; PTE个数 = 页表个数 * 1024
    mov    edi, PageTblBase0    ; 此段首地址为 PageTblBase0
    xor    eax, eax
    mov    eax, PG_P  | PG_USU | PG_RWW
.2:
    stosd
    add    eax, 4096        ; 每一页指向 4K 的空间
    loop    .2

    mov    eax, PageDirBase0
    mov    cr3, eax
    mov    eax, cr0
    or    eax, 80000000h
    mov    cr0, eax
    jmp    short .3
.3:
    nop

    ret
; 分页机制启动完毕 ----------------------------------------------------------

; 测试分页机制 --------------------------------------------------------------
PagingDemo:
    mov    ax, cs
    mov    ds, ax
    mov    ax, SelectorFlatRW
    mov    es, ax

    push    LenFoo
    push    OffsetFoo
    push    ProcFoo
    call    MemCpy
    add    esp, 12

    push    LenBar
    push    OffsetBar
    push    ProcBar
    call    MemCpy
    add    esp, 12

    push    LenPagingDemoAll
    push    OffsetPagingDemoProc
    push    ProcPagingDemo
    call    MemCpy
    add    esp, 12

    mov    ax, SelectorData
    mov    ds, ax            ; 数据段选择子
    mov    es, ax

    call    SetupPaging        ; 启动分页

    call    SelectorFlatC:ProcPagingDemo
    call    PSwitch            ; 切换页目录,改变地址映射关系
    call    SelectorFlatC:ProcPagingDemo

    ret
; ---------------------------------------------------------------------------

; 切换页表 ------------------------------------------------------------------
PSwitch:
    ; 初始化页目录
    mov    ax, SelectorFlatRW
    mov    es, ax
    mov    edi, PageDirBase1    ; 此段首地址为 PageDirBase1
    xor    eax, eax
    mov    eax, PageTblBase1 | PG_P  | PG_USU | PG_RWW
    mov    ecx, [PageTableNumber]
.1:
    stosd
    add    eax, 4096        ; 为了简化, 所有页表在内存中是连续的.
    loop    .1

    ; 再初始化所有页表
    mov    eax, [PageTableNumber]    ; 页表个数
    mov    ebx, 1024        ; 每个页表 1024 个 PTE
    mul    ebx
    mov    ecx, eax        ; PTE个数 = 页表个数 * 1024
    mov    edi, PageTblBase1    ; 此段首地址为 PageTblBase1
    xor    eax, eax
    mov    eax, PG_P  | PG_USU | PG_RWW
.2:
    stosd
    add    eax, 4096        ; 每一页指向 4K 的空间
    loop    .2

    ; 在此假设内存是大于 8M 的
    mov    eax, LinearAddrDemo
    shr    eax, 22
    mov    ebx, 4096
    mul    ebx
    mov    ecx, eax
    mov    eax, LinearAddrDemo
    shr    eax, 12
    and    eax, 03FFh    ; 1111111111b (10 bits)
    mov    ebx, 4
    mul    ebx
    add    eax, ecx
    add    eax, PageTblBase1
    mov    dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW

    mov    eax, PageDirBase1
    mov    cr3, eax
    jmp    short .3
.3:
    nop

    ret
; ---------------------------------------------------------------------------

PagingDemoProc:
OffsetPagingDemoProc    equ    PagingDemoProc - $$
    mov    eax, LinearAddrDemo
    call    eax
    retf
LenPagingDemoAll    equ    $ - PagingDemoProc

foo:
OffsetFoo        equ    foo - $$
    mov    ah, 0Ch            ; 0000: 黑底    1100: 红字
    mov    al, ‘F‘
    mov    [gs:((80 * 17 + 0) * 2)], ax    ; 屏幕第 17 行, 第 0 列。
    mov    al, ‘o‘
    mov    [gs:((80 * 17 + 1) * 2)], ax    ; 屏幕第 17 行, 第 1 列。
    mov    [gs:((80 * 17 + 2) * 2)], ax    ; 屏幕第 17 行, 第 2 列。
    ret
LenFoo            equ    $ - foo

bar:
OffsetBar        equ    bar - $$
    mov    ah, 0Ch            ; 0000: 黑底    1100: 红字
    mov    al, ‘B‘
    mov    [gs:((80 * 18 + 0) * 2)], ax    ; 屏幕第 18 行, 第 0 列。
    mov    al, ‘a‘
    mov    [gs:((80 * 18 + 1) * 2)], ax    ; 屏幕第 18 行, 第 1 列。
    mov    al, ‘r‘
    mov    [gs:((80 * 18 + 2) * 2)], ax    ; 屏幕第 18 行, 第 2 列。
    ret
LenBar            equ    $ - bar

; 显示内存信息 --------------------------------------------------------------
DispMemSize:
    push    esi
    push    edi
    push    ecx

    mov    esi, MemChkBuf
    mov    ecx, [dwMCRNumber]    ;for(int i=0;i<[MCRNumber];i++) // 每次得到一个ARDS(Address Range Descriptor Structure)结构
.loop:                    ;{
    mov    edx, 5            ;    for(int j=0;j<5;j++)    // 每次得到一个ARDS中的成员,共5个成员
    mov    edi, ARDStruct        ;    {    // 依次显示:BaseAddrLow,BaseAddrHigh,LengthLow,LengthHigh,Type
.1:                    ;
    push    dword [esi]        ;
    call    DispInt            ;        DispInt(MemChkBuf[j*4]); // 显示一个成员
    pop    eax            ;
    stosd                ;        ARDStruct[j*4] = MemChkBuf[j*4];
    add    esi, 4            ;
    dec    edx            ;
    cmp    edx, 0            ;
    jnz    .1            ;    }
    call    DispReturn        ;    printf("\n");
    cmp    dword [dwType], 1    ;    if(Type == AddressRangeMemory) // AddressRangeMemory : 1, AddressRangeReserved : 2
    jne    .2            ;    {
    mov    eax, [dwBaseAddrLow]    ;
    add    eax, [dwLengthLow]    ;
    cmp    eax, [dwMemSize]    ;        if(BaseAddrLow + LengthLow > MemSize)
    jb    .2            ;
    mov    [dwMemSize], eax    ;            MemSize = BaseAddrLow + LengthLow;
.2:                    ;    }
    loop    .loop            ;}
                    ;
    call    DispReturn        ;printf("\n");
    push    szRAMSize        ;
    call    DispStr            ;printf("RAM size:");
    add    esp, 4            ;
                    ;
    push    dword [dwMemSize]    ;
    call    DispInt            ;DispInt(MemSize);
    add    esp, 4            ;

    pop    ecx
    pop    edi
    pop    esi
    ret
; ---------------------------------------------------------------------------

%include    "lib.inc"    ; 库函数

SegCode32Len    equ    $ - LABEL_SEG_CODE32
; END of [SECTION .s32]

; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
[SECTION .s16code]
ALIGN    32
[BITS    16]
LABEL_SEG_CODE16:
    ; 跳回实模式:
    mov    ax, SelectorNormal
    mov    ds, ax
    mov    es, ax
    mov    fs, ax
    mov    gs, ax
    mov    ss, ax

    mov    eax, cr0
    and     eax, 7FFFFFFEh          ; PE=0, PG=0
    mov    cr0, eax

LABEL_GO_BACK_TO_REAL:
    jmp    0:LABEL_REAL_ENTRY    ; 段地址会在程序开始处被设置成正确的值

Code16Len    equ    $ - LABEL_SEG_CODE16

; END of [SECTION .s16code]
时间: 2024-08-29 03:32:57

一个操作系统的实现(8)-进一步体会分页机制的相关文章

一个操作系统的实现(6)-初识分页机制

这节仍然是从实现的角度来讲述分页机制. 为什么要引入分页机制.我们都知道分段机制是为了提供保护机制,那么为什么还要引入分页机制呢? 为什么引入分页机制 想象一下这样一种情况:假设我们用的计算机物理内存是4GB,但是我们的程序大小是5GB.那么这个时候我们无法将程序全部放到内存中,也就无法运行程序.分页机制引入的原因之一就是为了解决这个问题.分页机制的引入实现了虚拟存储器的机制. 另外,程序执行具有局部性,也就是说一段时间内,只需要程序代码中的一小部分(相对于整个程序)就可以实现程序的执行.那么我

一个操作系统的实现(7)-获取机器内存并进行合理分页

在前面的程序中,我们用了4MB的空间来存放页表,并用它映射了4GB的内存空间,而我们的物理内存不见得有这么大,这显然是太浪费了.如果我们的内存总数只有16MB的话,只是页表就占用了25%的内存空间.而实际上,如果仅仅是对等映射的话,16MB的内存只要4个页表就够了.所以,我们有必要知道内存有多大,然后根据内存大小确定多少页表是够用的.而且,一个操作系统也必须知道内存的容量,以便进行内存管理. 克勤克俭用内存 这里利用中断15h来获取计算机的内存. 在调用中断15h之前,我们需要填充下列寄存器:

Linux的分段和分页机制

1 基于80x86的Linux分段机制 80386的两种工作模式:80386的工作模式包括实地址模式和虚地址模式(保护模式).Linux主要工作在保护模式下. 在保护模式下,80386虚地址空间可达16K个段,每段大小可变,最大达4GB.逻辑地址到线性地址的转换由80386分段机制管理.段寄存器CS.DS.ES.SS.FS或GS各标识一个段.这些段寄存器作为段选择器,用来选择该段的描述符. 分段逻辑地址到线性地址转换图: Linux对80386的分段机制使用得很有限,因为Linux的设计目标是支

操作系统实践(7)——分页机制

为什么分页,分页有什么好处? 从苦逼码农的角度,用一个租房的例子来说,原来分段机制,就好比房子整租,当租客不想租的时候,就整个房子退还给房东,房东再找其它人整租,这样有个缺点,在北京,整租一套房子,很贵,而且很多租客都是一个人或者两个人,用不着租那么大,于是一方面房东如果找不到租客,房子就空闲着,一方面要是租客咬咬牙租下整套房子,很多房间也是很浪费.于是,下面要讨论的分页机制,就是现在市场上各种单间出租了.单间出租的好处很明显,房子利用率很高,一个租客不租了,很快就能找到下一个租客.其实分页的好

Linux内存寻址之分页机制

http://blog.xiaohansong.com/2015/10/05/Linux内存寻址之分页机制/ 在上一篇文章Linux内存寻址之分段机制中,我们了解逻辑地址通过分段机制转换为线性地址的过程.下面,我们就来看看更加重要和复杂的分页机制. 分页机制在段机制之后进行,以完成线性-物理地址的转换过程.段机制把逻辑地址转换为线性地址,分页机制进一步把该线性地址再转换为物理地址. 硬件中的分页 分页机制由CR0中的PG位启用.如PG=1,启用分页机制,并使用本节要描述的机制,把线性地址转换为物

分页机制

本文为<x86汇编语言:从实模式到保护模式> 第16章笔记 因为段的长度不定, 在分配内存时, 可能会发生内存中的空闲区域小于要加载的段, 或者空闲区域远远大于要加载的段. 在前一种情况下, 需要另外寻找合适的空闲区域; 在后一种情况下, 分配会成功, 但太过于浪费. 为了解决这个问题, 从80386处理器开始, 引入了分页机制. 分页功能从总体上来说, 是用长度固定的页来代替长度不一定的段, 藉此解决因段长度不同而带来的内存空间管理问题. 尽管操作系统也可以用软件来实施固定长度的内存分配,

[转帖]Linux分页机制之分页机制的演变--Linux内存管理(七)

Linux分页机制之分页机制的演变--Linux内存管理(七) 2016年09月01日 20:01:31 JeanCheng 阅读数:4543 https://blog.csdn.net/gatieme/article/details/52402967 ~ 版权声明:本文为博主原创文章 && 转载请著名出处 @ http://blog.csdn.net/gatieme https://blog.csdn.net/gatieme/article/details/52402967 日期 内核版

操作系统篇-浅析分页机制

|| 版权声明:本文为博主原创文章,未经博主允许不得转载. 一.前言 在我们进行程序开发的时候,一般情况下,是不需要管理内存的,也不需要操心内存够不够用,其实,这就是分页机制给我们带来的好处.它是实现虚拟存储的关键,位于线性地址与物理地址之间,在使用这种内存分页管理方法时,每个执行中的进程(任务)可以使用比实际内存容量大得多的连续地址空间.而且当系统内存实际上被分成很多凌乱的块时,它可以建立一个大而连续的内存空间的映象,好让程序不用操心和管理这些分散的内存块.分页机制增强了分段机制的性能.页地址

32机的内存分页机制

在实模式下寻址的时候,"段寄存器+偏移地址"进过转换计算以后得到的地址是"物理地址",也就是在物理内存中的实际地址,而在保护模式下,"段选择器+偏移地址"转换后的地址被称为"线性地址"而不是"物理地址",那么线性地址就是物理地址吗? 答案可能是,也可能不是,这取决于80386的内存分页机制是否被使用. 为什么有内存分页机制? 我们回顾一下,单任务的DOS系统中,一个应用程序可以使用所有的空闲内存,程序退出以