第四章 探索Windows 2000的内存管理机制
翻译:Kendiv( 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()函数使用该句柄来查询Spy设备的版本信息,如果IOCTL调用成功,该信息将会在屏幕上显示出来。接下来,CommandParser()函数将被调用两次,第一次调用只是简单的检查命令行中是否有无效的参数,并显示任何可能的错误。第二次调用则执行所有的命令。我不想讨论该函数的细节。列表4-29中的剩余代码是为了进行清理工作,如关闭句柄和卸载Spy驱动(该功能是可选的)。w2k_mem.exe的源代码中还有一些有趣的代码片断,但我不在这里讨论它们了。请参考本书光盘的\src\w2k_mem目录下的w2k_mem.c和w2k_mem.h。
现在唯一需要注意的就是gfSpyUnload标志,该标志决定是否卸载Spy驱动。我已经将这个全局标志设为了FALSE,因此不会自动卸载该驱动。这提高w2k_mem.exe或w2k_spy.sys的任何客户端的性能,因为加载一个驱动需要花费一定的时间。只有第一个客户端会产生加载开销。这种设置还可避免多个客户端间的竞争,如,一个客户试图卸载该驱动而此时另一个还在使用这个驱动。当然,Windows 2000不会卸载一个驱动,除非该驱动的所有句柄都被关闭了,但系统会将驱动置于STOP_PENDING状态,这样新的客户端将无法访问此设备。不过,如果你不在一个多客户端的环境下运行w2k_spy.sys,而且你需要经常更新设备的驱动程序,你就应该将gfSpyUnload标志设为TRUE。
深入Windows 2000内存
引入用户模式和内核模式的独立4GB地址空间被再次划分为多个更小的块。正如你可能猜到的,它们中的大多数都包含未文档化的结构,而且服务于未文档化的地目的。其中某些东西对于任何开发系统诊断或调试软件的人来说都是真正的金矿。
基本的操作系统信息
如果你注意过示列4-1下半部分的帮助信息,你会发该节的标题是:“系统状态选项”。现在试试名为“显示操作系统信息”的选项:+o。示列4-12给出了在我的机器上使用该选项的输出结果。这里显示的信息都是SPY_OS_INFO结构的内容,该结构定义与列表4-13,由Spy设备函数SpyOutputOsInfo()实际创建该结构,此函数也包含在列表4-13中。在示列4-12中,你可以看到位于4GB地址空间中的进程的一些典型地址。例如,有效的用户地址范围是:0x00010000 ---- 0x7FFFFFFF。你可能阅读过其他有关Windows NT或2000的程序设计书籍,用户模式的第一个和最后一个64KB线性内存区域是“不能访问区域”,访问这一区域将引发一个错误(参见第五章,Solomon 1998),W2k_mem.exe输出证明了这一点。
示列4-12. 显示操作系统信息
示列4-12中的最后三行包含的信息非常有趣,它们都是有关系统的。这些信息大部分都取自位于地址0xFFDF0000处的SharedUserData区域中。系统在该处维护一个名为KUSER_SHARED_DATA的结构,该结构定义于DDK头文件ntddk.h。
……………………待续………………….