1. 错误处理原理
1. 错误获取
Windows函数错误处理方式:调用 Windows 函数时,它会先验证我们传给它的参数,然后再开始执行任务。如果传入参数无效,或者由于其他原因导致操作无法执行,则函数的返回值将指出函数因为某些原因失败了。
常见的Windows函数返回值数据类型
数据类型 | 含义 |
---|---|
VOID | 意味该函数的执行不可能失败 |
BOOL | 失败返回 0,成功返回非 0 值 |
HANDLE | 如果失败则返回 NULL,否则返回一个可以操作的对象的ID |
PVOID | 如果失败则返回NULL,否则返回一个内存地址 |
LONG/DWORD | 不确定 |
在内部,当 Windows 函数检测到错误时,它会使用一种名为 “线程本地存储区”(thread-local storage)的机制将相应的错误代码与”主调线程”(calling thread)关联到一起,这种机制使不同的线程能独立运行,不会出现相互干扰到对方的错误代码的情况。
绝大多数的Windows API在执行完毕后会调用SetLastError()函数在线程上设置一个错误码,可以在相关API函数执行完毕后使用GetLastError()函数获取其错误码。也可以通过在Visual Studio中的监视窗口输入: $err,hr :分别指出错误码和错误信息.
一个API函数调用成功会有不同的原因,如,创建一个命名的事件内核对象,以下两种情况都会创建成功:对象实际完成创建 或 存在一个同名的事件内核对象。(此时可通过GetLastError()函数获取信息)
错误格式化函数——FormatMessage
DWORD FormatMessage(
DWORD dwFlags, //格式化选项,指定函数如何处理缓冲区处理行转换
LPCVOID pSource,
DWORD dwMessageId, //待格式化的错误码
DWORD dwLanguageId, //转换出来的语言(英语 中文...)
PTSTR pszBuffer, //接收转换出的字符串缓冲区
DWORD nSize //输出缓冲区大小
va_list *Arguments); //一组插入格式化消息中的值
一个显示错误信息的函数示例:
void ShowErrorInfo(LPCTSTR lpErrInfo, UINT unErrCode, UINT unLine\*=__LINE__*\)
{
LPCTSTR lpMsgBuf = nullptr;
WCHAR szMessage[128] = {0};
WCHAR szCaption[32] = {0};
FormatMessage(0x1300, NULL, unErrCode, 0x400, (LPTSTR)&lpMsgBuf, 64, NULL);
StringCchPrintfW(szMessage, 128, L"Error_0x%08X:%s",unErrCode, lpMsgBuf);
StringCchPrintfW(szCaption, 32, L"%s(Error Line:%05d)", lpErrInfo, __LINE__);
MessageBox(NULL, szMessage, szCaption, MB_OK);
}
2. 自定义错误码
Windows API可以使用SetLastError()函数设置函数的错误码,我们也可以自定义自己的错误码.当我们让自己的函数返回错误码时,应该仔细参照WinError.h头文件,在系统已经定义好的错误码中选择合适我们使用的,如果实在没有合适的,我们可以使用以下规则设计我们自己的错误码:
位: | 31-30 | 29 | 28 | 27-16 | 15-0 |
---|---|---|---|---|---|
内容 | 严重性 | Microsoft/客户 | 保留 | Facility 代码 | 异常代码 |
含义 | 0 = 成功 1=信息(提示) 2=警告 3=错误 | 0=Microsoft定义的代码 1=客户定义的代码 | 必须为0 | 前256个值由Microsoft保留 | Microsoft/客户定义的代码 |
我们自己定义的错误代码无法使用”错误查找”工具解析其具体含义.
2.字符和字符串处理
ASCII码与UNICODE规范
在UNICODE规范下有 UTF-8、UTF-16、UTF-32这三种不同的Unicode转换格,因为现在操作系统内部默认的是unicode编码,所以编程时采用unicode编码可以避免操作系统内部频繁转换字符编码所带来的计算与内存消耗.
C运行库提供的字符串处理函数:
// C语言提供的ASCII与宽字符操作函数 strlen() strcpy() wcslen() wcscpy() ... // C语言提供安全的字符操作函数 _tcscpy_s() _tcscat_s() // StringCbCat() StringCbCopy() StringCbPrintf() HRESULT StringCchCat(PTSTR pszDest, size_t cchDest, PCTSTR pszSrc) HRESULT StringCchCatEx(PTSTR pszDest, size_t cchDest, PCTSTR pszSrc, PTSTR *ppszDestEnd, //目标缓冲区终止字符 size_t *pcchRemaining,//缓冲区还有多少变量可使用,为NULL,则不返回 DOWRD dwFlags) HRESULT StringCchCopy() StringCchPrintf()
新版字符串处理函数大多数同时包含Cch或Cb版本,Cch版函数以字符为操作单位的,而Cb版本函数以字节为操作单位
Windows字符串函数
StrFormatKBSize() 与 StrFormatByteSize() //对操作系统有关的数值进行格式化操作 CompareString(Ex)与CompareStringOrdinal() //字符串比较函数
//字符转换函数
宽字符转换为多字符(Unicode–>ASCII)
#define WCHAR_TO_CHAR(lpW_Char, lpChar) \ WideCharToMultiByte(CP_ACP, NULL, lpW_Char, -1, lpChar, _countof(lpChar), NULL, FALSE);
多字符转换为宽字符 (ASCII–> Unicode)
#define CHAR_TO_WCHAR (lpChar, lpW_Char) \ MultiByteToWideChar(CP_ACP, NULL, lpChar, -1, lpW_Char, _count(lpW_Char));
- 转变思想,意识到”字符” 不等于 “字节”, 并应用到以后的程序开发中;
- 明确有意义的字符与字节数据的区别,编程时可以使用PCHAR表示一个存有字符信息的缓冲区指针,用PBYTE表示一个存有任何单字节数据的缓冲区指针,其他情况同理;
- 字符数应使用_countof()或相应的函数计算,而不能使用sizeof(),sizeof()仅限于计算某一数据的大小
3.内核对象
- 在Windows开发过程中,我们所做的绝大多数操作都是与内核对象打交道,例如使用CreateProcess()创建一个进程时,实际上就是创建了一个内核进程对象,内核对象有: 访问令牌(access token)对象、事件对象、文件对象、文件映射对象、I/O完成端口对象、作业对象、mailslot对象、mutex对象、pipe对象、进程对象、semaphore对象、线程对象、waitable timer对象以及 thread pool worker factory对象等。
这些内核对象的本质就是一块块从四字节到几十字节不等的内存对象,里面保存着此对象的一些配置信息与状态信息,这些内存空间中保存的内核对象仅限于系统内核使用,在用户层是无法直接访问并修改的。如果想操作这些内核对象,需要使用系统提供的一系列API函数进行。
绝大部分复杂的内核对象都有一个安全描述符,这个描述符说明了谁拥有此对象,哪些用户和组被允许访问或使用此对象,以及哪些用户和组被拒绝访问或者使用此对象。
- Windows下的安全描述符使用一个名为: SECURITY_ATTRIBUTES的结构体描述,其原型如下:
typedef _SECURITY_ATTRIBUTES{
DWORD nLength; //此结构体大小
LPVOID lpSecurityDescriptor; //安全描述符
BOOL bInheritHandle; //能否被新创建的进程继承
}SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES;
- 一般情况下,在创建内核对象时这个安全描述符可以给一个null,这意味着此内核对象将使用与当前令牌相关的默认安全属性.
进程调用 CloseHandle() 时,在 CloseHandle 函数返回之前,会清除进程句柄表中的记录项——这个句柄对该进程已无效。
- 跨进程边界共享内核对象
- 利用文件映射对象,可以在同一台机器上运行的两个不同进程之间共享数据块;
- 借助 mailslots 和 named pipes,在网络中不同计算机上运行的进程可以相互发送数据块;
- mutexes、semaphores 和 Event 允许不同进程中的线程同步执行。
命名内核对象,除了可以使用Create*()函数打开以外,也可以使用Open*()函数打开。例如,CreateEvent()、OpenEvent()。Create函数与Open类函数的区别是:当内核对象不存在时,前者会创建,后者不创建。
总结:
- 所有的内核对象都可以创建、打开、关闭;
- 绝大部分的内核对象都可以命名,且所有被命名的内核对象都是全局/局部有效的,都是可以跨进程访问的;
- 内核对象是可以被复制的, 例如使用第三方进程调用DuplicateHandle()将进程A的某些句柄复制给进程B。
- 内核对象的所有者是内核,而非进程,内核对象使用计数,内核对象的生命期可能长于创建它的那个进程。
在r3内调用的API 在内核层本质上没有创建文件,只不过是对一些结构体属性的设置。在r3是进程 在r0是内核对象—–结构体。