《Undocumented Windows 2000 Secrets》翻譯 --- 第四章(8)
第四章 探索 Windows 2000 的內存管理機制
翻譯: Kendiv( fcczj@263.net )
更新: Tuesday, February 22, 2005
聲明:轉載請注明出處,并保證文章的完整性,本人保留譯文的所有權利。
請求式分頁動作
在討論 Spy 設備的 SPY_IO_MEMORY_DATA 函數時,我提到過該函數可以讀取已被置換到頁面文件中的內存頁。要證明這一點,首先,必須讓系統(tǒng)處于低內存狀態(tài),以強迫它將不馬上使用的數據置換到頁面文件中。我喜歡采用的方法如下:
1. 使用 PrintKey ,將 Windows 2000 的桌面復制到剪切板中。
2. 將該圖片粘貼到一個圖形處理程序中。
3. 將該圖片的尺寸放到最大。
現(xiàn)在,執(zhí)行命令: w2k_mem +d #16 0xC02800000 0xA0000000 0xA0001000 0xA0002000 0xC0280000 ,察看它在屏幕上的輸出。你可能會驚訝。在觸及某些 PTE 所引用的頁之前,它會獲取這些 PTE 的快照。在地址 0xC0280000 處發(fā)現(xiàn)的四個 PTE 與地址范圍: 0xA0000000---0xA0003FFF 相關,這是內核模塊 win32k.sys 的一部分。如 示列 4-11 所示,該地址范圍已經被置換出去了,因為在地址 0xC0280000 的四個 DWord 都是偶數,這意味著它們的最低位(即 PTE 的 P 位)為零,這表示沒有存在于物理內存中的頁。接下來的三塊 16 進制 Dump 信息屬于 0xA0000000 、 0xA0001000 、 0xA0002000 , w2k_mem 可以毫無問題的訪問這些頁(系統(tǒng)會根據請求將它們再次換入內存)。
示列 4-11 觀察 PTE 的狀態(tài)變化
在開始下一節(jié)之前,請再次研究一下 示列 4-11 中的第一欄。位于地址 0xC0280000 的四個 PTE 看上去都很像。但事實上,它們僅有最低的三個位不同。如果你檢查所有位于頁面文件中的 PNPE ,你會發(fā)現(xiàn)它們的第 10 位都為 1 。這就是為什么我在 列表 4-3 中,將該位的名字取為 PageFile 。如果該位為 1 ,除 P 位外的所有位都將用來表示該頁在頁面文件中的位置。
更多的命令選項
示列 4-1 給出的某些命令選項還沒有解釋過。例如,系統(tǒng)狀態(tài)選項: +o 、 +c 、 +g 、 +i 和 +b ,我會在本章的最后一節(jié)介紹它們,在那兒我們將發(fā)現(xiàn)幾個 Windows 2000 內存系統(tǒng)的秘密。
Spy 設備的接口
現(xiàn)在你已經知道如何使用 w2k_mem 了,該是介紹它是如何工作的了?,F(xiàn)在來看看這個程序是如何與 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 需要該結構。我們不需要發(fā)送異步請求,所以該參數總是 NULL 。
列表 4-28 列出了所有執(zhí)行基本 IOCTL 操作的外包函數。最基本的一個是: IoControl() ,該函數調用 DeviceControl() 并測試返回的輸出數據的大小。因為 w2k_mem.exe 精確的提供了輸出緩沖區(qū)的大小,所以,輸出的字節(jié)數應該總是等于緩沖區(qū)的大小。 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 的外包函數
到目前為止,本節(jié)列出的所有函數都需要 Spy 設備的一個句柄。現(xiàn)在,我將介紹如何獲取該句柄。這實際上是一個非常簡單的 Win32 操作,和打開文件類似。 列表 4-29 展示了 w2k_mem.exe 的命令處理例程的實現(xiàn)細節(jié)。該代碼使用 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 的源代碼中發(fā)現(xiàn)設備相關的定義, 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() 。該函數可打開或創(chuàng)建 Windows 2000 中幾乎所有可被打開和創(chuàng)建的東西。如果提供了內核設備的符號鏈接名,形如 .<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() 函數將被調用兩次,第一次調用只是簡單的檢查命令行中是否有無效的參數,并顯示任何可能的錯誤。第二次調用則執(zhí)行所有的命令。我不想討論該函數的細節(jié)。 列表 4-29 中的剩余代碼是為了進行清理工作,如關閉句柄和卸載 Spy 驅動(該功能是可選的)。 w2k_mem.exe 的源代碼中還有一些有趣的代碼片斷,但我不在這里討論它們了。請參考本書光盤的 srcw2k_mem 目錄下的 w2k_mem.c 和 w2k_mem.h 。
現(xiàn)在唯一需要注意的就是 gfSpyUnload 標志,該標志決定是否卸載 Spy 驅動。我已經將這個全局標志設為了 FALSE ,因此不會自動卸載該驅動。這提高 w2k_mem.exe 或 w2k_spy.sys 的任何客戶端的性能,因為加載一個驅動需要花費一定的時間。只有第一個客戶端會產生加載開銷。這種設置還可避免多個客戶端間的競爭,如,一個客戶試圖卸載該驅動而此時另一個還在使用這個驅動。當然, Windows 2000 不會卸載一個驅動,除非該驅動的所有句柄都被關閉了,但系統(tǒng)會將驅動置于 STOP_PENDING 狀態(tài),這樣新的客戶端將無法訪問此設備。不過,如果你不在一個多客戶端的環(huán)境下運行 w2k_spy.sys ,而且你需要經常更新設備的驅動程序,你就應該將 gfSpyUnload 標志設為 TRUE 。
深入 Windows 2000 內存
引入用戶模式和內核模式的獨立 4GB 地址空間被再次劃分為多個更小的塊。正如你可能猜到的,它們中的大多數都包含未文檔化的結構,而且服務于未文檔化的地目的。其中某些東西對于任何開發(fā)系統(tǒng)診斷或調試軟件的人來說都是真正的金礦。
基本的操作系統(tǒng)信息
如果你注意過 示列 4-1 下半部分的幫助信息,你會發(fā)該節(jié)的標題是:“系統(tǒng)狀態(tài)選項”?,F(xiàn)在試試名為“顯示操作系統(tǒng)信息”的選項: +o 。 示列 4-12 給出了在我的機器上使用該選項的輸出結果。這里顯示的信息都是 SPY_OS_INFO 結構的內容,該結構定義與 列表 4-13 ,由 Spy 設備函數 SpyOutputOsInfo() 實際創(chuàng)建該結構,此函數也包含在 列表 4-13 中。在 示列 4-12 中,你可以看到位于 4GB 地址空間中的進程的一些典型地址。例如,有效的用戶地址范圍是: 0x00010000 ---- 0x7FFFFFFF 。你可能閱讀過其他有關 Windows NT 或 2000 的程序設計書籍,用戶模式的第一個和最后一個 64KB 線性內存區(qū)域是“不能訪問區(qū)域”,訪問這一區(qū)域將引發(fā)一個錯誤(參見第五章, Solomon 1998 ), W2k_mem.exe 輸出證明了這一點。
示列 4-12. 顯示操作系統(tǒng)信息
示列 4-12 中的最后三行包含的信息非常有趣,它們都是有關系統(tǒng)的。這些信息大部分都取自位于地址 0xFFDF0000 處的 SharedUserData 區(qū)域中。系統(tǒng)在該處維護一個名為 KUSER_SHARED_DATA 的結構,該結構定義于 DDK 頭文件 ntddk.h 。