Windows 核心编程 学习笔记 (第一部分)

目录

1. 错误处理

  •     Windows函数常见返回类型
  •     每个错误的三种表示法
  •     错误的内部机制
  •     错误代码转化为英文描述
  •     自定义错误代码

2. Unicode

  •     ANSI/UNICODE及通用宏
  •     Windows字符串函数

3. Unicode和ANSI之间的转换

  •     MultiByteToWideChar
  •     WideCharToMultiByte

4. 内核对象

5. 区分内核对象和用户对象或GDI对象

6. 进程的内核对象句柄表

7. 跨进程共享内核对象

  •     (1) 利用对象句柄的继承性
  •     (2) 利用命名对象 (实现程序只有一个实例)
  •     (3) 利用DuplicateHandle复制句柄

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中

[cpp]//
// MessageId: ERROR_SUCCESS
//
// MessageText:
//
// The operation completed successfully.
//
#define ERROR_SUCCESS 0L [/cpp]

每个错误都有三种表示法

(1)  消息ID,如ERROR_SUCCESS,可以用来和GetLastError返回值进行比较。

(2)  消息文本,如"The operation completed successfully", 是对错误的英文描述

(3)  数值(号码),如0L,避免使用错误的数值。

Visual Studio中提供了Error Lookup工具可以查看消息ID对应的英文描述。

clipboard.png clipboard1.png

内部机制

从系统内部来讲,当一个Windows函数检测到一个错误之后,它会使用一个称为线程本地存储器(thread-local storage)的机制,将相应的错误代码与调用的线程关联起来。这使得线程能够互相独立的运行,而不影响各自的错误代码。

函数返回时,它的返回值就能指明一个错误已经发生。若要确定具体是什么错误,使用GetLastError()函数,它返回错误的具体32位数值。

[cpp]DWORD WINAPI GetLastError(void);[/cpp]

调试代码的过程中,可以在Watch窗口中,输入@err,hr来查看最后的错误代码及其具体的英文描述。

将错误代码转换为英文描述

程序中通过GetLastError得到错误ID之后,可以通过FormatMessage函数转换为相应的英文描述

[cpp]#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;
}[/cpp]

自定义错误代码

31~30
29
28
27~16
15~0
内容含义
严重性
0=成功   1=供参考
2=警告   3=错误
区分Microsoft/客户
0=Microsoft
1=客户自定义错误
保留
必须是0
设备代码
由Microsoft公司定义
异常代码
由Microsoft/客户定义

2. Unicode

WinCE只支持Unicode函数,不支持ANSI函数。

[cpp]typdef unsigned short wchar_t[/cpp]

所有的Unicode字符串函数均以wcs开头,只需要将ANSI字符串函数的前缀str替换成wcs,即可得到相应的Unicode字符串函数。

[cpp]//字符串拷贝
char* strcpy(char*,const char*);
wchar_t* wcscpy(wchar_t*,const wchar_t*);

//字符串长度
size_t strlen(const char*);
size_t wcslen(const wchar_t*);[/cpp]

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:多字节字符串转换为宽字节字符串

[cpp]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        //缓存的大小
);[/cpp]

[cpp]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);[/cpp]

WideCharToMultiByte: 宽字节字符串转多字节字符串

[cpp]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 //
);[/cpp]

[cpp]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);[/cpp]

每个内核对象只是内核分配的一个内存卡,并且只能由该内核访问。该内存块是一种数据结构,它的成员负责维护对象的各种信息。

进程只能调用Windows API创建内核对象,并通过将内核对象的句柄传递给Windows API 从而访问和操作内核对象。

为了是操作系统更加健壮,这些句柄值是与进程密切相关的。不能直接将一个进程中的句柄传递给另一个进程使用。(跨进程的内核对象共享也是可以实现的)

内核对象由内核所拥有,而不是由进程所拥有。进程结束,内核对象不一定被撤销(可能另一个进程正在访问该内核对象)。

每个内核对象有一个引用计数,创建时为1,当另一个进程访问现有的内核对象时,计数值+1,进程结束-1,计数值为0时,内核就撤销该对象。

5. 区分内核对象和用户对象或GDI对象

菜单、窗口、光标、字体、图标等等,属于用户对象或GDI对象,不是内核对象。

区分的方法是看创建函数有没有PSECURITY_ATTRIBUTES这个参数,有则为内核对象,无则不是内核对象。如CreateFile和CreateIcon。

[cpp]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
);[/cpp]

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参数指明该内核对象句柄可继承。

[cpp]SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE; //指明该句柄可继承

HANDLE hMutex = CreateMutex(&sa,FALSE,NULL);[/cpp]

第二步:父进程生成子进程,指明子进程可继承父进程的内核对象句柄。设置bInheritHandles为TRUE.

[cpp]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//
);[/cpp]

bInheritHandles为TRUE时,系统会遍历父进程的句柄表,然后将包含可继承句柄的项,拷贝到子进程句柄表的相同位置(这就保证了句柄值相同),之后系统会递增内核对象的引用计数。

内核对象句柄的继承性有一个特性:子进程不知道它已经继承了任何句柄

通常可以将句柄值作为命令行参数传递给子进程,子进程使用sscanf函数进行分析,取出句柄值。或者,使用环境变量的方法传递(GetEnvironmentVariable())。

可以通过SetHandleInformation函数实现只允许某一个子进程继承内核对象的句柄。

SetHandleInformation函数

[cpp]BOOL WINAPI SetHandleInformation(  //设置句柄对象的属性(其实就是设置句柄表中的一项)
_In_  HANDLE hObject,  //要设置的句柄
_In_  DWORD dwMask,    //要设置的标志的掩码(与dwFlags相同)
_In_  DWORD dwFlags    //标志的具体值 HANDLE_FLAG_INHERIT 或 HANDLE_FLAG_PROTECT_FROM_CLOSE
);[/cpp]

[cpp]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)[/cpp]

很少有需要将句柄设置为受保护的,但是有一种情况下需要:父进程生成了子进程,子进程生成了孙进程,父进程需要和孙进程通信,所以不希望子进程在创建孙进程之前把句柄关闭了,这是就需要设置为受保护,子进程就关闭不了,孙进程就能继承,继而可以和父进程通信。

相应的还有个GetHandleInformation()函数。

(2) 利用命名对象

不是所有的内核对象都是可以命名的。

[cpp]HANDLE WINAPI CreateMutex(
__in_opt  LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全属性
__in      BOOL bInitialOwner, //TRUE表示如果调用者创建了该Mutex,则其属于调用者。FALSE表示调用者没有该Mutex的拥有权
__in_opt  LPCTSTR lpName      //Mutex的名称
);[/cpp]

如果lpName设置为NULL, 则创建的是一个未命名对象。lpName是一个 \0 结尾的字符串,最长为MAX_PATH。

问题:所有命名对象的名称是保存在系统单个名空间中的,所以可能存在命名冲突。

[cpp]HANDLE hMutex = CreateMutex(NULL,FALSE,"MyMutex");[/cpp]

进程创建一个命名对象时,系统首先会查找是否已存在同名的内核对象,如果存在,则再判断是否是相同类型的对象,如果是相同类型的对象,则会执行安全检查,确定是否拥有该对象的访问权,如果有访问权,则会在当前进程的句柄表中找一空项,初始化该项并指向现有的内核对象。类型不匹配或拒绝访问,创建都会失败。

当然,也可以使用Open*系列函数来打开已有的内核对象句柄。

[cpp]HANDLE WINAPI OpenMutex(
__in  DWORD dwDesiredAccess,
__in  BOOL bInheritHandle,
__in  LPCTSTR lpName
);[/cpp]

Create*系列函数和Open*系列函数的区别是:如果命名对象不存在,Create* 会创建,Open* 会失败。

为了保证对象的唯一性,建议创建一个GUID,使用GUID的字符串作为对象名。

保证程序只有一个运行实例的实现:

[cpp]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;
}[/cpp]

(3) 利用DuplicateHandle复制句柄

该函数取出一个进程的句柄表中的一项,并将该项拷贝到另一个进程的句柄表中

[cpp]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的任何组合
);[/cpp]

DUPLICATE_SAME_ACCESS: 新句柄和源句柄具有相同的访问屏蔽,此时会忽略dwDesiredAccess参数。

DUPLICATE_CLOSE_SOURCE:  关闭源进程中的句柄。该标志实现了内核对象句柄在进程间的传递

作者:JarvisChu
原文链接:Windows 核心编程 学习笔记 (第一部分)
版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0

发表评论