Windows 核心编程 学习笔记(第一部分)
Apr 23, 2014
1. 错误处理 #
Windows函数常见返回类型 #
函数返回值类型 | 意义 |
---|---|
VOID | 该函数运行不可能失败。Windows函数返回值很少是VOID |
BOOL | 失败返回0,否则非0. [注:不要测试返回值是否为TRUE] |
HANDLE | 失败通常返回NULL,也可能是INVALID_HANDLE_VALUE(它被定义为-1)。成功则返回一个可用的HANDLE |
PVOID | 失败返回NULL,否则返回PVOID,以标识数据块的内存地址 |
LONG/DWORD | 返回数量的函数通常返回LONG或DWORD。如果由于某种 原因,函数无法对想要进行计数的对象进行计数,那么该函数通常返回0或- 1(根据函数而定) |
Windows编译了一个所有可能的错误代码的列表,并且为每个错误代码分配了一个32位的数值(号码),存放在WinError.h中
//
// MessageId: ERROR_SUCCESS
//
// MessageText:
//
// The operation completed successfully.
//
#define ERROR_SUCCESS 0L
每个错误都有三种表示法 #
- (1) 消息ID,如 ERROR_SUCCESS,可以用来和GetLastError返回值进行比较。
- (2) 消息文本,如 ”The operation completed successfully”, 是对错误的英文描述
- (3) 数值(号码),如 0L,避免使用错误的数值。
Visual Studio中提供了Error Lookup工具可以查看消息ID对应的英文描述。
内部机制 #
从系统内部来讲,当一个Windows函数检测到一个错误之后,它会使用一个称为线程本地存储器(thread-local storage)的机制,将相应的错误代码与调用的线程关联起来。这使得线程能够互相独立的运行,而不影响各自的错误代码。
函数返回时,它的返回值就能指明一个错误已经发生。若要确定具体是什么错误,使用GetLastError()函数,它返回错误的具体32位数值。
DWORD WINAPI GetLastError(void);
调试代码的过程中,可以在Watch窗口中,输入 @err,hr
来查看最后的错误代码及其具体的英文描述。
将错误代码转换为英文描述 #
程序中通过GetLastError得到错误ID之后,可以通过FormatMessage函数转换为相应的英文描述
#include <windows.h>
int main()
{
LPVOID lpMsgBuf;
SetLastError(ERROR_INVALID_FUNCTION);
DWORD dw = GetLastError();
//Translate Error Message
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS,//dwFlags
NULL, //lpSource
dw, //dwMessageID
MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT), //dwLanguageID
(LPTSTR)&lpMsgBuf, //lpBuffer
0, //nSize
NULL //va_list* Arguments
);
//Display Error Message
MessageBox(NULL,(LPCTSTR)lpMsgBuf,TEXT("Error"),MB_OK);
return 0;
}
自定义错误代码 #
位 | 31~30 | 29 | 28 | 27~16 | 15~0 |
---|---|---|---|---|---|
内容含义 | 严重性 0=成功 1=供参考 2=警告 3=错误 | 区分Microsoft/客户 0=Microsoft 1=客户自定义错误 | 保留 必须是0 | 设备代码 由Microsoft公司定义 | 异常代码 由Microsoft/客户定义 |
2. Unicode #
WinCE只支持Unicode函数,不支持ANSI函数。
typdef unsigned short wchar_t
所有的Unicode字符串函数均以wcs开头,只需要将ANSI字符串函数的前缀str替换成wcs,即可得到相应的Unicode字符串函数。
//字符串拷贝
char* strcpy(char*,const char*);
wchar_t* wcscpy(wchar_t*,const wchar_t*);
//字符串长度
size_t strlen(const char*);
size_t wcslen(const wchar_t*);
ANSI/UNICODE 及通用宏 #
ANSI | Unicode | 通用 |
---|---|---|
char | wchar_t | TCHAR |
“中文abc” | L"中文abc" | _T(“中文abc”) 或_TEXT(“中文abc”) |
strlen,strcpy… | wcslen,wcscpy… | _tcslen,_tcscpy… [C运行时] 或 lstrlen,lstrcpy… [Windows运行库] |
通用宏会根据是否define了_UNICODE宏,来转换成相应的char或wchar_t。 【包含在 tchar.h
头文件中,提供了C运行时对Unicode的支持】
Windows对Unicode的支持 #
WCHAR
Unicode字符PWSTR
指向Unicode字符串的指针PCWSTR
指向一个const型Unicode字符串的指针
通用 PTSTR/PCTSTR 需要定义UNICODE宏
- _UNICODE 宏用于C运行时头文件
- UNICODE 宏用于Windows头文件
当编译源代码模块时,通常必须同时定义这两个宏
Windows字符串函数 #
shlWApi.h头文件下 StrCat, StrChr, StrCmp, StrCpy 等字符串函数 推荐使用Windows字符串函数以提高效率。因为这些函数通常被操作系统的应用程序使用,所以当你的程序运行时,它们可能已经装入RAM了。
函数 | 说明 |
---|---|
lstrcat | 字符串连接 |
lstrcmp | 字符串比较 |
lstrcmpi | 字符串比较(忽略大小写) |
lstrcpy | 字符串拷贝 |
lstrlen | 字符串长度 |
3. Unicode和ANSI之间的转换 #
MultiByteToWideChar:多字节字符串转换为宽字节字符串 #
int MultiByteToWideChar(
__in UINT CodePage, //用来执行转换的代码页。如CP_ACP(系统缺省的ANSI代码页)
__in DWORD dwFlags, //
__in LPCSTR lpMultiByteStr, //需要被转化的MultiByte字符串
__in int cbMultiByte, //需要被转化的MultiByte字符串 的字节数。-1表示整个源字符串,包括’\0’.
__out LPWSTR lpWideCharStr, //指向存储转换后的宽字节字符串的缓存的指针
__in int cchWideChar //缓存的大小
);
char * pMultiByte = "多字节字符串";
int wlen = MultiByteToWideChar(CP_ACP,0,pMultiByte,-1,NULL,0);//计算需要的宽字节数组大小
wchar_t* buf = new wchar_t[wlen]; //宽字节缓存
MultiByteToWideChar(CP_ACP,0,pMultiByte,-1,buf,wlen); //多字节编码转换成宽字节编码
MessageBox(NULL,buf,_T("Caption"),MB_OK);
WideCharToMultiByte: 宽字节字符串转多字节字符串 #
int WideCharToMultiByte(
__in UINT CodePage, //用来执行转换的代码页。如CP_ACP(系统缺省的ANSI代码页)
__in DWORD dwFlags, //
__in LPCWSTR lpWideCharStr, //需要被转化的宽字节字符串
__in int cchWideChar, //需要被转化的宽字节字符串 的字节数
__out LPSTR lpMultiByteStr, //指向存储转换后的多字节字符串的缓存的指针
__in int cbMultiByte, //缓存的大小
__in LPCSTR lpDefaultChar, //
__out LPBOOL lpUsedDefaultChar //
);
wchar_t* pWideChar = L"宽字节字符串";
int len = WideCharToMultiByte(CP_ACP,0,pWideChar,-1,NULL,0,NULL,NULL); //计算需要的字节数
char* buf = new char[len]; //多字节缓存
WideCharToMultiByte(CP_ACP,0,pWideChar,-1,buf,len,NULL,NULL); //宽字节转多字节
printf("%s\n",buf);
每个内核对象只是内核分配的一个内存卡,并且只能由该内核访问。该内存块是一种数据结构,它的成员负责维护对象的各种信息。
进程只能调用Windows API创建内核对象,并通过将内核对象的句柄传递给Windows API 从而访问和操作内核对象。
为了是操作系统更加健壮,这些句柄值是与进程密切相关的。不能直接将一个进程中的句柄传递给另一个进程使用。(跨进程的内核对象共享也是可以实现的)
内核对象由内核所拥有,而不是由进程所拥有。进程结束,内核对象不一定被撤销(可能另一个进程正在访问该内核对象)。
每个内核对象有一个引用计数,创建时为1,当另一个进程访问现有的内核对象时,计数值+1,进程结束-1,计数值为0时,内核就撤销该对象。
5. 区分内核对象和用户对象或GDI对象 #
菜单、窗口、光标、字体、图标等等,属于用户对象或GDI对象,不是内核对象。
区分的方法是看创建函数有没有PSECURITY_ATTRIBUTES这个参数,有则为内核对象,无则不是内核对象。如CreateFile和CreateIcon。
HANDLE WINAPI CreateFile(
__in LPCTSTR lpFileName,
__in DWORD dwDesiredAccess,
__in DWORD dwShareMode,
__in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,
__in DWORD dwCreationDisposition,
__in DWORD dwFlagsAndAttributes,
__in_opt HANDLE hTemplateFile
);
HICON CreateIcon(
HINSTANCE hInstance,
int nWidth,
int nHeight,
BYTE cPlanes,
BYTE cBitsPixel,
const BYTE* lpbANDbits,
const BYTE* lpbXORbits
);
6. 进程的内核对象句柄表 #
当一个进程被初始化时,系统要为它分配一个句柄表。该句柄表只用于内核对象,不用于用户对象或GDI对象。句柄表大概如下(没有具体的官方文档资料):
索引 内核对象内存块的指针 访问屏蔽(标志位的DWORD) 标志(标志位的DWORD)
1 0xXXXXXX 0xXXXXXX 0xXXXXXX
2 0xXXXXXX 0xXXXXXX 0xXXXXXX
… … … …
- (1) 初始时,句柄表是空的
- (2) 创建一个内核对象时,首先为内核对象分配内存并初始化,然后扫描进程的句柄表,找出一个空项,依据创建的内核对象设置该项。
- 创建内核对象的所有函数均返回与进程相关的句柄。该句柄实际上是进程句柄表中的索引。
- 通常创建失败返回NULL,但是也有返回INVALID_HANDLE_VALUE的情况。如CreateFile返回的是INVALID_HANDLE_VALUE.
- (3) CloseHandle() 关闭内核对象。它会清除进程句柄表中的该项,同时使内核对象的引用计数-1,如果引用计数减为0,内核会撤销该对象。
7. 跨进程共享内核对象 #
(1) 利用对象句柄的继承性 #
只有当进程具有父子关系时,才能使用对象句柄的继承性。父进程在生成子进程时,可以赋予子进程对内核对象的访问权。
注意:内核对象句柄具有继承性,但是内核对象本身不具备继承性
第一步:父进程创建内核对象,并通过SECURITY_ATTRIBUTES结构体的bInheritHandle参数指明该内核对象句柄可继承。
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE; //指明该句柄可继承
HANDLE hMutex = CreateMutex(&sa,FALSE,NULL);
第二步:父进程生成子进程,指明子进程可继承父进程的内核对象句柄。设置bInheritHandles为TRUE.
BOOL WINAPI CreateProcess(
__in_opt LPCTSTR lpApplicationName, //要执行的模块的名称,如"cmd.exe",可以为NULL,则名称为lpCommandLine的第一个参数
__inout_opt LPTSTR lpCommandLine, //命令行参数
__in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes, //指向进程安全属性结构体的指针,用于确定指向新进程的句柄能否被它的子进程继承
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, //指向线程安全属性结构体的指针,用于确定指向新线程的句柄能否被它的子进程继承
__in BOOL bInheritHandles, //是否继承父进程的内核对象句柄
__in DWORD dwCreationFlags, //标志
__in_opt LPVOID lpEnvironment, //运行环境,NULL则使用父进程的环境
__in_opt LPCTSTR lpCurrentDirectory, //当前目录,NULL则与父进程相同
__in LPSTARTUPINFO lpStartupInfo, //
__out LPPROCESS_INFORMATION lpProcessInformation//
);
bInheritHandles为TRUE时,系统会遍历父进程的句柄表,然后将包含可继承句柄的项,拷贝到子进程句柄表的相同位置(这就保证了句柄值相同),之后系统会递增内核对象的引用计数。
内核对象句柄的继承性有一个特性:子进程不知道它已经继承了任何句柄。
通常可以将句柄值作为命令行参数传递给子进程,子进程使用sscanf函数进行分析,取出句柄值。或者,使用环境变量的方法传递(GetEnvironmentVariable())。
可以通过SetHandleInformation函数实现只允许某一个子进程继承内核对象的句柄。
SetHandleInformation函数
BOOL WINAPI SetHandleInformation( //设置句柄对象的属性(其实就是设置句柄表中的一项)
_In_ HANDLE hObject, //要设置的句柄
_In_ DWORD dwMask, //要设置的标志的掩码(与dwFlags相同)
_In_ DWORD dwFlags //标志的具体值 HANDLE_FLAG_INHERIT 或 HANDLE_FLAG_PROTECT_FROM_CLOSE
);
SetHandleInformation(hObject,HANDLE_FLAG_INHERIT,HANDLE_FLAG_INHERIT); //打开内核对象句柄的可继承性
SetHandleInformation(hObject,HANDLE_FLAG_INHERIT,0); //关闭内核对象句柄的可继承性
SetHandleInformation(hObject,HANDLE_FLAG_PROTECT_FROM_CLOSE,HANDLE_FLAG_PROTECT_FROM_CLOSE);//内核对象受保护,不可关闭
SetHandleInformation(hObject,HANDLE_FLAG_PROTECT_FROM_CLOSE,0); //内核对象不受保护,可以关闭(CloseHandle)
很少有需要将句柄设置为受保护的,但是有一种情况下需要:父进程生成了子进程,子进程生成了孙进程,父进程需要和孙进程通信,所以不希望子进程在创建孙进程之前把句柄关闭了,这是就需要设置为受保护,子进程就关闭不了,孙进程就能继承,继而可以和父进程通信。
相应的还有个GetHandleInformation()函数。
(2) 利用命名对象 #
不是所有的内核对象都是可以命名的。
HANDLE WINAPI CreateMutex(
__in_opt LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全属性
__in BOOL bInitialOwner, //TRUE表示如果调用者创建了该Mutex,则其属于调用者。FALSE表示调用者没有该Mutex的拥有权
__in_opt LPCTSTR lpName //Mutex的名称
);
如果lpName设置为NULL, 则创建的是一个未命名对象。lpName是一个 \0 结尾的字符串,最长为MAX_PATH。
问题:所有命名对象的名称是保存在系统单个名空间中的,所以可能存在命名冲突。
HANDLE hMutex = CreateMutex(NULL,FALSE,"MyMutex");
进程创建一个命名对象时,系统首先会查找是否已存在同名的内核对象,如果存在,则再判断是否是相同类型的对象,如果是相同类型的对象,则会执行安全检查,确定是否拥有该对象的访问权,如果有访问权,则会在当前进程的句柄表中找一空项,初始化该项并指向现有的内核对象。类型不匹配或拒绝访问,创建都会失败。
当然,也可以使用Open*系列函数来打开已有的内核对象句柄。
HANDLE WINAPI OpenMutex(
__in DWORD dwDesiredAccess,
__in BOOL bInheritHandle,
__in LPCTSTR lpName
);
Create系列函数和Open系列函数的区别是:如果命名对象不存在,Create* 会创建,Open* 会失败。
为了保证对象的唯一性,建议创建一个GUID,使用GUID的字符串作为对象名。
保证程序只有一个运行实例的实现:
int WINAPI WinMain( HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
HANDLE h = CreateMutex(NULL,FALSE,"{FA53CC1-0911-AF32-AFCE-1921213}"); //创建内核对象
if(GetLastError() == ERROR_ALREADY_EXISTS){
//已经存在,则表明已有一个程序实例在运行,退出
return 0;
}
//…
CloseHandle(h); //程序退出时,关闭句柄
return 0;
}
(3) 利用DuplicateHandle复制句柄 #
该函数取出一个进程的句柄表中的一项,并将该项拷贝到另一个进程的句柄表中
BOOL WINAPI DuplicateHandle( //复制句柄(取出一个进程句柄表的一项,并拷贝到另一个进程的句柄表中)
__in HANDLE hSourceProcessHandle, //进程的句柄,该进程拥有要复制的句柄
__in HANDLE hSourceHandle, //要复制的句柄
__in HANDLE hTargetProcessHandle, //进程的句柄,该进程接受复制的句柄
__out LPHANDLE lpTargetHandle, //指针,指向接受复制的句柄的变量
__in DWORD dwDesiredAccess, //新句柄的访问控制
__in BOOL bInheritHandle, //句柄是否可继承
__in DWORD dwOptions //其他可选项(可置0 或 DUPLICATE_SAME_ACCESS 和 DUPLICATE_CLOSE_SOURCE的任何组合
);
DUPLICATE_SAME_ACCESS: 新句柄和源句柄具有相同的访问屏蔽,此时会忽略dwDesiredAccess参数。
DUPLICATE_CLOSE_SOURCE: 关闭源进程中的句柄。该标志实现了内核对象句柄在进程间的传递