第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函数复制对象句柄
该函数取出一个进程的句柄表中的项目,将其拷贝到另一个进程的句柄表中