avatar

Catalog
缺页异常

前言

本篇学习缺页异常,缺页异常属于习惯叫法,本身是指与页相关的异常处理机制

什么是缺页异常

在学习PDE、PTE属性时介绍过P位,只有当PDE与PTE的P位均为1时,物理页才有效。当CPU访问一个地址,如果其PTE的P位为0,此时会产生缺页异常。缺页异常发生时,通过中断描述符表的e号中断进行处理。

异常不一定都是不好的,Windows系统运行的每一秒都在发生缺页异常,而恰恰是因为缺页异常的机制,Windows才能更加有效的利用物理页。假设当前操作系统的有效物理内存只有2M,如果一个线程通过VirtualAlloc函数申请了某个线性地址对应的物理页后就一直占据着它不释放,那么内存很快就会被占满,而线程又不是无时无刻执行着,它也可能sleep,进入等待队列中,却依然占据着物理页,这样内存的使用效率是非常低的。

内存交换与虚拟内存

内存交换机制

为了提高物理内存的使用效率,Windows引入了内存交换机制。其核心在于,只有正在被使用的线性地址才会被挂上物理页,如果一个线性地址隔了一段时间没有被使用,或者说当前的物理页快被使用完了,这时操作系统会将这些线性地址对应的物理页上的数据保存到硬盘上,并将线性地址对应PTE的P位设置为0

虚拟内存

那么,被交换到硬盘上的数据会保存到哪里呢?

右键我的电脑 -> 属性 -> 高级 -> 性能 -> 设置 -> 高级 ->虚拟内存更改,就可以找到图中对虚拟内存的设置选项。这个虚拟内存有什么用呢?在C盘根目录下,有一个叫做pagefile.sys的文件(文件夹选项显示所有文件后可见),如下图所示

这个pagefile.sys文件的大小就是刚刚设置的虚拟内存的大小,并且从内存交换到硬盘上的数据也会存在这个文件中,所以这个文件被称作虚拟内存。

内存交换导致的缺页异常

当操作系统将物理页上的数据保存到硬盘上时,也将该线性地址对应的PTE的P位设置成了0。一旦该线性地址再次被访问,由于P位为0,则会触发缺页异常。那么操作系统是如何处理的呢?

当P位为0时,此时的PTE被称作无效PTE,有以下四种情形:

每种情形对应的处理情况也不相同,而此时,因内存交换导致的缺页异常,属于第一种情况(位于页面文件)。此时PTE的1-4位,5-9位,12-31位都有值,说明这个线性地址是有效的,只是数据位于硬盘上。异常处理程序(e号中断)会根据PTE上的描述,从pagefile.sys获取到数据内容,并将其取出挂到一个新的物理页上,将PTE的12-31设置为新的物理页地址,并将P位置1。缺页异常处理完毕,这类缺页异常的情况是非常多见的,无时无刻不在发生,但是作为用户来说是察觉不到异样的,程序始终是正常执行的。

保留与提交的误区

回顾之前介绍的VirtualAlloc函数

c
1
2
3
4
5
6
LPVOID VirtualAlloc{
LPVOID lpAddress, // 要分配的内存区域的地址
DWORD dwSize, // 分配的大小
DWORD flAllocationType, // 类型:MEM_RESERVE MEM_COMMIT
DWORD flProtect // 该内存的初始保护属性
};

重点关注第三个参数flAllocationType,这个参数有两个取值,含义如下:

  • MEM_RESERVE:申请内存时,仅保留线性地址,不分配物理页。
  • MEM_COMMIT:可以有物理页,但不是立即有或者一直有。

之前一直以为,申请内存时将fAllocationType设置为MEM_COMMIT就可以获得物理页了,但事实真是如此吗?下面做个小实验,编写运行下面的代码:

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "stdafx.h"
#include "windows.h"

int main(int argc, char* argv[])
{
LPVOID pAddr = VirtualAlloc(NULL, 4096*8, MEM_COMMIT, PAGE_READWRITE);
printf("未使用物理页时: %x\n", pAddr);

getchar();
*(PDWORD)pAddr = 0x12345678;
printf("写入数据后\n");

getchar();
return 0;
}

第一次运行时,已经申请完内存并打印出对应的线性地址,但是此时还没有向申请的内存中写入数据,在Windbg中查看该线性地址的PTE。

先说明一下,这个!vtop指令,在给出Cr3和线性地址的情况下,会自动帮你计算PDE、PTE的值。可以看到,此时PTE的值是空的,没有指向任何物理页,尽管我们使用的是MEM_COMMIT作为fAllocationType的值,但是此时并没有给该线性地址分配任何物理页。接下来继续执行

当在申请的线性地址处写入数据后,再看这个线性地址对应的PTE时会发现,此时PTE已经有值,并且指向一个物理页。同样,这也是利用了缺页异常的机制。当CPU访问线性地址的时候,发现PTE的值是0,此时触发了无效PTE中的第四种情况(未知原因,需检查VAD),这个时候,操作系统会查看当前进程的Vad树,如果线性地址存在,就会给线性地址挂上一个物理页,并填写PTE的12-31位,1-9位,将P位置1;如果线性地址不存在,就会报0xC0000005错误。

结论:通过实验了解到在申请内存时即使令参数flAllocationType的值为MEM_COMMIT,也不是立刻获得物理页,而是在使用内存时,通过触发缺页异常,从而获得物理页。至于MEM_RESERVE,在Vad树中会显示Commit的值为0。

写拷贝原理

映射内存一篇中介绍过写拷贝,本篇就来分析一下写拷贝的实现原理。

通过之前的学习了解到,PTE属于物理内存的范畴,Vad树属于线性地址的范畴,而写拷贝的实现同时借助了两者的属性。下面来看具体步骤:

  1. 当一个进程试图对受到写拷贝保护的文件进行写操作的时候,操作系统会先检查PTE的R/W属性。
  2. 受到写拷贝保护的文件,其物理页所在PTE的R/W属性被设置为只读(0)。
  3. 当操作系统检测到进程尝试向只读的物理页写入数据时,会触发异常,转入异常处理函数中执行。
  4. 异常处理函数会查找进程的Vad树,发现该文件的内存保护属性为写拷贝。
  5. 此时操作系统会创建一份新的物理页,并将源文件的内容拷贝一份到新的物理页中,让试图修改文件的进程中映射的文件指向这个新的物理页。
  6. 这样进程修改的文件只是一个副本,而不是真正修改源文件。

精简版可参考下图:

过掉写拷贝保护的方式也比较简单,直接修改PTE的属性,令R/W置1。

总结

至此,内存部分就基本结束了,主要介绍了线性地址与物理内存的管理与分配,以及缺页异常这种常见机制的介绍,更多内容可以学习潘爱民老师的《Windows内核原理与实现》这里就不再展开。

参考资料

参考书籍:

  • 《Windows内核原理与实现》p250~265 —— 潘爱民

参考教程:

  • 海哥逆向中级预习班

参考链接:

Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/09/05/%E7%BC%BA%E9%A1%B5%E5%BC%82%E5%B8%B8/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶