前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >详解操作系统分页机制与实战

详解操作系统分页机制与实战

作者头像
用户3147702
发布2022-06-27 14:22:36
1K0
发布2022-06-27 14:22:36
举报
文章被收录于专栏:小脑斧科技博客

1. 引言

经过一系列的讲解,我们从启动扇区一直加载到了分段。

分段让操作系统具备了对内存的保护能力,通过描述符表、选择子的多级跳转,让每一段内存都增加了一系列属性,从而可以实现读、写、执行等权限以及为不同程序赋予不同特权的保护功能。 在此前的文章中,我们已经提到,通过 LDT 来解决进程间内存独立的问题,其代价是寄存器的反复加载,这对于 CPU 来说是一件较为耗时的操作,于是,80386 开始,Intel 引入了内存分页功能,相比于 LDT,更为灵活高效,因此 LDT 已经基本不会被使用了。 那么,分页究竟是一种什么样的机制,又是如何实现的呢?本文我们就来一探究竟。

2. 分页机制

随着 80286 保护机制的引入,让多个程序共用 CPU、内存来执行成为了可能,虽然 CPU 可以通过反复的保存现场并切换完成多个进程的并发执行,但昂贵而又容量有限的内存成为了最大的限制,虽然 32 位的地址总线提供了 4GB 内存的寻址能力,但程序的运行受限于实际的内存容量,同时,系统在启动时又很难预先定义每个进程究竟要分配多大的段空间来满足每一个应用程序的需要。 此时,操作系统迫切需要一种类似 CPU 任务切换的机制来对内存进行切换,可以想到,这需要从两个方面来进行考虑:

  1. 离散化 — 如同 CPU 时间片,将内存尽量切碎,从而在一个任务中,非当前使用的内存切片可以被临时放置在辅助存储器上,让出内存供其他任务使用
  2. 虚拟化 — 离散化的解决方案引入了一个新的问题: 同一个物理地址在不同的时间可能载入不同任务所对应的内存,同一个任务在不同时间使用的相同变量又可能位于不同的物理地址中,要解决这些问题就必须要通过虚拟化的方式,隐藏物理地址,通过任务所使用的虚拟地址映射到内存的物理地址上,从而不同时刻同一虚拟地址可以映射到不同的物理地址上

经过上述的离散化与虚拟化,分页机制就这样诞生了。 从 80386 开始,内存被分为 4KB 固定大小的“页”,他们在需要使用时载入内存,不需要使用时可以被置换到磁盘上,由分页机制将程序持有的固定的线性地址动态映射到物理地址上。

操作系统的内存管理

3. 页目录表与页表

如图所示,在 80X86 的软硬件设计中,实现了两级页表。 第一级页表 Root page table 被称为“页目录表”,总计占用 4KB 空间,每个表项占用 4 字节,共计 1024 个表项,因此通过线性地址的最高 10bits 可以索引每一个表项,每个表项简称“PDE”(Page Directory Entry) 第二级页表是直接保存物理页基地址的列表,他同样有 1024 个表项,每个表项 4 字节,通过线性地址的中间 10bits 来进行索引。 线性地址剩余的 12bits 用来索引最终指向的页面的 4KB 内存。

3.1. 页目录表项 PDE 与页表项 PTE 的结构

PDE 与 PTE 的结构非常相似:

  • P 位 — 存在位,表示当前条目是否在物理内存中
  • R/W 位 — 读写权限位,为 0 表示只读,为 1 表示可读写
  • U/S 位 — 页或一组页的特权级,为 0 表示系统级,对应 CPL 0、1、2,为 1 表示用户级,对应 CPL 3,下文进行详细介绍
  • PWT — 页表缓冲写入机制,为 0 表示 write-back 模式,更新页表缓冲区时,只标记为已更新,不同步写内存,只有被新进入的数据取代时才更新到内存,为 1 表示 write-through 模式,更新页表缓冲区时,同步写内存,保证缓冲区与内存一致
  • PCD — 是否拒绝被缓冲,为 0 表示可以被缓冲,为 1 表示不可以被缓冲
  • A 位 — 是否被访问,CPU 会在访问到页面时将该位置 1,但不会清除,只有软件可以将 A 位复位
  • D 位 — 是否被写入,CPU 会在写入页面时将该位置 1,但不会清除,只有软件可以将 D 位复位
  • PS — 页大小位,为 0 表示页大小为 4KB,且 PDE 指向页表,为 1 表示页大小为 4MB,且 PDE 指向 4MB 的整块内存
  • PAT — 奔腾3以后的 CPU 引入的页属性表标识位,为 1 开启页属性表后,通过一系列专用寄存器(MBR)为每个页提供了详细的属性设置
  • G 位 — 全局位,如果该位与 CR4 寄存器的 PGE 位同时被置为 1,则该页或页目录项将不会在 TLB 中被逐出
  • 20bits 基地址 — PDE 与 PTE 的高 20bits 都是下级页基址,无论是页目录表还是页表还是在内存中的页,他们都是 4KB 对齐的,也就是说他们的首地址低12位均为0,这样,只需要通过 20bits 的基地址 * 12 就可以得到计算后的 32 位物理地址了

3.2. cr0 寄存器

此前,我们介绍了 CPU 的控制寄存器。 进军保护模式

硬件控制开关寄存器 cr0 的部分字段如下图所示:

这里重点介绍 PG 位、WP 位与 CD 位:

3.2.1. PG 位

PG 位就是是否开启分页的标志,当 PG 位被置为 1,则开启分页模式,上述一系列机制开始生效。

3.2.2. WP 位

WP 位是内核写保护位,当 WP 位为 0,那么当 CPL 为 0、1、2(系统级)的程序去访问 U/S 位为 1 的内存页时,不再校验页的 R/W 位,系统级程序对所有用户级页面均具有读写权限。 如果 WP 位为 1,那么系统级程序访问用户级内存时,仍然要校验用户级内存的 R/W 位是只读还是读写权限。

3.2.3. CD 位与 TLB

CD 位是页表缓冲位,用来标识是否开启 CPU 页表缓冲。 所谓的“页表缓冲”简称为“TLB”(translation lookaside buffer),指的是 CPU 内的一块缓冲区,用来缓存经常访问的页目录和页表项,从而加快访问页目录与页表的时间。 如果 CD 位为 0,即不开启 CPU 页表缓冲,那么 PDE 与 PTE 中的 PWT 位、PCD 位与 G 位也将不起作用。

4. 实战开启内存分页

接下来我们就来实战开启内存分页机制。 经过上述讲解,我们已经对分页机制了解的十分清楚了,那么,如何在我们已有的分段代码基础上实现分页机制呢? 为了以最快速度上手实战,我们这里不考虑线性地址与物理地址的映射关系,直接取线性地址 = 物理地址,同时假设内存地址空间足够容纳所有页面,并且页、页表在内存中均连续。

  1. 创建页目录段
  2. 创建页表段
  3. 填充 PDE
  4. 填充 PTE
  5. 设置 CR3 寄存器,指向页目录表首地址
  6. 设置 CR0 寄存器 PG 位,启动分页机制
  7. 执行程序
  8. 退出实地址模式时复位 PG 位

4.1. 创建页目录段

4.1.1. 在 GDT 中创建描述符

代码语言:javascript
复制
PageDirBase        equ    200000h    ; 页目录开始地址: 2M
LABEL_DESC_PAGE_DIR: Descriptor PageDirBase, 4095, 92h ; Page Directory,可读写

这里我们直接选择内存 2M 地址作为页目录表的起始地址,且页目录表大小为 4KB。

4.1.2. 创建页目录段选择子

代码语言:javascript
复制
SelectorPageDir        equ    LABEL_DESC_PAGE_DIR    - LABEL_GDT

4.2. 创建页表段

代码语言:javascript
复制
PageTblBase        equ    201000h    ; 页表开始地址: 2M+4K
LABEL_DESC_PAGE_TBL: Descriptor PageTblBase, 1023, 8092h ; Page Tables,段界限为 1023 * 4096 字节

页表位于页目录起始地址后 4KB 位置,同样为可读写数据段,但此处,我们置位了 GDT 描述符的 G 位,表示段界限的单位为 4096 字节。

4.2.1. 创建页表选择子

代码语言:javascript
复制
SelectorPageTbl        equ    LABEL_DESC_PAGE_TBL    - LABEL_GDT

4.3. 填充 PDE

接下来我们就要填充上面定义的 4096 字节的页目录中的每一个表项 PDE。

代码语言:javascript
复制
    ; 初始化页目录
    mov    ax, SelectorPageDir        ; 此段首地址为 PageDirBase
    mov    es, ax
    mov    ecx, 1024                ; 共 1K 个表项
    xor    edi, edi
    xor    eax, eax
    mov    eax, PageTblBase | 7     ; 用户级、存在于内存、可读写

.filter_pde:
    stosd
    add    eax, 4096                ; 为了简化, 所有页表在内存中是连续的.
    loop    .filter_pde

这里通过 stosd 指令与 loop 指令,将 eax 的内容循环填充到 4KB 大小的页目录表内。

4.3.1. stosd 指令与 loop 指令

这两个指令我们之前已经有过很多使用:

  • stosd 指令将 32 位的 eax 的内容复制到 es:edi 指向的内存空间,并自动将 edi 寄存器内容加 4,类似的有复制 2 字节 ax 寄存器的 stosw 以及复制 1 字节 al 寄存器的 stosb 命令
  • loop 指令先判断 ecx 是否为 0,如为 0 则跳出循环,否则对 ecx 寄存器内容减 1 并跳转到其参数 label 处,16 位模式下,则判断 cx 是否为 0,无论在任何模式下,loopw 指令均使用 CX 寄存器,loopd 指令均判断 ecx 寄存器

4.4. 填充 PTE

代码语言:javascript
复制
    ; 初始化所有页表 (1K 个, 4M 内存空间)
    mov    ax, SelectorPageTbl        ; 此段首地址为 PageTblBase
    mov    es, ax
    mov    ecx, 1024 * 1024        ; 共 1M 个页表项, 也即有 1M 个页
    xor    edi, edi
    xor    eax, eax
    mov    eax, 7                    ; 用户级、存在于内存、可读写
.filter_pte:
    stosd
    add    eax, 4096        ; 每一页指向 4K 的空间
    loop    .filter_pte

4.5. 设置 CR3 寄存器

代码语言:javascript
复制
; 设置页目录表起始地址
mov    eax, PageDirBase
mov    cr3, eax

4.6. 开启分页机制

代码语言:javascript
复制
; 开启分页机制
mov    eax, cr0
or    eax, 80000000h
mov    cr0, eax

4.7. 退出实地址模式时复位 PG 位

代码语言:javascript
复制
mov    eax, cr0
and    eax, 7FFFFFFEh        ; PE=0, PG=0
mov    cr0, eax

5. 执行结果

7.1 附录 -- 专题目录

计算机是如何启动的?一文教你自制操作系统

如何调试操作系统源码

一文详解 32 位保护模式与内存分段机制

进军保护模式

保护模式进阶 -- 再回实模式

实战局部描述符表 LDT

实战特权级间的跳转 -- 原理篇

利用调用门实现特权级间跳转 -- 实战篇

操作系统的内存管理

7.2 附录 — 完整代码

代码语言:javascript
复制
; ---------------- 内存段描述符宏 -------------
; usage: Descriptor Base, Limit, Attr
;        Base:  dd
;        Limit: dd (low 20 bits available)
;        Attr:  dw (lower 4 bits of higher byte are always 0)
%macro Descriptor 3
    dw    %2 & 0FFFFh                            ; 段界限1
    dw    %1 & 0FFFFh                            ; 段基址1
    db    (%1 >> 16) & 0FFh                    ; 段基址2
    dw    ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)    ; 属性1 + 段界限2 + 属性2
    db    (%1 >> 24) & 0FFh                    ; 段基址3
%endmacro

PageDirBase        equ    200000h    ; 页目录开始地址: 2M
PageTblBase        equ    201000h    ; 页表开始地址: 2M+4K

; ------------ DOS 加载初始内存地址 -----------
org    0100h
    jmp    LABEL_BEGIN

; ------------------- GDT ---------------------
[SECTION .gdt]
; GDT
;                              段基址,           段界限,        属性
LABEL_GDT:             Descriptor           0,                0, 0            ; 空描述符
LABEL_DESC_NORMAL:   Descriptor           0,           0ffffh, 92h            ; Normal 描述符
LABEL_DESC_PAGE_DIR: Descriptor PageDirBase,              4095, 92h            ; Page Directory,可读写
LABEL_DESC_PAGE_TBL: Descriptor PageTblBase,              1023, 8092h         ; Page Tables,段界限为 1023 * 4096 字节
LABEL_DESC_CODE32:   Descriptor              0, SegCode32Len - 1, 4098h          ; 非一致代码段
LABEL_DESC_CODE16:   Descriptor              0,           0ffffh, 98h          ; 非一致代码段, 用于跳回 16 BITS 模式
LABEL_DESC_DATA:     Descriptor              0,        DataLen-1, 92h           ; 可读写数据段,界限 64KB
LABEL_DESC_STACK:    Descriptor           0,       TopOfStack, 4093h          ; 32 位全局堆栈段,可读写数据段,且栈指针默认使用 esp 寄存器
LABEL_DESC_VIDEO:    Descriptor     0B8000h,           0ffffh, 92h            ; 显存首地址
; ------------------ END OF GDT ----------------

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

; ------------------ GDT 选择子 -----------------
SelectorNormal        equ    LABEL_DESC_NORMAL    - LABEL_GDT
SelectorPageDir        equ    LABEL_DESC_PAGE_DIR    - LABEL_GDT
SelectorPageTbl        equ    LABEL_DESC_PAGE_TBL    - 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 .data1]     ; 数据段
ALIGN    32
[BITS    32]
LABEL_DATA:
SPValueInRealMode    dw    0
BootMessage:        db  "Hello World my OS, techlog.cn!", 0
OffsetBootMessage    equ    BootMessage - $$
DataLen                equ    $ - LABEL_DATA

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

[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    [SPValueInRealMode], sp

    ; 初始化 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

    ; 初始化非一致代码段描述符
    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                ; 清空 eax 寄存器
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_GDT            ; 计算出 GDT 基地址的物理地址
    mov    dword [GdtPtr + 2], eax    ; [GdtPtr + 2] <- gdt 基地址

    ; 加载 GDTR
    lgdt    [GdtPtr]

    ; 关闭硬件中断
    cli

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

    ; 置位 PE 标志位,打开保护模式
    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, [SPValueInRealMode]

    ; 关闭 A20 地址线
    in    al, 92h
    and    al, 0fdh
    out    92h, al

    ; 打开硬件中断
    sti

    ; 触发 BIOS int 21h 中断,回到实地址模式
    mov    ax, 4c00h
    int    21h

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

LABEL_SEG_CODE32:
    call    SetupPaging

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

    mov    ax, SelectorVideo
    mov    gs, ax                    ; 赋值视频段选择子

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

    mov    esp, TopOfStack

    xor edi, edi
    mov    edi, 80 * 2 * 2         ; 屏幕第 2 行, 第 0 列
    xor esi, esi
    mov esi, OffsetBootMessage
    call DisplayString

    jmp SelectorCode16:0

; ---------------------- 分页机制启动 ---------------------------
SetupPaging:
    ; 为简化处理, 所有线性地址对应相等的物理地址.

    ; 初始化页目录
    mov    ax, SelectorPageDir        ; 此段首地址为 PageDirBase
    mov    es, ax
    mov    ecx, 1024                ; 共 1K 个表项
    xor    edi, edi
    xor    eax, eax
    mov    eax, PageTblBase | 7     ; 用户级、存在于内存、可读写
.filter_pde:
    stosd
    add    eax, 4096                ; 为了简化, 所有页表在内存中是连续的.
    loop    .filter_pde

    ; 初始化所有页表 (1K 个, 4M 内存空间)
    mov    ax, SelectorPageTbl        ; 此段首地址为 PageTblBase
    mov    es, ax
    mov    ecx, 1024 * 1024        ; 共 1M 个页表项, 也即有 1M 个页
    xor    edi, edi
    xor    eax, eax
    mov    eax, 7                    ; 用户级、存在于内存、可读写
.filter_pte:
    stosd
    add    eax, 4096        ; 每一页指向 4K 的空间
    loop    .filter_pte

    ; 设置页目录表起始地址
    mov    eax, PageDirBase
    mov    cr3, eax

    ; 开启分页机制
    mov    eax, cr0
    or    eax, 80000000h
    mov    cr0, eax

    ret

; ------------------------- 打印字符串 -------------------------
DisplayString:
    push eax
    mov    ah, 8Ch                    ; 0000: 黑底    1100: 红字
    cld
.loop_label:
    lodsb
    test al, al
    jz .over_print
    mov    [gs:edi], ax
    add    edi, 2
    jmp .loop_label

.over_print:
    pop eax
    ret

SegCode32Len        equ    $ - LABEL_SEG_CODE32

; 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 word 0:LABEL_REAL_ENTRY    ; 段地址会在程序开始处被设置成正确的值

Code16Len    equ    $ - LABEL_SEG_CODE16
; END of [SECTION .s16code]

8. 参考资料

https://en.wikipedia.org/wiki/Paging。 https://slide-finder.com/view/IA32-Paging-Scheme-Introduction.268455.html。 https://en.wikipedia.org/wiki/Translation\_lookaside\_buffer。 https://en.wikipedia.org/wiki/Memory\_type\_range\_register。 https://en.wikipedia.org/wiki/Control\_register。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-02-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小脑斧科技博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 引言
  • 2. 分页机制
  • 3. 页目录表与页表
    • 3.1. 页目录表项 PDE 与页表项 PTE 的结构
      • 3.2. cr0 寄存器
        • 3.2.1. PG 位
        • 3.2.2. WP 位
        • 3.2.3. CD 位与 TLB
    • 4. 实战开启内存分页
      • 4.1. 创建页目录段
        • 4.1.1. 在 GDT 中创建描述符
        • 4.1.2. 创建页目录段选择子
      • 4.2. 创建页表段
        • 4.2.1. 创建页表选择子
      • 4.3. 填充 PDE
        • 4.3.1. stosd 指令与 loop 指令
      • 4.4. 填充 PTE
        • 4.5. 设置 CR3 寄存器
          • 4.6. 开启分页机制
            • 4.7. 退出实地址模式时复位 PG 位
            • 5. 执行结果
            • 7.1 附录 -- 专题目录
            • 7.2 附录 — 完整代码
            • 8. 参考资料
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档