畅游内存分页

简介: 畅游内存分页

页表映射实验

  • 这个实验是我们后续实现多任务(多进程)的基础
  • 前面我们也提到过,操作系统中每个任务都有自己独立使用的页表,当切换任务的时候,也需要要切换页表
  • 每一个任务都有自己的虚拟地址空间,那么必然出现多个任务同时使用同一个虚拟地址的情况,那么操作系统就必须实现:将同一个虚拟地址映射到不同的物理地址
  • 接下来我们就来做一个实验:给定一个虚拟地址 0x401000, 在页表 0 中将其映射到实际物理地址 0xD01000;在页表 1 中将其映射到实际物理地址 0xE01000。给 0xD01000 这个实际物理地址处写入数据 “ABCD”;给 0xE01000 这个实际物理地址处写入数据 “abcd”。分别查看切换到页表 0 和页表 1 后读取虚拟地址 0x401000 中的数据
  • 由于整个实验比较复杂,我们就把它拆分成以下几个部分
  • 准备两组页表并实现页表切换功能
  • 在保护模式下,修改指定物理内存中的数据:将数据 “ABCD” 写入物理内存 0xD01000 处,将数据 “abcd” 写入物理内存 0xE01000 处
  • 修改页表:修改页表 0,将虚拟地址 0x401000 映射到实际物理地址 0xD01000 处;修改页表 1,将虚拟地址 0x401000 映射到实际物理地址 0xE01000 处
  • 切换页表,分别读取虚拟地址 0x401000 中的数据

准备两组页表并实现页表切换功能

  • 基本的准备工作有:
  • 构建两组二级页表
  • 切换页表,开启分页机制
  • 参照上一章节的代码,复制粘贴后稍加改动即可,改动后完整代码 loader.asm
; 页目录表与页起始地址,注意地址不要踩踏
PAGE_DIR_BASE0    equ    0x100000
PAGE_TAB_BASE0    equ    0x101000
PAGE_DIR_BASE1    equ    0x600000
PAGE_TAB_BASE1    equ    0x601000
; 全局描述符表定义
PAGE_DIR_DESC0 : Descriptor PAGE_DIR_BASE0, 4095, DA_DRW + DA_32
PAGE_TAB_DESC0 : Descriptor PAGE_TAB_BASE0, 1023, DA_DRW + DA_32 + DA_LIMIT_4K
PAGE_DIR_DESC1 : Descriptor PAGE_DIR_BASE1, 4095, DA_DRW + DA_32
PAGE_TAB_DESC1 : Descriptor PAGE_TAB_BASE1, 1023, DA_DRW + DA_32 + DA_LIMIT_4K
; 段选择符定义
PAGE_DIR0_SELECTOR equ   (0x0005 << 3) + SA_RPL0 + SA_TIG
PAGE_TAB0_SELECTOR equ   (0x0006 << 3) + SA_RPL0 + SA_TIG
PAGE_DIR1_SELECTOR equ   (0x0007 << 3) + SA_RPL0 + SA_TIG
PAGE_TAB1_SELECTOR equ   (0x0008 << 3) + SA_RPL0 + SA_TIG
  • 根据原 setup_page 创建页表并开启内存分页函数,我们实现另一个函数 Init_Page_Table,使其可以根据传参的不同从而创建不同的页表;切换页表的功能我们也实现成一个函数 Switch_Page_Table
  • 查看 setup_page 函数中页表创建部分,发现其中有三处是用宏定义固定的,它们分别是 PAGE_DIR_SELECTOR(页目录选择子)、PAGE_TAB_SELECTOR(页表选择子) 和 PAGE_TAB_BASE(页表基地址),把这 3 处变为参数输入即可实现不同页表的的创建
  • 我们先规划一下函数参数
; eax --> 页目录选择子
; ebx --> 页表选择子 
; ecx --> 页表基地址
Init_Page_Table:
  ...
  ret
  • 接下来 Init_Page_Table 函数的具体实现见源码吧
  • Switch_Page_Table 函数的实现:先将寄存器 cr0 的 PG 位清 0,暂时先关闭内存分页功能,再将页目录表首地址写入控制寄存器 cr3,最后再将寄存器 cr0 的 PG 位置 1,开启内存分页
; 寄存器 cr0 的 PG 位清 0,暂时先关闭内存分页功能
mov eax, cr0
and eax, 0x7FFFFFFF ; and 按位与
mov cr0, eax
; 将页目录表首地址写入控制寄存器 cr3
mov eax, [esp]      ; [esp] = Switch_Page_Table 运行前 eax
mov cr3, eax
; 寄存器 cr0 的 PG 位置 1,开启内存分页
mov eax, cr0
or  eax, 0x80000000
mov cr0, eax

在保护模式下,修改指定物理内存中的数据

  • 我们想把数据 “ABCD” 写入实际物理内存 0xD01000 中以及把数据 “abcd” 写入实际物理内存 0xE01000 中,然而这种指定实际物理地址的内存访问方式不是实模式下才有的吗。那么我们如何在保护模式下实现访问指定的物理内存呢
  • 保护模式下想要实现访问指定的物理内存,就需要用到内存平坦模型,而想要理清楚内存平坦模型,就需要了解 x86 芯片的发展历史
  • 早期的 x86 芯片寄存器是 16 位的,但地址却是 20 位的(1M的内存空间),一个 16 位的寄存器最多能访问 64K 的内存空间,想要访问 1M 的内存空间,于是就不得不使用两个寄存器组合的方式才能实现,这种方式(段基址:段内偏移)我们称为段式内存管理。
  • 随着硬件的发展,出现了 32 位寄存器,于是我们可以用一个 32 位寄存器直接访问 4G 的内存空间了,也就不需要两个寄存器了。这时候就引出了平坦内存模型,即将所有的 4G 内存看成一整个段,段基址为 0,段大小就是整个 4G 物理内存容量。此时段内偏移不就可以看成是实际物理地址了吗
  • 用代码实现内存平坦模型其实非常简单,就是将整个内存当成一个段,定义其描述符和选择符
; 全局描述符表定义
FLAT_MODE_DESC : Descriptor 0, 0xFFFFF, DA_DRW + DA_32 + DA_LIMIT_4K
; 段选择符定义
FLAT_MODE_SELECTOR equ   (0x0009 << 3) + SA_RPL0 + SA_TIG
  • 如果我们想访问实际物理地址 0x00FF00FF,实现方式如下(注意:想访问实际物理地址是不能开启内存分页机制的)
mov eax, FLAT_MODE_SELECTOR
mov es, eax
mov ecx, [es:0x00FF00FF]
  • 好了,让我们继续实验,先看完整代码:loader.asm
  • 首先,在 DATA_SEGMENT 数据段中添加数据 “ABCD” 和 “abcd”
TEST_DATA_ABCD db "ABCD"     ; 数据 "ABCD"
; "ABCD" 在数据段 DATA_SEGMENT 中的偏移量
TEST_ABCD_OFFSET equ  TEST_DATA_ABCD - DATA_SEGMENT
TEST_DATA_abcd db "abcd"     ; 数据 "abcd"
; "abcd" 在数据段 DATA_SEGMENT 中的偏移量
TEST_abcd_OFFSET equ  TEST_DATA_abcd - DATA_SEGMENT
  • 下面我们来实现一个函数 MemCpy32 用于把数据 "ABCD" 和 "abcd" 拷贝到指定的指定的物理地址处
; ds:esi --> 源
; es:edi --> 目标
; ecx    --> 长度
MemCpy32:
  ; 入栈...
  .s: mov al, [ds:esi]    ; [ds:esi] : 源地址
      mov [es:edi], al    ; [es:edi] : 目标地址
      inc edi             ; edi = edi + 1
      inc esi             ; esi = esi +1
      loop .s             ; 循环次数由 ecx 决定
  ; 出栈...
  ret
  • 最后调用 MemCpy32,将数据 “ABCD” 拷贝到实际物理地址 0xD01000 处,将数据 “abcd” 拷贝到实际物理地址 0xE01000 处。注意要在 Switch_Page_Table 函数执行前完成拷贝
mov esi, TEST_ABCD_OFFSET   ; 源
mov edi, 0xD01000           ; 目标
mov ecx, 4                  ; 循环次数
call MemCpy32
mov esi, TEST_abcd_OFFSET   ; 源
mov edi, 0xE01000           ; 目标
mov ecx, 4                  ; 循环次数
call MemCpy32
  • make 后反汇编

ndisasm -o 0x900 loader.bin  > loader.txt

  • 找到程序最后 “jmp $” 的地址为 0xA92,在 0xA92 处打断点后查看内存地址 0xD01000 和 0xE01000 处的数据
<bochs:1> b 0xa92
<bochs:2> c
<bochs:3> xp 0xD01000
[bochs]:
0x00d01000 <bogus+       0>:    0x44434241
<bochs:4> xp 0xE01000
[bochs]:
0x00e01000 <bogus+       0>:    0x64636261
  • 通过查 ASCII 表,0x44434241 即数据 "ABCD", 0x64636261 即数据 "abcd"

修改页表

  • 修改页表和下面的切换页表,分别读取虚拟地址 0x401000 中的数据实现代码:loader.asm
  • 我们创建的两个页表其中的内容是一模一样的,现在我们想要使得同一个虚拟地址 0x401000 映射到不同的物理地址 0xD01000 和 0xE01000
  • 先把虚拟地址 0x401000 转换成二进制 01 0000000001 000000000000,高 10 位值为 1,中 10 位值为 1,低 12 位值为 0
  • 因为是做实验,所以我们手动找到对应的内存处改一下。
  • 还是利用之前的程序 loader.asm,在 0xA92 处打断点,根据虚拟地址高中低 3 个值找到子页表项,将其修改后使其映射到我们想要的实际物理地址 0xD01000 和 0xE01000
<bochs:1> b 0xa92
<bochs:2> c
<bochs:3> xp 0x100004
[bochs]:
0x00100004 <bogus+       0>:    0x00102007
<bochs:4> xp 0x102004
[bochs]:
0x00102004 <bogus+       0>:    0x00401007
<bochs:5> xp 0x600004
[bochs]:
0x00600004 <bogus+       0>:    0x00602007
<bochs:6> xp 0x602004
[bochs]:
0x00602004 <bogus+       0>:    0x00401007
  • 通过查看断点调试的方式我们得到,虚拟地址 0x401000 在页表 0 中对应的子页表项所处的实际物理地址是 0x102004;在页表 1 中对应的子页表项所处的实际物理地址是 0x602004,而他们的子页表项的值都为 0x00401007。那么接下来我们要做的就是把实际物理地址 0x102004 中的值改为 0xD01007,把实际物理地址 0x602004 中的值改为 0xE01007。其中低 12 位 0x007 是属性
mov eax, 0xD01007
mov [es:0x102004], eax      ; 将数值 0xD01007 写入 0x102004 地址处
mov eax, 0xE01007
mov [es:0x106004], eax      ; 将数值 0xE01007 写入 0x602004 地址处
  • 要想显得更高大上一点,我们也可以实现一个映射函数 MapAddress,用于修改指定的页表,将指定的虚拟地址与指定的物理地址映射起来。MapAddress 函数主体实现见源码

切换页表,分别读取虚拟地址 0x401000 中的数据

  • 我们可以利用 print_str_32 函数将 0x401000 地址处的字符串字印出来,print_str_32 函数使用如下:
mov ax, FLAT_MODE_SELECTOR
mov ds, ax
mov ebp, 0x401000
mov bl, 0x0F        ; 打印属性,黑底白字
; 坐标 (0, 3)
mov dl, 0x00
mov dh, 0x03
call print_str_32
  • 页表映射实验结束,来欣赏一下最终效果吧

页表映射实验升级之函数

  • 前面的实验中,虚拟地址 0x401000 映射的物理地址 0xD01000 和 0xE01000 中都是数据。如果映射的物理地址中是一个函数入口,那么是否会发生函数调用呢?
  • 答案是:会。做个实验验证一下呗,实验代码:loader.asm
  • 在做实验之前,我们先了解一下函数的本质
  • 函数在内存中表现形式为一段数据
  • 这段数据遵循一定的规则,能够被处理器识别
  • 这段时间具有可执行属性,并且只读,不可修改
  • 如何在指定的地址 0xD01000 和 0xE01000 处创建函数呢?
  • 我们可以先在写好函数,然后利用先前实现的 MemCpy32 函数将目标函数拷贝到指定的物理地址
  • 开始实验,在前面实验代码基础上改动,先添加两个函数,函数没啥实际意义,就是给 eax 寄存器赋值。注意:由于函数被复制到其它地方,已不在本身所在的段内,所以应该用 retf
test_func1:
  mov eax, 8
  retf
test_func1_len equ      $ - test_func1
test_func2:
  mov eax, 9
  retf
test_func2_len equ      $ - test_func2
  • 删掉代码中数据 "ABCD" 和 "abcd" 的拷贝改成拷贝刚添加的测试函数 test_func1 和 test_func2
mov esi, test_func1           ; 源
mov edi, 0xD01000             ; 目标
mov ecx, test_func1_len       ; 循环次数
call MemCpy32
mov esi, test_func2           ; 源
mov edi, 0xE01000             ; 目标
mov ecx, test_func2_len       ; 循环次数
call MemCpy32
  • 然后删掉代码中的数据 "ABCD" 和 "abcd" 的打印调用,改为:
call FLAT_MODE_SELECTOR : 0x401000
  • 编译代码,成功,运行一下,崩了。提示关键信息如下:
read_virtual_byte_32(): segment limit violation
  • 根据提示信息,我们查找到段描述符 FLAT_MODE_DESC 的段属性是可读写的,但不可执行,可是我们要其具有可执行权限,怎么办?
  • 解决办法:再来一个平坦模型数据段描述符,具有可执行权限
; 段描述符
FLAT_MODE_C_DESC : Descriptor 0, 0xFFFFF, DA_C + DA_32 + DA_LIMIT_4K
; 段选择符
FLAT_MODE_C_SELECTOR equ (0x000A << 3) + SA_RPL0 + SA_TIG
  • 调用处也改为:
; call FLAT_MODE_SELECTOR : 0x401000
call FLAT_MODE_C_SELECTOR : 0x401000
  • 再次编译运行,发现又崩了,崩溃原因跟上次一样,这次又是啥原因呢?
  • 经查找后发现,MemCpy32 函数输入参数 ds:esi --> 源,我们程序中ds 初始化成了数据段,我们要拷贝的函数不在数据段中
mov ax, DATA_SELECTOR
mov ds, ax
  • 因为是做实验,所以我们临时把 ds 初始化为平坦模型段(可读写)
mov ax, FLAT_MODE_SELECTOR
mov ds, ax 
; ...
call MemCpy32
mov ax, DATA_SELECTOR
mov ds, ax
  • OK,这次程序运行不会崩溃了。但是由于测试函数仅给 eax 寄存器赋值,所以只能通过反汇编后断点调试来验证
  • 找到 call FLAT_MODE_C_SELECTOR : 0x401000 这条指令地址为 0xAC4 和 0xAD5,于是在这两个地址处打好断点,运行到断点处后再单步执行
(0) [0xfffffff0] f000:fff0 (unk. ctxt): jmp far f000:e05b         ; ea5be000f0
<bochs:1> b 0xac4
<bochs:2> b 0xad5
<bochs:3> c
...
(0) [0x00000ac4] 0008:000000be (unk. ctxt): call far 0050:00401000    ; 9a001040005000
<bochs:4> s
Next at t=23065731
(0) [0x00d01000] 0050:00401000 (unk. ctxt): mov eax, 0x00000008       ; b808000000
<bochs:5> c
(0) Breakpoint 2, 0x00000ad5 in ?? ()
Next at t=23065746
(0) [0x00000ad5] 0008:000000cf (unk. ctxt): call far 0050:00401000    ; 9a001040005000
<bochs:6> s
Next at t=23065747
(0) [0x00e01000] 0050:00401000 (unk. ctxt): mov eax, 0x00000009       ; b809000000
  • 从 bochs 打印信息中我们可以看出 test_func1 和 test_func2 函数被成功执行

分页相关的其它操作

  • 页表属性中的 P 位可作为标志位,判断是否需要进行页请求
  • 当需要页请求/页交换时,需要修改页目录或二级页表
  • 平坦内存模型可用于加载/移除物理页
  • 关于这些功能,后面章节中我们再做详细介绍
目录
相关文章
|
14天前
|
算法 程序员
深入理解操作系统内存管理:分页系统的优势与挑战
【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月前
|
存储 关系型数据库 C语言
开启内存分页
开启内存分页
27 0
|
3月前
|
存储 缓存 算法
内存分页机制
内存分页机制
23 0
|
5月前
|
存储 缓存 Linux
系统内存管理:虚拟内存、内存分段与分页、页表缓存TLB以及Linux内存管理
虚拟内存的主要作用是提供更大的地址空间,使得每个进程都可以拥有大量的虚拟内存,而不受物理内存大小的限制。此外,虚拟内存还可以提供内存保护和共享的机制,保护每个进程的内存空间不被其他进程非法访问,并允许多个进程共享同一份物理内存数据,提高了系统的资源利用率。虚拟内存的实现方式有分段和分页两种,其中分页机制更为常用和灵活。分页机制将虚拟内存划分为固定大小的页,将每个进程的虚拟地址空间映射到物理内存的页框中。为了减少页表的大小和访问时间,采用了多级页表的方式,将大的页表划分为多个小的页表,只加载需要的页表项,节约了内存空间。
196 0
系统内存管理:虚拟内存、内存分段与分页、页表缓存TLB以及Linux内存管理
|
6月前
内存分段和按需分页
内存分段和按需分页
|
6月前
|
调度
虚拟内存和按需分页
虚拟内存和按需分页