第四章 探索 Windows 2000 的内存管理机制
翻译: Kendiv( [url=http://www.pccode.net].net"fcczj@263.net )
更新: Tuesday, February 22, 2005
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
请求式分页动作
在讨论 Spy 设备的 SPY_IO_MEMORY_DATA 函数时,我提到过该函数可以读取已被置换到页面文件中的内存页。要证明这一点,首先,必须让系统处于低内存状态,以强迫它将不马上使用的数据置换到页面文件中。我喜欢采用的方法如下:
1. 使用 PrintKey ,将 Windows 2000 的桌面复制到剪切板中。
2. 将该图片粘贴到一个图形处理程序中。
3. 将该图片的尺寸放到最大。
现在,执行命令: w2k_mem +d #16 0xC02800000 0xA0000000 0xA0001000 0xA0002000 0xC0280000 ,察看它在屏幕上的输出。你可能会惊讶。在触及某些 PTE 所引用的页之前,它会获取这些 PTE 的快照。在地址 0xC0280000 处发现的四个 PTE 与地址范围: 0xA0000000---0xA0003FFF 相关,这是内核模块 win32k.sys 的一部分。如 示列 4-11 所示,该地址范围已经被置换出去了,因为在地址 0xC0280000 的四个 DWORD 都是偶数,这意味着它们的最低位(即 PTE 的 P 位)为零,这表示没有存在于物理内存中的页。接下来的三块 16 进制 Dump 信息属于 0xA0000000 、 0xA0001000 、 0xA0002000 , w2k_mem 可以毫无问题的访问这些页(系统会根据请求将它们再次换入内存)。
示列 4-11 观察 PTE 的状态变化
在开始下一节之前,请再次研究一下 示列 4-11 中的第一栏。位于地址 0xC0280000 的四个 PTE 看上去都很像。但事实上,它们仅有最低的三个位不同。如果你检查所有位于页面文件中的 PNPE ,你会发现它们的第 10 位都为 1 。这就是为什么我在 列表 4-3 中,将该位的名字取为 PageFile 。如果该位为 1 ,除 P 位外的所有位都将用来表示该页在页面文件中的位置。
更多的命令选项
示列 4-1 给出的某些命令选项还没有解释过。例如,系统状态选项: +o 、 +c 、 +g 、 +i 和 +b ,我会在本章的最后一节介绍它们,在那儿我们将发现几个 Windows 2000 内存系统的秘密。
Spy 设备的接口
现在你已经知道如何使用 w2k_mem 了,该是介绍它是如何工作的了。现在来看看这个程序是如何与 w2k_spy.sys 中的 Spy 设备通讯的。
回顾 ----- 设备 I/O 控制( Device I/O Control )
IOCTL 通讯的内核模式端已经由 列表 4-6 和 列表 4-7 给出了。 Spy 设备只是简单的等待 IRP 并处理其中的某些 IRP ,尤其是标识为 IPR_MJ_DEVICE_CONTROL ,其中的一些请求在用户模式下是被禁止的。调用 Win32 API 函数 DeviceIoControl() , 列表 4-27 给出了该函数的原型。可能你已经熟悉了 dwIocontrolCode 、 lpInBuffer 、 nInBufferSize 、 lpOutBuffer 、 nOutBufferSize 和 lpBytesReturned 参数。事实上,它们一一对应于: SpyDispatcher() 的 dCode 、 pInput 、 dInput 、 pOutput 、 dOutput 和 pdInfo 参数, SpyDispatcher 定义于 列表 4-7 。剩下的参数很快就会解释。 hDevice 是 Spy 设备的句柄, lpOverlapped (可选的)指向一个 OVERLAPPED 结构,异步 IOCTL 需要该结构。我们不需要发送异步请求,所以该参数总是 NULL 。
列表 4-28 列出了所有执行基本 IOCTL 操作的外包函数。最基本的一个是: IoControl() ,该函数调用 DeviceControl() 并测试返回的输出数据的大小。因为 w2k_mem.exe 精确的提供了输出缓冲区的大小,所以,输出的字节数应该总是等于缓冲区的大小。 ReadBinary() 是 IoControl() 的简单版本,它不需要输入数据。 ReadCpuInfo() 、 ReadSegment() 和 ReadPhysical() 专用于 Spy 函数 SPY_IO_CPU_INFO 、 SPY_IO_SEGEMNT 和 SPY_IO_PHYSICAL ,因为它们会经常被用到。将它们封装为 C 函数,可读性会更好些。
BOOL WINAPI DeviceIoControl( HANDLE hDevice,
DWORD dwIoControlCode,
PVOID lpInBuffer,
DWORD nInBufferSize,
PVOID lpOutBuffer,
DWORD nOutBufferSize,
PDWORD lpBytesReturned,
POVERLAPPED lpOverlapped);
列表 4-27. DeviceIoControl 函数的原型
BOOL WINAPI IoControl (HANDLE hDevice,
DWORD dCode,
PVOID pInput,
DWORD dInput,
PVOID pOutput,
DWORD dOutput)
{
DWORD dData = 0;
return DeviceIoControl (hDevice, dCode,
pInput, dInput,
pOutput, dOutput,
&dData, NULL)
&&
(dData == dOutput);
}
// -----------------------------------------------------------------
BOOL WINAPI ReadBinary (HANDLE hDevice,
DWORD dCode,
PVOID pOutput,
DWORD dOutput)
{
return IoControl (hDevice, dCode, NULL, 0, pOutput, dOutput);
}
// -----------------------------------------------------------------
BOOL WINAPI ReadCpuInfo (HANDLE hDevice,
PSPY_CPU_INFO psci)
{
return IoControl (hDevice, SPY_IO_CPU_INFO,
NULL, 0,
psci, SPY_CPU_INFO_);
}
// -----------------------------------------------------------------
BOOL WINAPI ReadSegment (HANDLE hDevice,
DWORD dSelector,
PSPY_SEGMENT pss)
{
return IoControl (hDevice, SPY_IO_SEGMENT,
&dSelector, DWORD_,
pss, SPY_SEGMENT_);
}
// -----------------------------------------------------------------
BOOL WINAPI ReadPhysical (HANDLE hDevice,
PVOID pLinear,
PPHYSICAL_ADDRESS ppa)
{
return IoControl (hDevice, SPY_IO_PHYSICAL,
&pLinear, PVOID_,
ppa, PHYSICAL_ADDRESS_)
&&
(ppa->LowPart || ppa->HighPart);
}
列表 4-28 几个 IOCTL 的外包函数
到目前为止,本节列出的所有函数都需要 Spy 设备的一个句柄。现在,我将介绍如何获取该句柄。这实际上是一个非常简单的 Win32 操作,和打开文件类似。 列表 4-29 展示了 w2k_mem.exe 的命令处理例程的实现细节。该代码使用 API 函数 w2kFilePath() 、 w2kServiceLoad() 和 w2kServiceUnload() ,这几个函数由 w2k_lib.dll 导出。如果你已经读过第三章中关于 Windows 2000 服务控制管理器的介绍,你应该通过 列表 3-8 已了解了 w2kServiceLoad() 和 w2kServiceUnload() 。这些强大的函数可随时加载或卸载内核模式的设备驱动,并且能处理一些良性的错误,如,妥善的处理加载一个已经载入内存的驱动程序。 w2kFilePath() 是一个帮助函数。 w2k_mem.exe 调用它来获取 Spy 驱动程序的完整路径。
WORD awSpyFile [] = SW(DRV_FILENAME);
WORD awSpyDevice [] = SW(DRV_MODULE);
WORD awSpyDisplay [] = SW(DRV_NAME);
WORD awSpyPath [] = SW(DRV_PATH);
// -----------------------------------------------------------------
void WINAPI Execute (PPWORD ppwArguments,
DWORD dArguments)
{
SPY_VERSION_INFO svi;
DWORD dOptions, dRequest, dReceive;
WORD awPath [MAX_PATH] = L"?";
SC_HANDLE hControl = NULL;
HANDLE hDevice = INVALID_HANDLE_VALUE;
_printf (L"\r\nLoading \"%s\" (%s) ...\r\n",
awSpyDisplay, awSpyDevice);
if (w2kFilePath (NULL, awSpyFile, awPath, MAX_PATH))
{
_printf (L"Driver: \"%s\"\r\n",
awPath);
hControl = w2kServiceLoad (awSpyDevice, awSpyDisplay,
awPath, TRUE);
}
if (hControl != NULL)
{
_printf (L"Opening \"%s\" ...\r\n",
awSpyPath);
hDevice = CreateFile (awSpyPath, GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
}
else
{
_printf (L"Unable to load the spy device driver.\r\n");
}
if (hDevice != INVALID_HANDLE_VALUE)
{
if (ReadBinary (hDevice, SPY_IO_VERSION_INFO,
&svi, SPY_VERSION_INFO_))
{
_printf (L"\r\n%s V%lu.%02lu ready\r\n",
svi.awName,
svi.dVersion / 100, svi.dVersion % 100);
}
dOptions = COMMAND_OPTION_NONE;
dRequest = CommandParse (hDevice, ppwArguments, dArguments,
TRUE, &dOptions);
dOptions = COMMAND_OPTION_NONE;
dReceive = CommandParse (hDevice, ppwArguments, dArguments,
FALSE, &dOptions);
if (dRequest)
{
_printf (awSummary,
dRequest, (dRequest == 1 ? awByte : awBytes),
dReceive, (dReceive == 1 ? awByte : awBytes));
}
_printf (L"\r\nClosing the spy device ...\r\n");
CloseHandle (hDevice);
}
else
{
_printf (L"Unable to open the spy device.\r\n");
}
if ((hControl != NULL) && gfSpyUnload)
{
_printf (L"Unloading the spy device ...\r\n");
w2kServiceUnload (awSpyDevice, hControl);
}
return;
}
列表 4-29. 控制 Spy 设备
请注意 列表 4-29 顶部给出的四个全局字符串的定义。常量 DRV_FILENAME 、 DRV_MODULE 、 DRV_NAME 和 DRV_PATH 来自 Spy 驱动的头文件 w2k_spy.h 。 表 4-4 列出了它们的当前值。你不会在 w2k_mem.exe 的源代码中发现设备相关的定义, w2k_spy.h 提供了客户端程序所需的一切。这非常重要:如果以后改变了任何设备相关的定义,就不需要更新任何程序文件了。只需要以新的头文件编译、链接程序即可。
列表 4-29 顶部调用的 w2kFilePath() 可以保证由全局变量 awSpyFile (见 表 4-4 )指定的 w2k_spy.sys 总是从 w2k_mem.exe 所在目录中加载。接下来, 列表 4-29 中的代码将全局字符串 awSpyDevice 和 awSpyDisplay ()传递给 w2kServiceLoad() ,以加载 Spy 设备的驱动。如果驱动没有被加载,这些字符串将被保存在驱动的属性列表中,可以由其他程序取出;否则,将保留当前的属性设置。尽管 列表 4-29 中的 w2kServiceLoad() 调用可返回一个句柄,但这并不是一个可用于任何 IOCTL 函数的句柄。要获取 Spy 设备的句柄,必须使用 Win32 的多用途函数 CreateFile() 。该函数可打开或创建 Windows 2000 中几乎所有可被打开和创建的东西。如果提供了内核设备的符号链接名,形如 \.<SymbolicLink > 给 CreateFile() 的 lpFileName 参数,那么该函数就可打开这个内核设备。 Spy 设备的符号链接名是: w2k_spy ,因此, CreateFile() 的第一个参数必须是 \.w2k_spy ,这正是 表 4-4 中的 awSpyPath 的值。
表 4-4. 设备相关的字符串定义
w2k_spy 常量
w2k_mem 变量
值
DRV_FILENAME
awSpyFile
w2k_spy.sys
DRV_MODULE
awSpyDevice
w2k_spy
DRV_NAME
awSpyDisplay
SBS Windows 2000 Spy Device
DRV_PATH
awSpyPath
\\.\ w2k_spy
如果 CreateFile() 成功,它将返回一个设备的句柄,该句柄可传递给 DeviceIoControl() 。 列表 4-29 中的 Execute() 函数使