第1章 對程序錯誤的處理
調用Windows函數時,首先檢驗傳遞參數的有效性,如無效,或無法執行,系統返回一個值,指明該函數運行失敗。
Windows函數常用的返回值類型:
VOID 該函數的運行不可能失敗。
BOOL 失敗返回0,否則非0。可測試
HANDLE 失敗返回NULL,否則返回HANDLE。標識可操作對象
注,有些函數失敗返回句柄值INVALID_HANDLE_VALUE,值爲-1。具體見Platform SDK 文檔
PVOID 失敗,返回NULL,否則PVOID,標識數據塊內存地址
LONG/DWORD 根據返回值閱讀Platform SDK文檔,以確保正確檢查潛在的錯誤
Microsoft編譯了一個所有可能的錯誤代碼列表,每個代碼都有一個32位的號碼.
Windows函數檢測到錯誤時,會使用線程本地存儲器(thread-local storage)將相應的錯誤代碼號碼與調用的線程關聯起來(線程本地存儲器見21章)。這使線程能互相獨立運行,不會影響各自的錯誤代碼。函數返回值將指明錯誤已經發生,如要確定錯誤,需調用GetLastError函數:返回值爲一個32位錯誤代碼。
WinError.h文件中包含了錯誤代碼列表。每個錯誤由3部分組成:消息ID,消息文本和一個號碼。
1. Microsoft選擇使用最後錯誤代碼機制(成功時也改寫最後的錯誤代碼)來返回函數成功原因信息。運行成功時,可通過調用 GetLadtError函數來確定其他的一些信息。
調試時,可在 Watch窗口中鍵入“@err,hr”,就能監控線程的最後錯誤代碼。
圖1-1鍵入“@err,hr”Watch窗口觀看最後錯誤代碼
2. Visual studio還配有Error Lookup程序,可將錯誤代碼號換成相應文本描述。
3. 可用Format-Message()函數將錯誤代碼轉換成它的文本描述。
DWORD dwError = GetDlgItemInt(hwnd, IDC_ERRORCODE, NULL, FALSE); //nr
HLCAL hlocal = NULL; //Buffer that gets the error message string
BOOL f0k= FormatMessage (FORMAT_MESSAGE_FROM_SYSTEM | //標誌
FORMAT_MESSAGE_ALLOCATE_BUFFER, //分配內存
NULL, dwError, //錯誤代碼號
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
(LPTSTR) &hlocal, 0, NULL); //內存句柄
此函數爲多語言的,這裏選擇英語描述。
定義自己的錯誤代碼:
設定線程最後的錯誤代碼,讓函數返回FALSE、INVALID_HANDLE_VA LUE、NULL等合適信息即可。
1. 使用 WinError.h中已經存在的代碼:
VOID SetLastError(DWORD dwErrCode);
2. 創建自己的代碼:錯誤代碼域的第29位(客戶定義代碼位)必須爲1
第2章 Unicode
單字節字符集將文本串作爲一系列單字節字符來進行編碼,並在結尾處放一個零。
strlen函數,只能指出到達結尾的0之前有多少個字節,不能分辨是否爲字符。
Unicode:寬字節字符集
Unicode中所有字符都是16位的(兩個字節), 所以只需對指針遞增或遞減,就可遍歷字符串中的各個字符,共約65000個。如將現行的所有字母和符號加起來,約35000個不同的代碼點. 分別位於65536個字符所分成的不同區域裏。
爲何使用Unicode
* 可在不同語言之間進行數據交換。
* 使你能夠分配支持所有語言的單個二進制. exe文件或DLL文件。
* 提高應用程序的運行效率
如何編寫Unicode源代碼
Microsoft爲Unicode設計了Windows API。你也可以編寫單個源代碼文件,來確定使用或不使用 Unicode來對它進行編譯。加上宏定義(UNICODE和_UNICODE),就可以修改然後重新編譯該源文件。
所有Unicode函數均以wcs開頭。只需用前綴wcs來取代 ANSI字符串函數的前綴str即可調用Unicode函數
若要創建同時爲ANSI和Unicode進行編譯的單個源代碼文件,必須包含TChar.h,而不是String.h。TChar.h的作用是幫助創建ANSI/Unicode通用源代碼文件。文件中包含了一組宏,如在編譯源代碼文件時定義了_UNICODE,這些宏就會引用wcs組函數。如沒有定義_UNICODE,這些宏將引用str組函數。
Windows定義的Unicode數據類型
Unicode編碼:
UCS: UCS-2: 2個字節。 2^16=65536個碼位
UCS-4: 4個字節,只用31位,最高位爲0. 2^31=2147483648個碼位
最高位爲0的最高字節分成2^7=128個group。每個Group下根據次高位分爲256個plane.每個plane根據第三個字節分爲256行(rows),每行包含256個cells.
Group 0的plane 0被稱爲BMP(Basic Multilingual Plane).即最高2字節爲0的碼位被稱爲BMP.
將UCS-4的BMP去掉前面的2個0字節就得到UCS-2.
31 23 15 7 0
0 |
group |
plane |
rows |
cells |
UTF = UCS Transformation Format,以8位爲單元對UCS進行編碼。
UCS-2(hex) to UTF-8 (bin)
0000-007F 0xxxxxxx
0080-07FF 110xxxxx 10xxxxxx
0800-FFFF 1110xxxx 10xxxxxx 10xxxxxx
“漢”Unicode碼 UCS-2: 6c49=0110110001001001<-> UTF16
UTF-8: 11100110 10110001 10001001即E6 B1 89
UTF的字節序: ”ZERO WIDTH NO-BREAK SPACE”字符即 BOM(Byte Order Mark)
UTF-16在BOM中寫入 FEFF-> Big-Endian
FFFE-> Little-Endian
UTF-8在BOM中寫入EF BB BF->表明編碼方式,即UTF-16的FEFF
11101111 10111011 10111111=EF BB BF
第3章 內 核 對 象
每個內核對象只是內核分配且只能由該內核訪問的一個內存塊。此內存塊是一種數據結構,它的成員負責維護該對象的各種信息。Windows提供了一組函數用來操作內核對象。
用於創建內核對象的所有函數均返回與進程相關的句柄,這些句柄可以被在相同進程中運行的任何或所有線程成功地加以使用, 這樣,系統就能知道你想操作哪個內核對象。
3.1.1 內核對象的使用計數
內核對象由內核所擁有,而不是由進程所擁有。它通過數據成員-使用計數來知道有多少個進程正在使用某個內核對象。如果內核對象的使用計數降爲 0,內核就撤消該對象。這樣可以確保在沒有進程引用該對象時系統中不保留任何內核對象。
3.1.2 安全性
內核對象能夠得到安全描述符的保護。它描述了誰創建了該對象,誰能夠訪問或使用該對象,誰無權訪問該對象。通常用在編寫服務器應用程序上。
默認安全性(PSECURITY_ATTRIBUTES NULL)意味着對象的管理小組任何成員和對象的創建者都擁有全部訪問權,其他人均無權訪問該對象。如想限制人們對你創建的內核對象的訪問,必須創建一個安全性描述符,自行定義一個 SECURITY_ATTRIBUTES結構,對它進行初始化,併爲該參數傳遞該結構的地址。
typedef struct _SECURITY_ATTRIBUTES{
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES;
當想要獲得相應的內核對象訪問權時,必須設定要對該對象執行什麼操作。例,OpenfileMapping(FILE_MAP_READ,FALSE, “MyFileMapping”),設定對象爲讀操作。函數首先執行安全檢查, 如被允許訪問,返回一個有效句柄,否則NULL,調用GetLastError函數返回5(ERROR_ACCESS_DENIED)。
創建內核對象的所有函數幾乎都有可用來設定安全屬性信息的 PSECURITY_ ATTRIBUTES參數,可以用來分辨用戶對象與內核對象。
3.2 進程的內核對象句柄表
當一個進程被初始化時,系統要爲它分配一個句柄表,即數據結構數組。每個結構都包含一個指向內核對象的指針、一個訪問屏蔽和一些標誌。
3.2.1 創建內核對象
當進程初次被初始化時,其句柄表爲空。然後,當進程中的線程調用創建內核對象的函數時,內核就爲該對象分配一個內存塊,並對它初始化。這時,內核對進程的句柄表進行掃描,以便找出一個空項。由於此時句柄表是空的,內核便找到索引1位置上的結構並對它進行初始化。該指針成員將被設置爲內核對象的數據結構的內存地址,訪問屏蔽設置爲全部訪問權,同時,各標誌也作了設置。
每當調用一個將內核對象句柄接受爲參數的函數時,就要傳遞由一個 Create*&函數返回的值。從內部來說,該函數要查看進程的句柄表,以獲取要生成的內核對象的地址,然後按定義好的方式來生成該對象的數據結構。
如傳遞了一個無效索引(句柄),該函數返回失敗,而GetLastError則返回6(ERROR_INVALID_HANDLE)。由於句柄值實際上是放入進程句柄表的索引,用於標識內核對象信息存放位置,因此這些句柄是與進程相關的,並且不能由其他進程成功使用。
在系統內存短缺,或遇到安全方面的問題時,會發生調用函數創建內核對象失敗,返回的句柄值通常是 0(NULL)。少數函數失敗時返回-1(INVALID_HANDLE_VALUE)。
3.2.2 關閉內核對象
無論怎樣創建內核對象,都要通過調用CloseHandle來結束對該對象的操作:
BOOL CloseHandle(HANDLE hobj);
該函數首先檢查調用進程的句柄表,以確保傳遞給它的索引(句柄)是用於標識一個進程實際上無權訪問的對象。如該索引有效,系統可獲得內核對象的數據結構地址,並可確定該結構中的數據成員-使用計數。如使用計數是0,該內核便從內存中撤消該內核對象。
如句柄無效,將出現兩種情況。1).如果進程運行正常,CloseHandle返回FALSE,而GetLastError則返回ERROR_INVALID_HANDLE。2).如果進程正在排除錯誤,系統將通知調試程序,以便能排除它的錯誤。
在CloseHandle返回之前,無論內核對象是否已經撤消,都會發生進程的句柄表項目清除操作。調用之後,函數不再擁有對內核對象的訪問權。
一般當進程終止運行時,系統將保證進程不會留下任何對象。對於內核對象來說,即使沒有執行CloseHandle(),系統也將執行下列操作:當進程終止運行時,系統會自動掃描進程的句柄表。如果該表擁有任何無效項目(即在終止進程運行前沒有關閉的對象),系統將關閉這些對象句柄。如果這些對象中的任何對象的使用計數降爲0,那麼內核便撤消該對象。
3.3 跨越進程邊界共享內核對象(允許進程共享內核對象的3個機制)
3.3.1 對象句柄的繼承性
進程需具有父子關係。父進程必須執行的操作步驟。
1). 父進程創建內核對象時,必須向系統指明,對象的句柄是個可繼承的句柄。(內核對象句柄具有繼承性,內核對象本身不具備繼承性。)
=> 父進程須指定一個 SECURITY_ATTRIBUTES結構並對它進行初始化,然後將該結構的地址傳遞給特定的 Create函數。
2).生成子進程。使用 CreateProcess()來完成:
將bInheritHandle參數設爲TRUE時,子進程就可以繼承父進程的可繼承句柄值。系統要遍歷父進程的句柄表,將找到的包含有效的可繼承句柄的每個項目準確地拷貝到子進程的句柄表中。即在父進程與子進程中,標識內核對象所用的句柄值是相同的。此外,系統還要遞增內核對象的使用計數。
* 對象句柄的繼承性只有在生成子進程的時候才能使用。當父進程創建帶有可繼承句柄的新內核對象時,那些已經產生過的子進程將無法繼承到這些新句柄。
第一個機制:將句柄值傳給子進程
l 初始化前: 句柄值作爲命令行參數(sscanf();)
進程間通信:
l 初始化後: (WaitForInpuIdle();)後,父進程將一條消息發送或展示在子進程中的一個線程創建的窗口中。
l 父進程給它的環境程序塊添加一個環境變量。該變量名是子進程知道的要查找的某種信息,變量值則是內核對象要繼承的值。(GetEnvironmentVariable();)
如子進程要生成另一個子進程,這種方法很好,因環境變量可以被再次繼承。
3.3.2 改變句柄的標誌
SetHandleInformation(HANDLE hObject, //標識一個有效的句柄
DWORD dwMask, //想要改變哪個或那幾個標誌
DWORD dwFlags); //該標誌設置成什麼值
第二個參數如想同時改變多個標誌,可逐位用 OR將這些標誌連接起來。
例如,打開一個內核對象句柄的繼承標誌:
#define HANDLE_FLAG_INHERT 0x00000001
#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002
SetHanleInformation(hobj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERT);
關閉標誌,代碼:
SetHanleInformation(hobj, HANDLE_FLAG_INHERIT, 0);
HANDLE_FLAG_PROTECT_FROM_CLOSE標誌將告訴系統,該句柄不應被關閉:
如一個線程試圖關閉一個受保護的句柄, CloseHandle就會產生一個異常條件。
一定要關閉的話,也可調用 HANDLE_FLAG_PROTECT_FROM_CLOSE標誌,來關閉句柄。
若要了解句柄是否是可繼承的,代碼如下:
GetHandleInformation (hObj, &dwFlags);
BOOL fHandleIsheritable = (0 !=(dwFlags & HANDLE_FLAG_INHERIT));
第二個機制:3.3.3 命名對象
許多(不是全部)內核對象都是可命名的。所有可以創建命名的內核對象的函數都有一個共同的最後參數 pszName。當該參數爲NULL時,就向系統指明瞭想創建一個未命名的(匿名)內核對象。匿名對象可通過使用繼承性或DuplicateHandle共享跨越進程的對象。若要按名字共享對象,則必須爲對象賦予一個名字ópszName參數不是NULL,而是一個以0結尾的字符串名字的地址。該名的長度最多可以達到MAX_PATH(定義爲260)個字符。
按名字共享對象法:
1. 進程調用Create*函數: 2個進程,產生同名對象. 如果完全相同,第二個被賦予與進程相關的句柄值. 在2個進程同時關閉它們的對象句柄之前,該對象是不會被撤消的。注--這兩個進程中的句柄值可以是不同的值。
確定應用程序是否確實創建了一個新內核對象,而不是打開了一個現有的對象的方法是在調用 Create*函數後立即調用GetLastError:
If (GetLastError()==ERROR_ALREADY_EXISTS){...}
2. 進程調用Open*函數:如不存在帶有指定名字的內核對象,函數返回 NULL,GetLastError返回2(ERROR_FILE_NOT_FOUND)。
如存在帶有指定名字的內核對象,並檢查到它是相同類型的對象,那麼系統就要查看是否允許執行訪問。如擁有該訪問權,調用進程的句柄表就被更新,對象的使用計數遞增。如InheritHandle參數設TRUE,那麼返回的句柄將是可繼承的。
1和2間的主要差別是,如對象並不存在,那麼Create*函數將創建該對象,而Open*函數則運行失敗。
命名對象常常用來防止運行一個應用程序的多個實例。
3.3.4 終端服務器的名字空間
終端服務器擁有內核對象的多個名字空間。供服務程序使用的全局名字空間和供每個客戶程序會話自己的名字空間。這可保證一個會話無法訪問另一個會話的對象,儘管他們擁有相同的名字。
服務程序的名字空間對象總是放在全局名字空間中。默認下,在終端服務器中,應用程序的命名內核對象將放入會話的名字空間中。如要使它進入到全局名字空間中,則需將“ Global/”置於對象名的前面:
HANDLE h = CreateEvent(NULL, FALSE, FALSE, „Global//MyName“);
將“ Local/”置於對象名前面,也可顯式說明讓內核對象進入會話的名字空間:
HANDLE h = CreateEvent(NULL, FALSE, FALSE, „Local//MyName“);
第三個機制:3.3.5 使用DuplicateHandle函數複製對象句柄
該函數取出一個進程的句柄表中的項目,將其拷貝到另一個進程的句柄表中