本章將會提示GDI控制代碼的每個細節和這些GDI控制代碼背後重要的資料結構,你也許對GDI資料結構的細節不感興趣,但是理解GDI/DirectDraw的內部設計會使你成為知識淵博的程式員。
對於GDI來說,常見的對象包括裝置上下文、邏輯畫筆、邏輯畫刷、邏輯調色盤、裝置相關位元影像。因此所有的裝置上下文對象都是裝置上下文類的執行個體,所有邏輯調色盤是邏輯調色盤類的執行個體。Win32API中的對象可以認為是使用沒有資料成員的抽象基類實現的,對象的資料表示對使用者程式是完全隱藏的。Win32API提供的資訊隱藏大大改善了程式的可移植性。
#指標與控制代碼
在物件導向語言中建立對象,需要分配一塊儲存對象成員變數的記憶體。如果它的類有虛函數,就要再分配一個額外的指標,並將它賦值成指向該類所有虛函數實現常式表的指標。在C++這樣的語言中,指向對象的指標很重要。它被傳遞給該類的所有非靜態成員函數,這樣才能訪問該對象的資料成員,調用正確的虛函數。這樣的指標被稱為C++中的this指標。
對於Win32API,儘管為每個對象分配了資料區塊,但微軟不想向使用者應用程式返回指標。因為指標給出了Object Storage Service的確切位置,指標一般允許對對象的內部表示進行讀寫操作,而這些內部表示也許正是作業系統想隱藏的。指標還使越過進程地址空間共識對象變得困難。
為了對程式員進一步隱藏資訊,Win32對象建立常式一般會返回物件控點,而不是返回指標。控制代碼被定義為唯一地標識對象的值,或者是對象的間接引用。
對象指標和控制代碼之間的映射可由函數Encode和Decode實現可視化。
#基於表格的映射
對象指標和控制代碼之間的映射最普通的機制是基於表格的映射。 在Win32API中,核心對象是用進程表實現的。核心對象包括互斥量、訊號量、事件、註冊鍵、連接埠、檔案、符號連結、對象目錄、記憶體對應檔、線程、進程、Windows工作站、案頭和計時器等。為了容納大量核心對象,每個進程都有自己的核心對象表。
#解碼GDI物件控點
建立了GDI對象,就會得到該對象的控制代碼。控制代碼的類型有可能是HPEN、HBRUSH、HFONT或HDC中的一種,這依賴於你建立的GDI物件類型。但是最普通的GDI物件類型是HGDIOBJ,HGDIOBJ被定義成null 指標。HPEN的實際編譯類型定義隨編譯時間宏STRICT的不同而不同。在STRICT版本中,編譯對於GDI物件控點的不正確混合將給出警告,或者對於非GDI控制代碼,如HWND、HMENU等的不正確混合也給出警告,把HPEN傳遞到接受HGDIOBJ的函數是可以的,但是如果不進行類型黑鬼就把HGDIOBJ傳遞給接受HBRUSH的函數是不可以的。
不同GDI控制代碼的這些清晰的定義模仿了GDI對象不同類的類階層,但沒有用真正的類階層。GDI對象只支援一個對象一個控制代碼,因此不能複製控制代碼為同一個對象建立另外一個控制代碼。GDI對象的控制代碼一般對進程是私人的。即只有建立GDI對象的進程才能使用它,將句西側傳到另外一個進程是無效的。
GDI對象一般有多個建立函數和一個接受HGDIOBJ的解構函式——DeleteObject。資源載入函數如LoadBitmap或LoadImage可以建立必要的GDI對象。
接下來作者利用程式Handles來說明HGDIOBJ的內部原理,其中對於對象表的映射利用到一個非文檔公開函數GdiQueryTable函數,但是很遺憾,在Win7下Note: GdiQueryTable (undocumented GDI32.DLL Win32 API) and PEB->GetSharedGdiHandleTable don't work on Win7.(http://www.chromium.org/developers/how-tos/leak-gdi-object-in-windows),無法使用GdiQueryTable函數。(但很奇怪,在Win8上恢複了此函數的使用。無語。
通過作者書上的例子(這個例子也需要修改才能在win7上正確運行),無論GetStockObject調用的順序是如何,它返回的控制代碼看起來總是常數。可能的解釋是系統初始化堆對象就被建立並被所有進程重新使用。另外我們注意到GDI控制代碼雖然在windef.h檔案中被定義成指標,實際上不可能是指標。書是討論了進程GDI和系統GDI最多建立的個數,我覺得現階段不需要關注。
HGDIOBJ的低4位16進位數內容為索引值,第3、4位16進位數內容是GDI物件類型的編碼和堆對象標記。32位HGDIOBJ控制代碼值基本結構如下:
8位未知+1位堆棧對象標記+7位物件類型+4位未使用+12位索引。
書中列舉了常規情況下GDI控制代碼類型的對應值(3,4位16進位數):
gdi_objtypeb_dc = 0x01;
gdi_objtypeb_region = 0x04;
gdi_objtypeb_bitmap = 0x05;
gdi_objtypeb_palette = 0x08;
gdi_objtypeb_font = 0x0a;
gdi_objtypeb_brush = 0x10;
gdi_objtypeb_enhmetafile = 0x21;
gdi_objtypeb_pen = 0x30;
gdi_objtypeb_extpen = 0x50;
#定位GDI控制代碼表
這部分是比較有Hack意思的部分,分析起來可以學習到很多東西,如果對記憶體處理不熟練,快點補充學習《windows核心編程》記憶體相關章節吧。
但是由於沒法正確調用GdiQueryTable返回控制代碼表的地址,我無法驗證我搜尋到的那個地址是否真的是實際上的控制代碼表地址,可以使用其他版本的windows開發環境嘗試一下。而再由於沒法確定這個地址,對於這個地址上面作者猜測的資料結構我目前也無法驗證。列舉作者猜測的每個表項結構:
typedef struct
{
void * pKernel;
unsigned short _nProcess; // NT/200 switch order for _nProcess, _nCount
unsigned short _nCount;
unsigned short nUpper;
unsigned short nType;
void * pUser;
} GDITableCell;
#GDI對象的使用者模式資料結構
使用者模式畫刷資料:純色畫刷最佳化;
使用者模式地區資料:正方形地區最佳化;
使用者模式字型資料:寬度表;
使用者模式裝置上下文資料:儲存設定資訊;
#存取核心模式地址空間
核心模式驅動程式是特殊的DLL,它遵守一套規則。如不能在核心模式驅動程式中調用Win32API,因為Win32API的進入點在使用者模式地址空間中。核心模式驅動程式被載入到核心地址空間。
WindowsNT/2000核心I/O裝置驅動程式一般只有進入點DriverEntry,當驅動程式載入後,DriverEntry就被調用。
NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, IN PUNICODE_STRING RegistryPath)
DriverEntry常式和_DllMainStartCRTStartup扮演同樣的角色——使用者模式DLL進入點,但是跟使用者模式DLL不一樣,核心模式驅動程式一般不匯出函數。。。
要深刻理解核心模式地址空間,需要把本章的Periscope例子編譯執行並理解。然而這需要不少的前置知識,包括部分彙編,Win32API,StartService,開啟裝置檔案,完成連接埠等。很幸運,我都在前面的讀書筆記中學習過,雖然有的不是很深刻,但現在第二次看到,加強了印象,再回去複習一下,鞏固知識。我目前已經在win7下調試還有點問題。
總的來說,這一章很枯燥,但很基礎,對後面的深入理解有重要的啟示作用。我打算多花點時間在這枯燥的一章,以弄明白一些來龍去脈。