图片无法显示,请右键点击新窗口打开图片

在 Linux 里,不同的场景和不同的内存行为都会导致缺页,缺页异常处理函数为了更好的处理缺页,因此需要获得引起缺页的原因。不同的架构有特定的寄存器在发生缺页异常时,将缺页原因写入指定寄存器,那么缺页异常处理函数可以从该寄存器中获得异常原因,进而更准确的处理缺页. 在 Intel X86 架构上发生缺页(页面故障)的原因包括以上几种,硬件上使用一个 32 位寄存器进行维护,每个 Bit 的含义如下:

  • BIT0 PRESENT: 置位时表示因为违法页级权限保护,例如进程没有满足 Protection Key 设定的权限导致缺页; 清零时表示因为虚拟内存不在系 统物理地址空间,这里分为两种情况,第一种情况是还没有为虚拟内存分配对应的物理内存引起的 #PF, 另外一种情况是已经为虚拟内存分配对应的物 理内存,只是物理内存被交换到 SWAP Space 上.
  • BIT1 Write/Read: 置位时表示读操作引起的缺页,例如发生缺页时进程正在读虚拟内存; 清零时表示写操作引起的缺页,例如发生缺页时正在>写虚拟内核,或者对写保护的虚拟内存执行写操作.
  • BIT2 User/Super: 置位表示用户模式访问引起的缺页; 清零表示内核模式访问引起的缺页.
  • BIT3 RSVD: 置位表示页表中 RSVD 的标志位被置位引起的.
  • BIT4 INSTR: 置位表示因为访问的指令对应的物理内存不存在引起的缺页.
  • BIT5 PK: 置位表示违法了 Protection Key 制定的权限引起的缺页
  • BIT6 SS: 置位表示访问 Shadow-Stack 引起的缺页
  • BIT15 SGX: 违法了 SGX 制定的权限引起的缺页

原理了解差不多,如果从内存场景角度来看,这些 ERROR Code 是如何产生的,那么接下来通过实践案例了解每种 ERROR CODE 产生的场景:

图片无法显示,请右键点击新窗口打开图片


ERROR CODE: PF_PRESENT

图片无法显示,请右键点击新窗口打开图片

当 CPU 访问虚拟内存时,MMU 发现虚拟内存对应的页表的 PRESENT 标志位为空,那么 MMU 会触发缺页异常(页表故障),最终确认异常的原因是 PF_PRESENT. 在 Linux 里引起 PF_PRSENT 缺页异常的场景如下:


PF_PRESENT 物理内存未分配场景

当进程采用 LazyAlloc 方式分配内存,那么调用 mmap() 函数分配内存时,系统只为进程分配虚拟内存,而只有当 CPU 访问这块虚拟内存时触发缺页异常,缺页异常中断为其分配物理内存并填充相应的页表,最终缺页异常处理返回后再次执行指令,CPU 才能正确访问内存。在这个场景里,缺页异常将异常的原因归结为 PF_PRESENT, 即虚拟内存对应页表的 PRESENT 为空。那么接下来通过实践案例了解该场景下的缺页过程,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault ERROR CODE: PF_PRESENT --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-PRESENT-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ECODE-PRESENT-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存,然后在 32 行对虚拟内存执行写操作,然后在 34 行又对虚拟内存执行读操作,最后再释放内存. 以上便是一个最基础的缺页实践案例,可以知道 32 行的写操作将触发一次缺页异常,为了更好的演示结果, 在 32 行写操作前后开启 BS_DEBUG 功能.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在缺页流程必经之路上打印 ERROR CODE 的信息,位于 ‘arch/x86/mm/fault.c’ 文件的 exc_page_fault 函数是缺页异常的入口,那么在该函数 1506 行添加 BS_DEBUG 信息,此时直接打印 ERROR CODE 信息,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-PRESENT-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到应用程序运行,并打印了 0x6000000000 处的值是 ‘B’, 此时可以看到 BS_DEBUG 打印了 [BiscuitOS-stub] ERROR CODE 0x6, 可以知道此时 BIT0 清零,那么 ERROR CODE 包含 PF_PRESENT. 以上便是未分配物理内存引起的缺页异常的场景.


PF_PRESENT 物理内存 SWAP OUT 不在系统物理地址空间场景

在开启 SWAP 功能的 Linux 上,当系统内存压力较大的时候,内核会将不经常使用的虚拟内存对应的物理内存交换到 SWAP Space,然后将物理内存释放出来,此时内核会将 SWAP Entry 信息存储到虚拟内存最后一级页表,并将页表的 PRESENT 标志位清除. 当进程再次访问这段虚拟内存时,MMU 发现页表的 PRESENT 不存在,那么触发缺页异常,并且缺页原因包括 PF_PRESENT,接着缺页异常处理函数根据页表里存储的 SWAP Entry 信息将物理页的内容加载到一个新的物理页上,并将页表指向新的物理页,最后缺页异常处理返回之后再次执行异常的指令,此时进程可以正常访问该段内存. 接下来通过一个实践案例了解该场景, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault ERROR CODE: PF_PRESENT with SWAP --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-PRESENT-SWAP-default/
# 部署源码
make download    
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ECODE-PRESENT-SWAP-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存,然后在 32 行对虚拟内存执行写操作,接着在 35 行调用 madvise 函数建议这段虚拟内存执行 MADV_PAGEOUT, 其会将虚拟内存对应的物理内存交换到 SWAP Space. 等物理内存被 SWAP OUT 之后,程序接着在 39 行再次对虚拟内存执行写操作,并在 41 行对虚拟内存执行读操作,最后就是释放内存。以上便是一个最基础的缺页实践案例,可以知道 32 行的写操作将触发一次缺页异常,这次缺页是为了防止虚拟内存映射到 ZERO Page,另外 39 行对虚拟内存的写操作也会引起缺页。为了更好的演示结果, 在 39 行写操作前后开启 BS_DEBUG 功能.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在缺页流程必经之路上打印 ERROR CODE 的信息,位于 ‘arch/x86/mm/fault.c’ 文件的 exc_page_fault 函数是缺页异常的入口,那么在该函数 1506 行添加 BS_DEBUG 信息,此时直接打印 ERROR CODE 信息,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-PRESENT-SWAP-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到应用程序运行,并打印了 0x6000000000 处的值是 ‘C’, 此时可以看到 BS_DEBUG 打印了 [BiscuitOS-stub] ERROR CODE 0x6, 可以知道此时 BIT0 清零,那么 ERROR CODE 包含 PF_PRESENT. 以上便是物理内存 SWAP OUT 不在系统物理地址空间场景.


PF_PRESENT 物理内存被内存压缩不在系统物理地址空间场景

在开启 ZSWAP 功能的 Linux 上,当系统内存压力较大的时候,内核会将不经常使用的虚拟内存对应的物理内存进行压缩,然后将物理内存释放出来,此时内核会将 ZSWAP Entry 信息存储到虚拟内存最后一级页表,并将页表的 PRESENT 标志位清除. 当进程再次访问这段虚拟内存时,MMU 发现页表的 PRESENT 不存在,那么触发缺页异常,并且缺页原因包括 PF_PRESENT,接着缺页异常处理函数根据页表里存储的 ZSWAP Entry 信息将物理页的内容解压并加载到一个新的物理页上,并将页表指向新的物理页,最后缺页异常处理返回之后再次执行异常的指令,此时进程可以正常访问该段内存. 接下来通过一个实践案例了解该场景, 实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      (zswap.enabled=1) CMDLINE on Kernel  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault ERROR CODE: PF_PRESENT with ZSWAP --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-PRESENT-ZSWAP-default/
# 部署源码
make download    
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ECODE-PRESENT-ZSWAP-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,其在 23 行调用 mmap() 函数分配一段虚拟内存,然后在 34 行对虚拟内存执行写操作,接着在 37 行调用 madvise 函数建议这段虚拟内存执行 MADV_PAGEOUT, 其会将虚拟内存对应的物理内存进行内存压缩. 等物理内存被压缩之后,程序接着在 41 行再次对虚拟内存执行写操作,并在 43 行对虚拟内存执行读操作,最后就是释放内存。以上便是一个最基础的缺页实践案例,可以知道 34 行的写操作将触发一次缺页异常,这次缺页是为了防止虚拟内存映射到 ZERO Page,另外 41 行对虚拟内存的写操作也会引起缺页。为了更好的演示结果, 在 41 行写操作前后开启 BS_DEBUG 功能.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在缺页流程必经之路上打印 ERROR CODE 的信息,位于 ‘arch/x86/mm/fault.c’ 文件的 exc_page_fault 函数是缺页异常的入口,那么在该函数 1506 行添加 BS_DEBUG 信息,此时直接打印 ERROR CODE 信息,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-PRESENT-ZSWAP-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到应用程序运行,并打印了 0x6000000000 处的值是 ‘C’, 此时可以看到 BS_DEBUG 打印了 [BiscuitOS-stub] ERROR CODE 0x6, 可以知道此时 BIT0 清零,那么 ERROR CODE 包含 PF_PRESENT. 以上便是物理内存被内存压缩不在系统物理地址空间场景.

图片无法显示,请右键点击新窗口打开图片


ERROR CODE: PF_READ

图片无法显示,请右键点击新窗口打开图片

当 CPU 访问虚拟内存时引发页面故障(缺页异常),其此时 CPU 正在执行读操作,那么缺页异常原因会包含 PF_READ,PF_READ 不能单独引起缺页异常,必须和其他异常原因一起才能引起缺页异常, 那么记下来通过一个实践案例了解包含 PF_READ 缺页异常场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault ERROR CODE: PF_READ --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-READ-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ECODE-READ-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存,然后在 32 行对虚拟内存执行读操作,然后在 34 行又对虚拟内存执行读操作,最后再释放内存. 以上便是一个最基础的缺页实践案例,可以知道 32 行的读操作将触发一次缺页异常,为了更好的演示结果, 在 32 行读操作前后开启 BS_DEBUG 功能.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在缺页流程必经之路上打印 ERROR CODE 的信息,位于 ‘arch/x86/mm/fault.c’ 文件的 exc_page_fault 函数是缺页异常的入口,那么在该函数 1506 行添加 BS_DEBUG 信息,此时直接打印 ERROR CODE 信息,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-READ-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到应用程序运行,并打印了 0x6000000000 处的值是空的,因为对匿名内存读操作引发的缺页会分配 ZERO PAGE,因此读到的数据为 0, 此时可以看到 BS_DEBUG 打印了 [BiscuitOS-stub] ERROR CODE 0x4, 可以知道此时 BIT1 清零,那么 ERROR CODE 包含 PF_READ. 以上便是包含 PF_READ 缺页异常的场景.

图片无法显示,请右键点击新窗口打开图片


ERROR CODE: PF_WRITE

图片无法显示,请右键点击新窗口打开图片

当 CPU 对虚拟内存执行写操作时,MMU 发现虚拟内存对应的页表的 WRITE 标志位为空,那么 MMU 会触发缺页异常(页表故障),最终确认异常的原因是 PF_WRITE. 另外由于其他原因触发缺页异常时,CPU 正在执行写操作,那么异常原因也会包含 PF_WRITE。因此 PF_WRITE 可以独立触发缺页异常, 也可以和其他异常原因一起引起缺页异常。在 Linux 里引起 PF_WRITE 缺页异常的场景如下:


PF_WRITE 首次写操作触发异常场景

当进程采用 LazyAlloc 方式分配内存,那么调用 mmap() 函数分配内存时,系统只为进程分配虚拟内存,而只有当 CPU 访问这块虚拟内存时触发缺页异常,缺页异常中断为其分配物理内存并填充相应的页表,最终缺页异常处理返回后再次执行指令,CPU 才能正确访问内存。在这个场景里,缺页的主要原因是页表的 PRESENT 标志位为空,其次因为 CPU 正在执行写操作,因此缺页异常原因归结为 PF_PRESENT 和 PF_WRITE。那么接下来通过实践案例了解该场景下的缺页过程,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault ERROR CODE: PF_WRITE --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-WRITE-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ECODE-WRITE-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存,然后在 32 行对虚拟内存执行写操作,然后在 34 行又对虚拟内存执行读操作,最后再释放内存. 以上便是一个最基础的缺页实践案例,可以知道 32 行的写操作将触发一次缺页异常,为了更好的演示结果, 在 32 行写操作前后开启 BS_DEBUG 功能.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在缺页流程必经之路上打印 ERROR CODE 的信息,位于 ‘arch/x86/mm/fault.c’ 文件的 exc_page_fault 函数是缺页异常的入口,那么在该函数 1506 行添加 BS_DEBUG 信息,此时直接打印 ERROR CODE 信息,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-WRITE-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到应用程序运行,并打印了 0x6000000000 处的值是 ‘B’, 此时可以看到 BS_DEBUG 打印了 [BiscuitOS-stub] ERROR CODE 0x6, 可以知道此时 BIT1 置位,那么 ERROR CODE 包含 PF_WRITE. 以上便是向未分配物理内存写操作引起的缺页异常的场景.


PF_WRITE 对写保护的内存执行写操作场景

当进程对虚拟内存进行写操作时,MMU 发现虚拟内存对应的页表的 W/R 标志位没有置位,那么 MMU 就会触发缺页异常,这是异常原因归结为 PF_WRITE. 发生这种异常的场景一般是系统动态修改了页表的权限,让虚拟内存变成写保护(WP: Write-Protect), 那么虚拟内存变成了只读,所以对其进行写操作就会触发异常,这里的异常有两个层面,一个是缺页异常,另外一个则是 SegmentFault 软件层面的异常,最终导致程序崩溃退出. 接下来通过实践案例了解这种场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault ERROR CODE: PF_WRITE with WP --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-WRITE-WP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ECODE-WRITE-WP-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存,然后在 32 行对虚拟内存执行写操作,然后在 34 行又对虚拟内存执行读操作. 接着程序在 37 行调用 mprotect 函数将虚拟区域的权限修改为 PROT_READ,即虚拟区域为只读,然后在 40 行对虚拟区域执行写操作,最后在释放内存. 以上便是一个最基础的缺页实践案例,可以知道 32 行的写操作将触发一次缺页异常,当这次缺页不会引起程序的崩溃,然而 40 行的写操作会再次引起缺页异常,同时也会引起程序的崩溃, 为了更好的演示实践成果,在 40 行写操作前后开启 BS_DEBUG 功能.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在缺页流程必经之路上打印 ERROR CODE 的信息,位于 ‘arch/x86/mm/fault.c’ 文件的 exc_page_fault 函数是缺页异常的入口,那么在该函数 1506 行添加 BS_DEBUG 信息,此时直接打印 ERROR CODE 信息,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-WRITE-WP-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到应用程序运行,并打印了 0x6000000000 处的值是 ‘B’, 此时可以看到 BS_DEBUG 打印了 [BiscuitOS-stub] ERROR CODE 0x7, 可以知道此时 BIT1 置位,那么 ERROR CODE 包含 PF_WRITE. 以上便是对写保护的内存执行写操作场景.

图片无法显示,请右键点击新窗口打开图片


ERROR CODE: PF_USER

图片无法显示,请右键点击新窗口打开图片

在 Linux 里,程序执行的模式分作两种,一种是用户进程使用的模式,称为 User-mode; 另外一种则是内核线程使用的模式,称为 Super-mode. 当 MMU 检测到页面故障时, 会将发生缺页的模式信息作为其中一种故障源,如果发生故障时程序是 User-mode,那么故障原因就会包含 PF_USER; 反之发生故障时程序是 Super-mode,那么故障原因是 PF_SUPER。缺页异常将 PF_USER 和 PF_SUPER 统一到 ERROR CODE 的 Bit2 里. 对于 PF_USER 发生的场景包括下面两种:


PF_USER 用户进程访问 Userspace 虚拟地址场景

当用户进程访问页面时触发了缺页异常,那么缺页异常的原因会包含 PF_USER, 以此表示是有用户进程触发的,这里不能理解为访问用户空间虚拟地址触发。那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault ERROR CODE: PF_USER --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-USER-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ECODE-USER-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,其在 21 行调用 mmap() 函数分配一段虚拟内存,然后在 32 行对虚拟内存执行写操作,然后在 34 行又对虚拟内存执行读操作,最后再释放内存. 以上便是一个最基础的缺页实践案例,可以知道 32 行的写操作将触发一次缺页异常,为了更好的演示结果, 在 32 行写操作前后开启 BS_DEBUG 功能.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在缺页流程必经之路上打印 ERROR CODE 的信息,位于 ‘arch/x86/mm/fault.c’ 文件的 exc_page_fault 函数是缺页异常的入口,那么在该函数 1506 行添加 BS_DEBUG 信息,此时直接打印 ERROR CODE 信息,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-USER-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到应用程序运行,并打印了 0x6000000000 处的值是 ‘B’, 此时可以看到 BS_DEBUG 打印了 [BiscuitOS-stub] ERROR CODE 0x6, 可以知道此时 BIT2 置位,那么 ERROR CODE 包含 PF_USER. 以上便用户进程访问 Userspace 虚拟地址场景.


PF_USER 用户进程访问 Kernel Space 虚拟地址场景

当用户进程访问页面时触发了缺页异常,那么缺页异常的原因会包含 PF_USER, 以此表示是有用户进程触发的,这里不能理解为访问用户空间虚拟地址触发。为了证明这个猜想,接下来通过一个实践案例在用户空间访问内核空间虚拟地址,以此证明 PF_USER 的真正含义,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault ERROR CODE: PF_USER Access KVADDR --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-USER-KVADDR-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ECODE-USER-KVADDR-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由两部分组成,其中一部分是一个内核模块,该模块有 MSIC 驱动构成,向用户空间提供 “/dev/BiscuitOS” 文件,并为文件实现了 ioctl 接口,当用户空间调用 ioctl 函数时最终会调用到 BiscuitOS_ioctl(). BiscuitOS_ioctl() 函数只接受 BISCUTIOS_FAULT 请求,处理该请求时,函数在 25 行调用 vmalloc() 函数分配一段内核空间虚拟内存,然后在 31 和 33 行对该虚拟内存进行读写操作,最后将该虚拟内存的起始地址返回给用户空间.

图片无法显示,请右键点击新窗口打开图片

实践案例另外一部分由一个应用程序构成,程序首先在 26 行打开 “/dev/BiscuitOS” 文件,然后在 33 行调用 ioctl 函数向文件发送 BISCUTIOS_FAULT 请求,并返回一个内核空间虚拟地址,接着程序在 40 行对虚拟地址进行写操作,并且在 42 行对虚拟地址进行读操作. 以上便是一个最基础的缺页实践案例,可以知道 40 行的写操作将触发一次缺页异常,为了更好的演示结果, 在 32 行写操作前后开启 BS_DEBUG 功能.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在缺页流程必经之路上打印 ERROR CODE 的信息,位于 ‘arch/x86/mm/fault.c’ 文件的 exc_page_fault 函数是缺页异常的入口,那么在该函数 1506 行添加 BS_DEBUG 信息,此时直接打印 ERROR CODE 信息,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-USER-KVADDR-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到内核模块向 0xffffa4f5c0075000 写入字符 B,并成功读取到只读,接着可以看到应用程序运行时 BS_DEBUG 打印了 [BiscuitOS-stub] ERROR CODE 0x6, 可以知道此时 BIT2 置位,那么 ERROR CODE 包含 PF_USER. 以上便用户进程访问 Kernel 虚拟地址场景. 通过两个实践案例可以知道 PF_USER 代表的是发生缺页时的是应用程序,而不是代表用户空间虚拟地址.

图片无法显示,请右键点击新窗口打开图片


ERROR CODE: PF_SUPER

图片无法显示,请右键点击新窗口打开图片

在 Linux 里,程序执行的模式分作两种,一种是用户进程使用的模式,称为 User-mode; 另外一种则是内核线程使用的模式,称为 Super-mode. 当 MMU 检测到页面故障时, 会将发生缺页的模式信息作为其中一种故障源,如果发生故障时程序是 Super-mode,那么故障原因就会包含 PF_SUPER; 反之发生故障时程序是 User-mode,那么故障原因是 PF_USER。缺页异常将 PF_USER 和 PF_SUPER 统一到 ERROR CODE 的 Bit2 里. 对于 PF_SUPER 发生的场景包括下面两种:


PF_SUPER 内核线程访问 Kernel Space 虚拟地址场景

当内核线程访问页面时触发了缺页异常,那么缺页异常的原因会包含 PF_SUPER, 以此表示是有内核线程触发的,这里不能理解为访问内核空间虚拟地址触发。那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault ERROR CODE: PF_SUPER --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-SUPER-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ECODE-SUPER-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个内核模块构成,其在 17 行调用 get_vm_area() 函数从 VMALLOC 管理的区域分配一段内核空间虚拟内存,然后在 28 行对虚拟内存进行写操作,接着在 31 行调用 remove_vm_area() 函数释放该虚拟内存。以上便是一个最基础的缺页实践案例,可以知道 28 行的写操作将触发一次缺页异常,为了更好的演示结果, 在 28 行写操作前后开启 BS_DEBUG 功能.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在缺页流程必经之路上打印 ERROR CODE 的信息,位于 ‘arch/x86/mm/fault.c’ 文件的 exc_page_fault 函数是缺页异常的入口,那么在该函数 1506 行添加 BS_DEBUG 信息,此时直接打印 ERROR CODE 信息,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-SUPER-default/
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动阶段,由于实践案例就被调用,因此启动阶段直接 PANIC,内核打印的消息可以看到 BS_DEBUG 打印了 ERROR CODE 0x2, 可以知道此时 BIT2 清零,那么 ERROR CODE 包含了 PF_SUPER. 以上便是内核线程访问 Kernel Space 虚拟地址场景. 实践完毕之后还原 BiscuitOS 环境,请执行如下命令:

# 还原 BiscuitOS 环境
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make distclean
make download
make build

PF_SUPER 内核线程访问 Userspace 虚拟地址场景

当内核线程访问页面时触发了缺页异常,那么缺页异常的原因会包含 PF_SUPER, 以此表示是有内核线程触发的,这里不能理解为访问内核空间虚拟地址触发。为了证明这个猜想,接下来通过一个实践案例在内核线程访问用户空间虚拟地址,以此证明 PF_SUPER 的真正含义,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault ERROR CODE: PF_SUPER Access UVADDR --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-SUPER-UVADDR-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ECODE-SUPER-UVADDR-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由两部分组成,其中一部分是一个内核模块,该模块有 MSIC 驱动构成,向用户空间提供 “/dev/BiscuitOS” 文件,并为文件实现了 ioctl 接口,当用户空间调用 ioctl 函数时最终会调用到 BiscuitOS_ioctl(). BiscuitOS_ioctl() 函数只接受 BISCUTIOS_FAULT 请求,处理该请求时,函数读取 arg 参数,将其作为用户空间虚拟地址存储在 base 变量里,然后在 28 行对用户空间虚拟地址执行写操作. 可以知道 28 行的写会触发一次缺页异常,为了更好的演示结果,在 28 行写操作前后开启 BS_DEBUG 功能.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

实践案例另外一部分由一个应用程序构成,程序首先在 28 行打开 “/dev/BiscuitOS” 文件,然后在 34 行调用 mmap 函数分配一段虚拟内存,这段虚拟内存仅仅是虚拟内存,接着在 44 行调用 ioctl 函数向文件发送 BISCUTIOS_FAULT 请求,并将 base 对应的虚拟地址值传入该函数。当 ioctl 成功执行之后,在 51 行对虚拟内存执行读操作。

图片无法显示,请右键点击新窗口打开图片

接着在缺页流程必经之路上打印 ERROR CODE 的信息,位于 ‘arch/x86/mm/fault.c’ 文件的 exc_page_fault 函数是缺页异常的入口,那么在该函数 1506 行添加 BS_DEBUG 信息,此时直接打印 ERROR CODE 信息,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-SUPER-UVADDR-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到内核模块向 0x6000000000 写入字符 B,并成功读取 B 字符,接着可以看到应用程序运行时 BS_DEBUG 打印了 [BiscuitOS-stub] ERROR CODE 0x2, 可以知道此时 BIT2 清零,那么 ERROR CODE 包含 PF_SUPER. 以上便内核线程访问用户空间虚拟地址场景. 通过两个实践案例可以知道 PF_SUPER 代表的是发生缺页时的是内核线程,而不是代表内核空间虚拟地址.

图片无法显示,请右键点击新窗口打开图片


ERROR CODE: PF_INSTR

图片无法显示,请右键点击新窗口打开图片

当 CPU 执行指令时,MMU 发现指令不在内存,那么 MMU 会触发缺页异常,此时异常的原因归咎为 PF_INSTR,发生指令缺页之后,缺页异常处理函数会将指令加载到内存,并建立相应的页表,待到缺页异常返回之后再次执行指令。那么接下来通过一个实践案例了解指令缺页异常处理,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault ERROR CODE: PF_INSTR --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-INSTR-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ECODE-INSTR-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,BIN.bin 文件是包含代码的文件,因此程序在 22 行调用 open 函数打开文件,并在 28 行调用 mmap() 函数分配一段虚拟内存映射到文件,然后在 38 行将 func 函数指针指向虚拟内存的起始地址,然后在 42 行执行 func 函数,最后将虚拟内存释放和关闭文件。以上便是一个最基础的缺页实践案例,可以知道 42 行的执行函数操作将触发一次缺页异常,为了更好的演示结果, 在 42 行读操作前后开启 BS_DEBUG 功能.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在缺页流程必经之路上打印 ERROR CODE 的信息,位于 ‘arch/x86/mm/fault.c’ 文件的 exc_page_fault 函数是缺页异常的入口,那么在该函数 1506 行添加 BS_DEBUG 信息,此时直接打印 ERROR CODE 信息,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-INSTR-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到应用程序运行,映射代码的虚拟地址是 0x7f3886050000,当 func 函数执行时,MMU 发现 func 对应的代码还没有加载到内存,那么触发缺页,此时可以看到 BS_DEBUG 打印了 [BiscuitOS-stub] ERROR CODE 0x14, 可以知道此时 BIT4 置位,那么 ERROR CODE 包含 PF_INSTR. 以上便是包含 PF_INSTR 缺页异常的场景.

图片无法显示,请右键点击新窗口打开图片


ERROR CODE: PF_PK

图片无法显示,请右键点击新窗口打开图片

在 Linux 里,如果要动态修改一段虚拟内存的权限,例如将一段可读可写的虚拟区域动态修改为只读区域,那么需要修改相应的页表和更新 TLB,这样的操作开销有点大,于是提出了 Protection Key 的方案,其特点就是可以动态修改虚拟区域的属性,但无需修改页表和更新 TLB,这样的操作很轻,相对于页级权限保护的操作是很不错的选择。当因为 Protection Key 修改虚拟区域权限之后,导致 CPU 访问虚拟内存权限不够,那么 Protection Key 机制会触发页面故障,页面故障的原因归结成 PF_PK,根据该原因对程序做出相应的处理。那么接下来通过一个实践案例了解 Protection Key 触发缺页的过程,实践案例在 BiscuitOS 上的部署逻辑如下(内核使能 CONFIG_ARCH_HAS_PKEYS 宏):

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Support Host CPU Feature Passthrough  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault ERROR CODE: PF_PK --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-PK-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ECODE-PK-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,程序首先在 45 行调用 mmap 函数分配一段虚拟内存,然后在 56 行对虚拟内存进行写操作,并接着对虚拟内存进行读操作,然后 sleep 1s. 接着程序在 63 行调用 pkey_alloc 系统调用分配 PKEY,然后在 71 行将 PKEY 设置为禁止访问,然后在 80 行调用 mprotect_key 更新 PEKY,更新完毕之后程序在 89 行对虚拟内存进行读操作,最后在 92 行调用 pkey_free 系统调用释放 PKEY。以上便是一个最基础的实践案例,可以知道 89 行的执行函数操作将触发一次缺页异常,为了更好的演示结果, 在 89 行读操作前后开启 BS_DEBUG 功能.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在缺页流程必经之路上打印 ERROR CODE 的信息,位于 ‘arch/x86/mm/fault.c’ 文件的 exc_page_fault 函数是缺页异常的入口,那么在该函数 1506 行添加 BS_DEBUG 信息,此时直接打印 ERROR CODE 信息,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-PK-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到应用程序运行,程序一开始可以从 0x6000000000 处写入数据和读取数据,但经过 Protection Key 设置之后,程序对虚拟内存已经没有读写权限了,这时对虚拟内存进行读写将触发缺页异常,可以看到 BS_DEBUG 打印了 [BiscuitOS-stub] ERROR CODE 0x25,可以知道此时 BIT5 置位,那么 ERROR CODE 包含了 PF_PK。另外 Protection Key 会让程序 Segment Fault. 以上便是包含 PF_PK 的异常缺页场景.

图片无法显示,请右键点击新窗口打开图片


ERROR CODE: PF_PROT

图片无法显示,请右键点击新窗口打开图片

在 Linux 里,程序的地址空间被分配之后形成零散的区域,这些区域都是权限相同的虚拟内存,例如都是可读或者只读或者可读可行,CPU 只要不未被虚拟区域的权限,那么就可以正常访问虚拟内存。一旦 CPU 访问虚拟内存时违背了虚拟区域的权限,那么 MMU 检查到违法权限的访问,于是触发缺页异常,此时缺页异常的原因归咎为 PF_PROT,及未被了页级别的权限保护。缺页异常在处理这类异常时通常是让应用程序 SegmentFault. 在 Linux 里发生违反权限的场景有如下:


PF_PROT: mprotect 动态修改权限导致非法访问场景

当进程通过 mmap 分配虚拟内存时可以设置虚拟内存的权限,例如 PROT_READ、PROT_WRITE、PROT_EXEC 和 PROT_NONE, 这些权限可以限制进程对虚拟内存的访问,如果进程对虚拟内核没有权限的访问,MMU 检查到权限不足时,MMU 就会触发缺页异常,此时缺页原因归咎为 PF_PROT, 然后在结合其他异常原因,可以知道进程之前执行了什么操作导致的缺页异常。那么接下来通过一个实践案例了解这种场景的缺页异常,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault ERROR CODE: PF_PROT --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-PROT-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ECODE-PROT-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,函数首先在 21 行调用 mmap() 函数分配一段虚拟内存映射,然后在 31 行对虚拟内存执行写操作,并随后对虚拟内存执行读操作,接着函数在 35 行调用 mprotect 函数动态修改虚拟内存区域的权限为只读,那么程序继续在 37 行执行写操作,那么会触发缺页异常,最后程序在 39 行释放内存。以上便是一个最基础的缺页实践案例,可以知道 37 行的执行函数操作将触发一次缺页异常,为了更好的演示结果, 在 37 行读操作前后开启 BS_DEBUG 功能.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在缺页流程必经之路上打印 ERROR CODE 的信息,位于 ‘arch/x86/mm/fault.c’ 文件的 exc_page_fault 函数是缺页异常的入口,那么在该函数 1506 行添加 BS_DEBUG 信息,此时直接打印 ERROR CODE 信息,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-PROT-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到应用程序运行,程序一开始可以从 0x6000000000 处写入数据和读取数据,但经过 mprotect 设置之后,程序对虚拟内存已经没有读权限了,这时对虚拟内存进行写将触发缺页异常,可以看到 BS_DEBUG 打印了 [BiscuitOS-stub] ERROR CODE 0x7,可以知道此时 BIT0 置位,那么 ERROR CODE 包含了 PF_PROT。另外缺页异常会让程序 Segment Fault. 以上便是 mprotect 动态修改权限导致非法访问场景.


PF_PROT: Protection Key 动态修改权限导致非法访问场景

在 Linux 里,如果要动态修改一段虚拟内存的权限,例如将一段可读可写的虚拟区域动态修改为只读区域,那么需要修改相应的页表和更新 TLB,这样的操作开销有点大,于是提出了 Protection Key 的方案,其特点就是可以动态修改虚拟区域的属性,但无需修改页表和更新 TLB,这样的操作很轻,相对于页级权限保护的操作是很不错的选择。当因为 Protection Key 修改虚拟区域权限之后,导致 CPU 访问虚拟内存权限不够,那么 Protection Key 机制会触发页面故障,页面故障的原因归结成 PF_PROT,根据该原因对程序做出相应的处理。那么接下来通过一个实践案例了解 Protection Key 触发缺页的过程,实践案例在 BiscuitOS 上的部署逻辑如下(内核使能 CONFIG_ARCH_HAS_PKEYS 宏):

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Support Host CPU Feature Passthrough  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault ERROR CODE: PF_PROT with Protection Key --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-PROT-PK-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ECODE-PROT-PK-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,程序首先在 45 行调用 mmap 函数分配一段虚拟内存,然后在 56 行对虚拟内存进行写操作,并接着对虚拟内存进行读操作,然后 sleep 1s. 接着程序在 63 行调用 pkey_alloc 系统调用分配 PKEY,然后在 71 行将 PKEY 设置为禁止访问,然后在 80 行调用 mprotect_key 更新 PEKY,更新完毕之后程序在 89 行对虚拟内存进行读操作,最后在 92 行调用 pkey_free 系统调用释放 PKEY。以上便是一个最基础的实践案例,可以知道 89 行的执行函数操作将触发一次缺页异常,为了更好的演示结果, 在 89 行读操作前后开启 BS_DEBUG 功能.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在缺页流程必经之路上打印 ERROR CODE 的信息,位于 ‘arch/x86/mm/fault.c’ 文件的 exc_page_fault 函数是缺页异常的入口,那么在该函数 1506 行添加 BS_DEBUG 信息,此时直接打印 ERROR CODE 信息,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-PROT-PK-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到应用程序运行,程序一开始可以从 0x6000000000 处写入数据和读取数据,但经过 Protection Key 设置之后,程序对虚拟内存已经没有读写权限了,这时对虚拟内存进行读写将触发缺页异常,可以看到 BS_DEBUG 打印了 [BiscuitOS-stub] ERROR CODE 0x25,可以知道此时 BIT0 置位,那么 ERROR CODE 包含了 PF_PROT。另外 Protection Key 会让程序 Segment Fault. 以上便是 Protection Key 动态修改权限导致非法访问场景.

图片无法显示,请右键点击新窗口打开图片


ERROR CODE: PF_RSVD

图片无法显示,请右键点击新窗口打开图片

上图是 Linux PTE 页表的 BITMAP,其中有部分区域标注为 Reserved,即这些 Bit 位被硬件保留,不能对这些 Bit 位写入值,否则 CPU 访问虚拟内存时,MMU 发现页表的 Reserved 区域被写入值,那么 MMU 就会触发缺页异常,此时缺页原因归咎为 PF_RSVD. 该机制可以有效的保护页表的非法写入,那么接下来通过一个实践案例了解这种异常场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault ERROR CODE: PF_RSVD --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-RSVD-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ECODE-RSVD-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由两部分组成,其中一部分如上图是一个内核模块,其由一个 MSIC 驱动框架构成,并向用户空间提供了 “/dev/BiscuitOS-PageTable” 文件,文件实现了 mmap 接口,也就是用户空间打开该文件,然后调用 mmap 映射文件时,就会调用到 BiscuitOS_mmap() 函数。BiscuitOS_mmap 函数的作用是分配一个物理页,然后调用 PageWalk 机制遍历页表,找到对应的 PMD 页表之后,并调用 BiscuitOS_pmd_entry() 函数. BiscuitOS_pmd_entry 函数是整个模块的核心逻辑,其找到虚拟地址对应的 PTE Entry,然后在 37 行调用 set_pte_at() 函数填充页表,此时在 39 行将 _PAGE_RSVD 位置位,该标志位位于页表的 Reserved 区域.

图片无法显示,请右键点击新窗口打开图片

实践案例的另外一部分是由一个应用程序构成的,程序首先在 23 行调用 open 函数打开 “/dev/BiscuitOS-PageTable” 文件,然后在 29 行调用 mmap 函数将文件映射到进程的地址空间,接着在 41 行对虚拟内存执行写操作,并在 42 行对虚拟内存执行读操作,最后释放内存和关闭文件。以上便是一个最基础的实践案例,可以知道 41 行写操作会触发缺页异常,为了更好的演示实践结果,在 41 行左右添加 BS_DEBUG 调试开关.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在缺页流程必经之路上打印 ERROR CODE 的信息,位于 ‘arch/x86/mm/fault.c’ 文件的 exc_page_fault 函数是缺页异常的入口,那么在该函数 1506 行添加 BS_DEBUG 信息,此时直接打印 ERROR CODE 信息,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ECODE-RSVD-default/
make
make install
make pack
# 编译内核
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-kernel-default/
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到内核模块构建好了页表,只是当进程对虚拟内存进行写操作时,MMU 发现页表的 Reserved 区域被写入值,于是 MMU 直接触发缺页异常,可以看到 BS_DEBUG 打印了 [BiscuitOS-stub] ERROR CODE 0xff, 可以知道此时 BIT3 置位,那么 ERROR CODE 包含 PF_RSVD. 以上便是包含 PF_RSVD 缺页异常的场景.

图片无法显示,请右键点击新窗口打开图片