作者:shenzi
鏈接:http://hi.csdn.net/shenzi
Windows核心編程:內核對象
對象是靜態定義的對象類型的單個運行時實例。對象類型包括系統定義的數據類型,在數據類型實例上的操作的函數以及一組對象屬性集。
對象爲完成下列四種重要的操作系統任務提供了方便的方法:
- 爲系統資源提供可讀的名字;
- 在進程間共享資源和數據;
- 保護資源以免非授權訪問;
- 引用跟蹤,它允許系統確知對象什麼時候不再使用以便自動被釋放;(引用計數,稍後講到)
在Windows操作系統中我們常常接觸的有三種對象類型:
- Windows內核對象 (事件對象,文件對象,進程對象,I/O完成端口對象,互斥量對象,進程對象,線程對象等等):由執行體(Excutive)對象管理器(Object Manager)管理,內核對象結構體保存在系統內存空間(0x80000000-0xFFFFFFFF),句柄值與進程相關。
- Windows GDI對象
(畫筆對象,畫刷對象等):由Windows子系統管理,句柄值在系統,會話範圍
(system-wide / session-wide)
有效。
- Windows USER對象 (窗口對象,菜單對象等) :由Windows子系統管理,句柄值在系統,會話範圍 (system-wide / session-wide) 有效。
2.Windows 內核對象
每個內核對象都只是一個內存塊,它由操作系統分配,並只能由操作系統訪問。這個內存塊是一個數據結構,其成員維護着與對象相關的信息。少數成員(安全描述符和使用計數等)是所有對象都有的,但其它大多數成員都是不同類型的對象特有的。由於內核對象的數據結構只能由操作系統內核訪問,所以應用程序不能在內存中定位這些數據結構並直接更改其內容。
應用程序只能間接的操縱這些數據結構,這就有了句柄(HANDL)。調用一個創建內核對象的函數後系統會返回一個句柄,它標識了多創建的對象,它可由進程中的任何線程訪問。
2.1使用計數
我們應該搞清楚的是:內核對象的所有者是操作系統,而內核對象的句柄的所有者是進程。
內核對象的生命期可能長於創建它的那個進程。
進程創建一個內核對象,如果該內核對象使用了跨進程的內核對象共享(馬上講到),即其它進程也取得了該內核對象的使用權限,操作系統使用一個叫使用計數
的數據成員來記錄內核對象的使用情況。每當有進程取得該內核對象的訪問後,使用計數加1,當進程終止或關閉該內核對象句柄時,使用計數減1;操作系統記錄每個內核對象的使用計數,一旦內核對象的使用計數爲0,操作系統銷燬該對象。
2.2內核對象安全性
內核對象可以用一個安全描述符(security descriptor,SD)來保護。它描述了誰擁有對象,哪些用戶和用戶被允許訪問或使用此對象;哪些組和用戶被拒絕訪問此對象。通常在編寫服務器應用程序的時候使用。
用於創建內核對象的函數幾乎都有指向一個SECURITY_ATTRIBUTES
結構的指針作爲參數,如下面的CreateFileMapping
:
HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
PCTSTR pszName);
大多數應用程序只是爲這個參數傳入NULL,這樣創建的內核對象具有默認的安全性——具體包括哪些默認的安全性要取決於當前進程的安全令牌(security token)。
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES;
2.3進程內核對象句柄表
一個進程在初始化時,系統將爲它分配一個句柄表(handle table)。這個句柄表僅供內核對象使用
,不適用於USER對象或GDI對象。
而對於句柄表的結構我們不得而知,不過大概如下:
索引 |
指向內核對象內存塊的指針 |
訪問掩碼 | 標誌
|
---|---|---|---|
1 |
0x???????? |
0x???????? |
0x???????? |
2 |
0x???????? |
0x???????? |
0x???????? |
… |
… |
… |
… |
圖1—句柄,指針,對象
2.3.1創建一個內核對象
一
個進程首次初始化時,其句柄表爲空。當進程的某個線程調用了一個會創建內核對象的函數時,內核將爲這個對象分配內存,並掃描進程句柄表,找到一個空白的記
錄項,並對其進行初始化。指針成員被設置成內核對象的數據結構的內部內存地址,訪問掩碼被設置成擁有完全訪問權限,標誌也會被設置,根據句柄的繼承性。
由於句柄值實際是作爲進程句柄表的索引來使用的,所以這些句柄是與當前所使用的進程相關的,無法供其它進程使用。如果我們真的在其它進程使用它,那麼實際引用的知識那個進程的句柄表中位於同一個索引的內核對象——只是索引值相同而已,我們根本不知道它會指向什麼對象。
調用函數
來創建一個內核對象時,如果調用失敗,那麼返回的句柄值通常爲0(NULL),有幾個函數調用失敗時返回的是-1,即INVALID_HANDLE_VALUE;所以在檢查函數返回值時務必相當仔細。
2.3.2關閉內核對象
我們通過調用函數:
BOOL CloseHandle(HANDLE hobject);
來表明結束使用內核對象,該函數將清除進程句柄表中對應的記錄項,並且遞減該內核對象的“使用計數”
,如果該內核對象的"使用計數"減爲“0”,那麼操作系統銷燬該內核對象,即將對象從內存中清除,但是“使用計數”大於“0”,該內核對象將繼續保留,因爲還有其它進程擁有該內核對象的訪問權限。
通常,在創建一個內核對象時,我們會將相應句柄保存到一個變量中。將此變量作爲參數調用CloseHandle()函數後,最好能同時將這個變量設爲NULL。
在不需要再使用對象時,釋放相應對象句柄是一個好習慣,如果句柄沒有及時釋放,可能引起內存泄露。
當進程終止運行,操作系統會確保此進程所使用的所有資源都被釋放。進程終止時,系統掃描進程句柄表,操作系統將關閉所有句柄表中的有效記錄項。只要這些對象中有一個的“使用計數”減爲0,內核就會銷燬對象。
進程終止時,系統也將保證進程擁有的所有資源(GDI對象等)以及內存塊都將得到正確清除。
3.跨進程共享內核對象
不同進程運行中的線程處於某些原因需要共享內核對象:
- 利用文件映射對象,可以再同一臺機器上運行的兩個不同進程之間共享數據塊;
- 藉助郵件槽和命名管道,在網絡中的不同計算機上運行的進程可以相互發送數據塊;
- 互斥量,信號量和事件允許不同進程中的線程同步執行。
有三種不同機制來允許進程共享對河對象:使用對象句柄繼承;爲對象命名;複製對象句柄。
3.1使用對象句柄繼承
還記得
SECURITY_ATTRIBUTES
結構嗎?
SECURITY_ATTRIBUTES
結構中有個
BOOL bInheritHandle屬性,該屬性就是制定該內核對象是否具有繼承性。
對象句柄繼承,就是說讓進程的子進程繼承父進程的對象句柄,實例如下:
- 首先建立一個可繼承的對象句柄;
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE; // 指定返回的句柄具有繼承性;
HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);
- 父進程生成子進程,並讓子進程繼承對象句柄值;
BOOL CreateProcess(
PCTSTR pszApplicationName,
PTSTR pszCommandLine,
PSECURITY_ATTRIBUTES psaProcess,
PSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles,//制定子進程是否繼承父進程句柄表中“可繼承的句柄”,設爲“TRUE”
DWORD dwCreationFlags,
PVOID pvEnvironment,
PCTSTR pszCurrentDirectory,
LPSTARTUPINFO pStartupInfo,
PPROCESS_INFORMATION pProcessInformation);
這樣創建的子進程就繼承了父進程句柄表中“可繼承的句柄”,並且在子進程句柄表中的位置和父進程句柄中的位置是一樣的。即在父進程和子進程中,對一個內核對象的句柄值是完全一樣的,在句柄表中的記錄項也是一樣的。
爲了使子進程得到它想要的一個內核對象的句柄值,最常見的方式是將句柄值作爲命令行參數傳給子進程。
介紹兩個函數:
改變對象句柄表項信息:
BOOL SetHandleInformation(
HANDLE hObject,
DWORD dwMask,
DWORD dwFlags);
dwMask:
#define HANDLE_FLAG_INHERIT 0x00000001
#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002
獲取對象句柄表項信息:
BOOL GetHandleInformation(
HANDLE hObject,
PDWORD pdwFlags);
3.2爲對象命名
許多(但不是全部)
內核對象
都可以進行命名。舉例:
HANDLE CreateMutex(
PSECURITY_ATTRIBUTES psa,
BOOL bInitialOwner,
PCTSTR pszName);
要爲內核對象制定一個名稱,參數
pszName應該傳入一個“以0爲終止符的名稱字符串”的地址。所有這些對象都共享同一個命名空間,及時它們的類型並不相同。
相較於使用句柄繼承,利用對象的名稱來共享內核對象,最大的一個優勢是“進程B不一定是進程A的子進程”。
當要建立指定名稱的對象時,系統先檢查是否存在此“名稱”的對象,如果確實存在內核將接着檢查對象的類型,如果類型匹配,再進行一個安全檢查,驗證調用者
是否擁有對該對象的安全訪問權限。如果答案是肯定的,系統將在調用進程句柄表中建立以記錄項,並將其初始化爲指向現有的內核對象。如果對象的類型不匹配或
調用者被拒絕訪問,函數返回NULL。
HANDLE hMutex = CreateMutex(&sa, FALSE, TEXT("JeffObj"));
if (GetLastError() == ERROR_ALREADY_EXISTS) {
// Opened a handle to an existing object.
// sa.lpSecurityDescriptor and the second parameter
// (FALSE) are ignored.
} else {
// Created a brand new object.
// sa.lpSecurityDescriptor and the second parameter
// (FALSE) are used to construct the object.
}
HANDLE OpenMutex(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);
Create*函數和Open*函數的主要區別是,如果對象存在,Create*函數會創建它;Open*函數則不同,如果對象不存在,它只是簡單地以調用失敗而告終。
3.3複製對象句柄
使用函數DuplicateHandle
實現共享內核對象
:
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle,
HANDLE hSourceHandle,
HANDLE hTargetProcessHandle,
PHANDLE phTargetHandle,
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions);
dwOptions:
DUPLICATE_SAME_ACCESS
:
表明我們希望目標句柄擁有與源進程的句柄一樣的訪問掩碼。
DUPLICATE_CLOSE_SOURCE
:會關閉源進程中的句柄,內核對象的“使用計數”不會受到影響。