Windows核心编程:(一)内核对象
内核对象
内核对象的管理
用户程序不能直接操作内核对象,内核对象由操作系统内核所有。用户程序要操作内核对象,需要使用函数来操作。
内核对象创建
使用create*
函数创建内核对象,返回一个HANDLE。
对于每个进程,初始化时系统会分配一个句柄表(handle table),在其中维护一个个内核对象的句柄。包括:索引、指向内核对象内存块的指针、访问掩码(控制访问权限)、标志等。
在创建内核对象时,操作系统内核会为这个内核对象分配内存,然后查找主调进程的句柄表,找到一个 empty entry,然后对其进行初始化。
返回值
在调用内核对象创建函数时,如果创建失败(可能是内存不够,或者是遇到安全问题),那么就会返回一个句柄值。该句柄值可能是INVALID_HANDLE_VALUE
或者是NULL
。具体根据函数的不同而有不同的返回值。
内核对象关闭
BOOL CloseHandle(HANDLE hobject)
关闭流程如下:
- 检查主调进程的句柄表,验证
HANDLE hobject
的值是有效的。
若指定的HANDLE是无效的,那么就会有以下两种情况:
- 如果该进程是正常运行的,那么将会返回FALSE。GetLastError()函数返回ERROR_INVALID_HANDLE。
- 如果该进程正在调试中,则抛出0xC0000008异常
-
获取内核对象地址,访问内核对象。
-
将内核对象使用计数递减。
-
如果内核对象使用计数为0,销毁。
在这个过程中,同时也将清空句柄表中的对应项。
需要注意的是,调用CloseHandle
之后同时也应该将传入的句柄参数hobject
设为NULL。否则如果之后使用了这个HANDLE,要么因为句柄表对应位置为空,导致调用失败,返回ERROR_INVALID_HANDLE,更严重的可能会因为已经创建了一个新的内核对象,导致这个HANDLE指向的为一个完全不同的内核对象,到时出现难以预料的错误。
如果忘记关闭handle了呢?
进程在结束时,操作系统会自动释放已经分配的资源,包括其句柄表。在进程结束时,操作系统会扫描句柄表,然后将其中的HANDLE一个个释放,如果某个内核对象使用计数为0,那么就将其销毁。
内核对象的共享
Windows系统的内核对象是进程相关的。这样有两个好处:
- 可靠性。内核对象进程相关,一个进程就很难去干扰另一个进程的内核对象的运行,系统的稳定性和可靠性就会增加。
- 安全性。一个进程可以通过设置访问权限阻止其它进程操作自己的内核对象。
使用对象句柄继承
在父子进程之间继承对象句柄。(继承的是句柄,而不是对象)
父进程需要通过如下操作创建一个可以继承的句柄:
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
HANDLE hMutes = CreateMutex(&sa, FALSE, NULL);
以上代码初始化了一个SECURITY_ATTRIBUTES
结构,然后设置bInheritHandle
为TRUE;
如果在创建内核对象时,将NULL作为PSECURITY_ATTRIBUTES
参数传入,则默认不可继承。如果将以上设置了bInheritHandle
的SECURITY_ATTRIBUTES
传入,则内核对象是可以继承的。
然后使用BOOL CreateProcess()
函数创建子进程,然后指定其中的bInheritHandles
参数为TRUE。这样,在创建子进程时,操作系统就会遍历父进程的句柄表,然后将其中的可继承的项完全复制到子进程的句柄表。(包括作为索引的句柄值)。因此,子进程继承的父进程的内核对象,其句柄值是一致的。同时,操作系统还会自动增加内核对象的使用计数。
传递句柄
子进程并不知道自己继承了哪些句柄,因此需要一种方式,让子进程能够知道自己继承了哪些句柄,并且将它利用起来。
有以下几种方式让父进程向子进程传递句柄:
- 命令行参数:父进程将句柄值作为命令行参数传递给子进程。子进程调用
_stscanf_s()
函数解析命令行,获取句柄值。 - 利用进程间的通信机制。比如:
- 父进程等待子进程完成初始化(
WaitForInputIdle()
函数)。然后父进程向子进程发布消息。 - 父进程向环境块中添加环境变量,变量名称应该为子进程所知道的一个名称,变量的值为待继承的句柄值。然后,当父进程创建子进程,子进程继承父进程的环境变量,然后通过
GetEnvironmentVariable
来获得继承到的内核对象的句柄值(子进程通过查询环境变量的名称,到环境块中查询,得到句柄值)。
- 父进程等待子进程完成初始化(
改变句柄标志
使用函数SetHandleInformation()
。如下:
BOOL SetHandleInformation(
HANDLE hObject,
DWORD dwMask,
DWORD dwFlags
);
例如要打开继承标志:
SetHandleInformation(hObj, HANDLE_FLAG_INHERIT,HANDLE_FLAG_INHERIT);
SetHandleInformation(hObj, HANDLE_FLAG_INHERIT,0); // CLOSE
要查看句柄表项的标志,可以使用:
BOOL GetHandleInformation(
HANDLE hObject,
PDWORD pdwFlags
);
则会在pdwFlags
中存储标志值。
为内核对象命名
进程间也可以用对内核对象命名的方式来传递一个句柄。如:
进程A调用如下函数:
HANDLE hMutexProcessA = CreateMutex(NULL, FALSE, TEXT("JeffMutex"));
进程B要获取这个命名了的内核对象,只需要这样:
HANDLE hMutexProccessB = CreateMutex(NULL, FALSE, TEXT("JeffMutex"));
此时系统会查看内核对象的命名空间是否有一个叫做"JeffMutex"的对象,然后检查这个对象的类型,如果是要创建的对象类型(Mutex),则在进程B的句柄表中找到一个 empty entry,然后将其初始化,指向现有的内核对象。(这一步中不保留句柄值,因为可能B进程中已经存在了一个相同的句柄值。应该新生成一个句柄值)。同样,这个被共享的内核对象的使用计数会递增。
如果类型不匹配,或者内核对象的创建者不允许访问,则CreateMutex()
函数就会执行失败,然后返回NULL。
也可以使用Open*
函数来打开一个命名内核对象。
复制对象句柄
使用DuplicateHandle
函数。
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle,
HANDLE hSourceHandle,
HANDLE hTargetProcessHandle,
PHANDLE phTargetHandle,
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD DwOptions
);
第1、3个参数是标识进程内核对象的句柄,分别为源进程和目标进程。该函数会将源进程中的句柄信息复制到目标进程中,即第二个参数hSourceHandle
。
第4个参数phTargetHandle
是一个HANDLE变量的地址,用来保存复制后得到的HANDLE值。
第五个参数dwDesiredAccess
指定访问掩码,如指定了DwOptions
为DUPLICATE_SAME_ACCESS
,则这一项会被忽略.
bInheritHandle
控制是否能够继承。
当执行完DuplicateHandle
,目标进程同样不知道自己拥有了一个内核对象。但是不能使用命令行,因为它已经启动了。所以需要通过其他进程间的通信机制通知它。
上一篇: python音频处理用到的操作的示例代码