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

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

Apr 23, 2014
Coding
Windows, C++

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对应的英文描述。

img.png img.png

内部机制 #

从系统内部来讲,当一个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: 关闭源进程中的句柄。该标志实现了内核对象句柄在进程间的传递