电脑技术学习

《Undocumented Windows 2000 Secrets》翻译 --- 第四章(8)

dn001

第四章 探索 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"rnLoading "%s" (%s) ...rn",

awSpyDisplay, awSpyDevice);

if (w2kFilePath (NULL, awSpyFile, awPath, MAX_PATH))

{

_printf (L"Driver: "%s"rn",

awPath);

hControl = w2kServiceLoad (awSpyDevice, awSpyDisplay,

awPath, TRUE);

}

if (hControl != NULL)

{

_printf (L"Opening "%s" ...rn",

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.rn");

}

if (hDevice != INVALID_HANDLE_VALUE)

{

if (ReadBinary (hDevice, SPY_IO_VERSION_INFO,

&svi, SPY_VERSION_INFO_))

{

_printf (L"rn%s V%lu.%02lu readyrn",

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"rnClosing the spy device ...rn");

CloseHandle (hDevice);

}

else

{

_printf (L"Unable to open the spy device.rn");

}

if ((hControl != NULL) && gfSpyUnload)

{

_printf (L"Unloading the spy device ...rn");

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 的源代码中还有一些有趣的代码片断,但我不在这里讨论它们了。请参考本书光盘的 srcw2k_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 。

标签: