开启内存分页

简介: 开启内存分页

x86系列处理器上的内存分页

  • 硬件层直接支持内存分页机制
  • 默认情况下并没有打开分页机制
  • 分页机制开启后,使用二级页表对内存进行管理

启用分页机制步骤

  1. 创建页目录表及页表
  2. 将页目录表首地址写入控制寄存器 cr3
  3. 寄存器 cr0 的 PG 位置 1

启动分页机制代码实现

  • 找到以前的代码 loader.asm,在这个代码的基础上做更改
  • 回顾代码的框架
boot.asm 跳转到 loader.asm,并打印 “Welcome to KOS.” 
      |
CODE16_START   ; 实模式,打印 “Loader...”
      |
CODE32_START   ; 进入保护模式,打印 “Enter protection”
  • 先上改动后的完整代码 loader.asm
  • 为了程序的美观,我们把开启分页机制的工作都封装成了一个函数 setup_page
  • 先来看一下开启分页机制的第 2 步和第 3 步,这两步比较简单
; 将页目录表首地址写入控制寄存器 cr3
mov eax, PAGE_DIR_BASE
mov cr3, eax
; 寄存器 cr0 的 PG 位置 1
mov eax, cr0
or  eax, 0x80000000
mov cr0, eax
  • 下面我们就实现页目录表及页表的创建
  • 知识点准备
  • 页目录占用 1 内存页(可访问 1024 个子页表)
  • 单个子页表占用 1 内存页(可访问 1024 个页面)
  • 页面起始地址按 4K 字节对齐(4096 的整数倍)
  • 分页后可访问的虚拟地址空间为:4K * (1024 * 1024) = 4G
  • 首先,我们脑子中要有页表的整体样子,如下图(由下往上看)。PDE:页目录项;PTE:页表项

  • 从图中可以看出,为了让页表和更紧凑一些,页目录后面紧跟着页表,其实这不是必须的
  • 页目录表和页表都存在于物理内存之中,它们自身也要有个“安身”的地方,我们就把页目录表放在物理地址 0x100000 处(),页目录本身占 4KB,所以第一个页表的物理地址是 0x101000

  • 第一个页目录项(忽略属性)PDE0 的值为 0x101000,第二个页目录项(忽略属性)PDE1 的值为 0x102000,依次类推...
  • 第一个页表项(忽略属性)PTE0 的值为 0x0(第一个页表我们就让它指向实际物理内存 0x0 处),第二个页表录项(忽略属性)PTE1 的值为 0x1000,依次类推... 不过这在图中没有显示出来
  • 从 PDE0 找到 PTE0,然后再从 PTE0 找到实际内存 0x0;从 PDE1 找到 PTE1,然后再从 PTE1 找到实际内存 0x1000;从 PDE2 找到 PTE2,然后再从 PTE2 找到实际内存 0x2000。依次类推...
  • 在实现创建页目录表和页表代码之前,我们先学习一个汇编小知识
; 传送指令
stosb / stosw / stosd
  • 把 al / ax / eax 中的值存储到 es:edi 指向的内存单元中
  • 同时 edi 的值根据方向标志自增或自减(cld:自增 / std:自减)
  • 举例:
mov ax, 0
mov es, ax  ; es = 0
mov edi, 0  ; edi = 0
mov eax, 0xFF
cld
stosd
; 程序执行到这里后
; 地址 0:0 所指向的内存被赋值 0xFF
; edi = 0x04  ; eax 为 4 字节,顾 edi 自增 4 字节
  • 由于我们是在保护模式下开启分页机制,所以把页目录表和页表也当成一种数据段处理,于是段描述符相关操作来一遍
; 增加页目录表和页表的段描述符和选择子
PAGE_DIR_DESC : Descriptor PAGE_DIR_BASE, 4095,       DA_DRW + DA_32
; 子页表我们把颗粒度 G 设为 1,则内存单位就变为了 4K ,于是子页表内存数据段总大小 = (1023+1)*4K 
PAGE_TAB_DESC : Descriptor PAGE_TAB_BASE, 1023,       DA_DRW + DA_32 + DA_LIMIT_4K
PAGE_DIR_SELECTOR  equ   (0x0005 << 3) + SA_RPL0 + SA_TIG
PAGE_TAB_SELECTOR  equ   (0x0006 << 3) + SA_RPL0 + SA_TIG
  • 就剩下最后的页目录表和页表实现代码实现了,用 C 语言来理解就是循环给页目录表和页表这两个数组填充每一个元素
; 创建页目录表
    mov ax, PAGE_DIR_SELECTOR
    mov es, ax
    mov edi, 0          ; es:edi -> PAGE_DIR_SELECTOR:0
    mov cx, 1024        ; 循环 1024 次,页目录共 1024 项
    mov eax, PAGE_TAB_BASE + PG_P + PG_US_U + PG_RW_W ; eax = 第一个子页表的地址和属性
    cld                 ; edi 自增
.creat_pde:             ; 创建页目录项
    stosd               ; 把 eax 中的值写入 [es:edi] 指向的内存中
    add eax, 4096       ; eax = eax + 4K
    loop .creat_pde
; 创建页表
    mov ax, PAGE_TAB_SELECTOR
    mov es, ax
    mov edi, 0          ; es:edi -> PAGE_TAB_SELECTOR:0
    mov ecx, 1024*1024  ;  循环 1024*1024 次,共 1024 个页表,每个页表有 1024 个页表项
    mov eax, PG_P + PG_US_U + PG_RW_W ; 只有属性,没有基地址,因为第一个页表项指向的就是物理地址 0x0 处
    cld
.creat_pte:             ; 创建页表项
    stosd               ; 把 eax 中的值写入 [es:edi] 指向的内存中
    add eax, 4096       ; eax = eax + 4K
    loop .creat_pte

发现

  • 我们来改动一下代码
; 原代码 call setup_page 开启分页机制后直接就死循环了
mov ebp, msg2Offset
mov bl, 0x0F        ; 打印属性,黑底白字
; 坐标 (0, 2)
mov dl, 0x00
mov dh, 0x02
call print_str_32
call setup_page
jmp $
  • 改动后
; 改动后,先 call setup_page 开启分页机制,再打印"Enter protection"
call setup_page
mov ebp, msg2Offset
mov bl, 0x0F        ; 打印属性,黑底白字
; 坐标 (0, 2)
mov dl, 0x00
mov dh, 0x02
call print_str_32
jmp $
  • 编译执行,发现程序居然跟改动前一样,按道理开启分页机制后,映射后的地址不应该改变,导致开启分页机制后的程序无法执行吗?
  • 猜测:我们的分页机制没有开启成功?
  • 验证一下,把 add eax, 4096 注释掉,破坏页表的创建
.creat_pte:         ; 创建页表项
stosd               ; 把 eax 中的值写入 [es:edi] 指向的内存中
; add eax, 4096       ; eax = eax + 4K
loop .creat_pte
  • 编译运行一下,发现 "Enter protection" 未打印出来,程序崩溃了,感觉分页机制好像是开启了的
  • 这到底是什么原因呢?
  • 以一个具体地址 0x00804ABC,分析其映射后的地址变成了多少。我们把 0x00804ABC 转成二进制 100000000100101010111100,其中低 12 位的值为:0xABC,中 10 位的值为:0x4,高 10 位的值为:0x2。我们的第一个子页表对应的实际物理地址 0x0 处,0x00804ABC 映射后的实际物理地址为:4096 * (1024 * 0x02 + 0x04) + 0xABC = 0x00804ABC。映射前后的地址居然是一样的,自己可以再随便换个地址映射一下看看是不是一样。
  • 让我们来反汇编后断点调试证明
  • make 之后反汇编一下
ndisasm -o 0x900 loader.bin  > loader.txt
  • 找到程序最后死循环处(jmp $):0xA14,使用 “b 0xA14”在 0xA14 处打断点,执行 “c” 命令后程序运行到断点处,根据页目录表首地址 0x100000 和 高 10 位的值 0x2,于是我们找到 0x00804ABC 地址对应的页目录项实际对应的物理地址为 0x100000 + 2*4 = 0x100008,用 “xp 0x100008”命令 查看 0x100008 地址处的值为 0x00103007,其中低 12 位是属性,高 20 位为对应的子页表首地址,于是我们再根据中 10 位的值 0x4 计算出对应的子页表中的页表项地址为 0x103000 + 4*4 = 0x103010,然后我们使用 “xp 0x103010” 命令查看 0x103010 地址处的值,发现其值为 0x00804007,去掉低 12 位的属性,高 20 位的地址为 0x00804000,最后我们再加上偏移量 0xABC 得到最终映射的物理地址为 0x00804ABC
<bochs:3> xp 0x100008
0x00100008 <bogus+       0>:    0x00103007
<bochs:4> xp 0x103010
0x00103010 <bogus+       0>:    0x00804007
  • 当前的分页方式使得:任意虚拟地址都被直接映射为物理地址,因此,setup_page 调用的时间并不影响程序的执行结果
  • 由此可以看出,我们目前构建的页目录和页表其实是一种非常简单的方式,简单到直接将虚拟地址映射成物理地址
目录
相关文章
|
16天前
|
算法 程序员
深入理解操作系统内存管理:分页系统的优势与挑战
【4月更文挑战第7天】 在现代操作系统中,内存管理是一项至关重要的任务,它确保了计算机能够高效、安全地运行各种程序。分页系统作为内存管理的一种技术,通过将物理内存分割成固定大小的单元——页面,为每个运行的程序提供了一种独立且连续的内存地址空间。该技术不仅简化了内存分配,还允许更高效的内存使用和保护。本文探讨了分页系统的核心原理,优势以及面临的挑战,旨在为读者揭示其在操作系统设计中的重要性。
|
5月前
|
存储 网络虚拟化 索引
【OSTEP】分页(Paging) | 页表中究竟有什么 | 页表存在哪 | 内存追踪
【OSTEP】分页(Paging) | 页表中究竟有什么 | 页表存在哪 | 内存追踪
45 0
|
1月前
|
缓存 算法 安全
深入理解操作系统内存管理:分页系统的优势与挑战
【2月更文挑战第30天】 在现代操作系统中,内存管理是核心功能之一,它负责将有限的物理内存资源分配给多个并发运行的进程。分页系统作为内存管理的一种流行技术,其通过虚拟到物理地址的映射提供了程序的逻辑地址空间,并允许更高效的内存分配和保护。本文旨在探讨分页系统的关键优势,包括其如何提升内存利用率、实现内存保护以及支持多任务处理。同时,我们也将分析分页机制带来的挑战,诸如页面置换算法的效率问题、页表管理和TLB(Translation Lookaside Buffer)的维护等。
|
1月前
|
存储 算法
深入理解操作系统内存管理:分页系统的优势与挑战
【2月更文挑战第29天】 在现代操作系统中,内存管理是核心功能之一,它负责有效地分配、跟踪和回收内存资源。分页系统作为一种内存管理技术,已经成为大多数操作系统的标准配置。本文将探讨分页系统的原理、优势以及面临的挑战。通过对分页机制的深入分析,我们旨在提供一个全面的视角,以帮助读者更好地理解这一关键技术如何影响操作系统的性能和稳定性。
|
5月前
|
存储 算法 程序员
【OSTEP】超越物理内存:机制 | 请求分页 | 交换位与存在位 | 页错误
【OSTEP】超越物理内存:机制 | 请求分页 | 交换位与存在位 | 页错误
30 0
|
3月前
|
关系型数据库 芯片
畅游内存分页
畅游内存分页
28 0
|
3月前
|
存储 缓存 算法
内存分页机制
内存分页机制
23 0
|
5月前
|
存储 缓存 Linux
系统内存管理:虚拟内存、内存分段与分页、页表缓存TLB以及Linux内存管理
虚拟内存的主要作用是提供更大的地址空间,使得每个进程都可以拥有大量的虚拟内存,而不受物理内存大小的限制。此外,虚拟内存还可以提供内存保护和共享的机制,保护每个进程的内存空间不被其他进程非法访问,并允许多个进程共享同一份物理内存数据,提高了系统的资源利用率。虚拟内存的实现方式有分段和分页两种,其中分页机制更为常用和灵活。分页机制将虚拟内存划分为固定大小的页,将每个进程的虚拟地址空间映射到物理内存的页框中。为了减少页表的大小和访问时间,采用了多级页表的方式,将大的页表划分为多个小的页表,只加载需要的页表项,节约了内存空间。
199 0
系统内存管理:虚拟内存、内存分段与分页、页表缓存TLB以及Linux内存管理
|
6月前
内存分段和按需分页
内存分段和按需分页
|
6月前
|
调度
虚拟内存和按需分页
虚拟内存和按需分页

热门文章

最新文章