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

2020-02-06 10:48:04   最后更新: 2020-02-06 12:21:59   访问数量:96




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

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

进军保护模式

利用调用门实现特权级间跳转(上) -- 原理篇

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

 

分段让操作系统具备了对内存的保护能力,通过描述符表、选择子的多级跳转,让每一段内存都增加了一系列属性,从而可以实现读、写、执行等权限以及为不同程序赋予不同特权的保护功能

在此前的文章中,我们已经提到,通过 LDT 来解决进程间内存独立的问题,其代价是寄存器的反复加载,这对于 CPU 来说是一件较为耗时的操作,于是,80386 开始,Intel 引入了内存分页功能,相比于 LDT,更为灵活高效,因此 LDT 已经基本不会被使用了

那么,分页究竟是一种什么样的机制,又是如何实现的呢?本文我们就来一探究竟

 

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

此时,操作系统迫切需要一种类似 CPU 任务切换的机制来对内存进行切换,可以想到,这需要从两个方面来进行考虑:

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

 

经过上述的离散化与虚拟化,分页机制就这样诞生了

从 80386 开始,内存被分为 4KB 固定大小的“页”,他们在需要使用时载入内存,不需要使用时可以被置换到磁盘上,由分页机制将程序持有的固定的线性地址动态映射到物理地址上

操作系统的内存管理 -- 分段与分页、虚拟地址、逻辑地址、线性地址、物理地址

 

 

 

如图所示,在 80X86 的软硬件设计中,实现了两级页表

第一级页表 Root page table 被称为“页目录表”,总计占用 4KB 空间,每个表项占用 4 字节,共计 1024 个表项,因此通过线性地址的最高 10bits 可以索引每一个表项,每个表项简称“PDE”(Page Directory Entry)

第二级页表是直接保存物理页基地址的列表,他同样有 1024 个表项,每个表项 4 字节,通过线性地址的中间 10bits 来进行索引

线性地址剩余的 12bits 用来索引最终指向的页面的 4KB 内存

 

页目录表项 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 位物理地址了

 

cr0 寄存器

此前,我们介绍了 CPU 的控制寄存器

进军保护模式

 

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

 

 

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

 

PG 位

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

 

WP 位

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

如果 WP 位为 1,那么系统级程序访问用户级内存时,仍然要校验用户级内存的 R/W 位是只读还是读写权限

 

CD 位与 TLB

CD 位是页表缓冲位,用来标识是否开启 CPU 页表缓冲

所谓的“页表缓冲”简称为“TLB”(translation lookaside buffer),指的是 CPU 内的一块缓冲区,用来缓存经常访问的页目录和页表项,从而加快访问页目录与页表的时间

如果 CD 位为 0,即不开启 CPU 页表缓冲,那么 PDE 与 PTE 中的 PWT 位、PCD 位与 G 位也将不起作用

 

接下来我们就来实战开启内存分页机制

经过上述讲解,我们已经对分页机制了解的十分清楚了,那么,如何在我们已有的分段代码基础上实现分页机制呢?

为了以最快速度上手实战,我们这里不考虑线性地址与物理地址的映射关系,直接取线性地址 = 物理地址,同时假设内存地址空间足够容纳所有页面,并且页、页表在内存中均连续

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

 

创建页目录段

在 GDT 中创建描述符

PageDirBase equ 200000h ; 页目录开始地址: 2M LABEL_DESC_PAGE_DIR: Descriptor PageDirBase, 4095, 92h ; Page Directory,可读写

 

 

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

 

创建页目录段选择子

SelectorPageDir equ LABEL_DESC_PAGE_DIR - LABEL_GDT

 

 

创建页表段

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

 

 

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

 

创建页表选择子

SelectorPageTbl equ LABEL_DESC_PAGE_TBL - LABEL_GDT

 

 

填充 PDE

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

; 初始化页目录 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 大小的页目录表内

 

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 寄存器

 

填充 PTE

; 初始化所有页表 (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

 

 

设置 CR3 寄存器

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

 

 

开启分页机制

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

 

 

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

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

 

 

 

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 

 

; ---------------- 内存段描述符宏 ------------- ; 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]

 

 

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

 






操作系统      os      虚拟地址      物理地址      分页      内存      system      技术贴      mmu      保护模式     


京ICP备15018585号