win32基础-句柄和内核对象
win32基础-句柄和内核对象
笔者最近在通过白嫖一个 古老的网课 学习 win32 的基础,老师讲的很清晰,于是整理了一下句柄和内核对象部分。
什么是内核对象?
像进程、线程、文件、互斥体、事件等对象,在内核都有一个对应的、由内核负责管理的结构体,这就是内核对象。
有哪些内核对象呢?我们可以看一看
CloseHandle这个函数可以关闭哪些对象的句柄:
是的,这些对象都有相应的内核对象。

如何管理内核对象?
创建时返回内核对象的地址如何?
- 不行。用内核地址访问内核对象,若访问内核访问地址错误,连报错都没有直接就蓝屏了。我们可以通过表的方式来解决。
每个进程都有一个句柄表
句柄表中就是句柄和内核对象的一种映射关系。
想对内核对象做任何事都必须通过句柄。
这里也可以看出句柄的作用:是一道挡在 ring0 和 ring3 之间的防火墙,让用户层的程序只能通过句柄来访问内核对象。

实例
BOOL CreateChildProcess(PTCHAR szChildProcessName, PTCHAR szCommandLine)
{
//指定创建时进程的主窗口的窗口站、桌面、标准句柄和外观。
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
si.cb = sizeof(si);
if (!CreateProcess(
szChildProcessName,//进程名(完整的路径名+程序名)
szCommandLine,//命令行
NULL,//不继承进程句柄
NULL,//不继承线程句柄
FALSE,//不继承句柄
0,//没有创建标志
NULL,//使用父进程环境变量
NULL,//使用父进程工作目录为当前目录
&si,//[in]STARTUPINFO结构体
&pi//[out]LPPROCESS_INFORMATION
))
{
printf("CreateChildProcess Error:%d\n", GetLastError());
return FALSE;
}
//好像对msedge没用?
//对其他的程序还是有用的,比如exeinfo
SuspendThread(pi.hThread);
ResumeThread(pi.hThread);
//释放句柄
printf("ProcessId:%d\n", pi.dwProcessId);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return TRUE;
}
代码中对线程的操作(挂起和恢复)都是通过线程的句柄实现的。
多进程可以共享一个内核对象吗?
可以!
内核对象被不同的n个进程共享,其内部的计数器就为n。当计数器为 0 时,没有任何句柄指向,该内核对象被销毁。
CloseHandle 函数仅仅会将该内核对象的计数器减一,并不会让其直接被销毁。

例外:线程和进程这种内核对象仅仅是计数器为零是不会被销毁的,还必须要关闭这个线程(进程则是关闭其中的所有线程)
MSDN原文:
Closing a thread handle does not terminate the associated thread or remove the thread object. Closing a process handle does not terminate the associated process or remove the process object. To remove a thread object, you must terminate the thread, then close all handles to the thread. For more information, see Terminating a Thread. To remove a process object, you must terminate the process, then close all handles to the process. For more information, see Terminating a Process.
当然句柄表是私有的,A 进程的句柄的值对 B 进程来讲没有意义。如上图中 A 进程中,指向 A 这个进程内核对象的句柄值是 1;但是 B 进程中,打开 A 进程之后指向 A 这个进程内核对象的句柄值是 12 。
内核对象的句柄值可继承吗?
可以,不如说只有内核对象的句柄值才是可继承的,这也是进程间共享内核对象的一种方式。
刚刚我们说:“句柄表是私有的,A 进程的句柄值对 B 进程来讲没有意义”,但是如果我们可以从父进程那里继承句柄表,那么子进程就可以直接用父进程的句柄值来访问父进程创建的内核对象!
如我们观察 CreateEventA 函数原型:
HANDLE CreateEventA(
[in, optional] LPSECURITY_ATTRIBUTES lpEventAttributes,
[in] BOOL bManualReset,
[in] BOOL bInitialState,
[in, optional] LPCSTR lpName
);
其中的参数 LPSECURITY_ATTRIBUTES ,所谓的安全描述符指针,只有在创建内核对象时才可以给这个参数。
安全描述符结构体 _SECURITY_ATTRIBUTES 的定义如下:
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SE
第一个成员是结构体长度,为可拓展做准备的,不用管;
第二个成员是访问权限,一般传 NULL 和父进程访问权限一样即可,也不用管;
第三个成员是否允许继承,这是我们主要想看的。假如我们设置其为 TRUE ,就说明这个内核对象的句柄值是可以继承的。
事实上,我们可以将进程的句柄表看做如下的一张表(其中 1 表示可继承):
| 是否可继承 | 句柄值 | 内核对象地址 |
|---|---|---|
| 1 | 0 | 0xAAAAAAAA |
| 0 | 1 | 0xBBBBBBBB |
| 1 | 2 | 0xCCCCCCCC |
| ... | ... | ... |

但是子进程是如何继承句柄表这些允许继承的部分的呢?
也许这时我们应该再回去看一看 CreateProcess 这个超长函数的原型:
BOOL CreateProcessA(
[in, optional] LPCSTR lpApplicationName,
[in, out, optional] LPSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCSTR lpCurrentDirectory,
[in] LPSTARTUPINFOA lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);
这里第三个参数 lpProcessAttributes 和第四个参数 lpThreadAttributes 都是 LPSECURITY_ATTRIBUTES 类型的,分别是指明当前进程(调用这个函数的进程)正在创建的子进程和子进程中的线程,这两个内核对象的句柄值,是否允许当前进程的所有子进程继承。
第五个参数 bInheritHandles 则不同,它是用来决定正在创建的这个子进程,是否继承当前进程的句柄表中允许继承的部分。
一定要注意前两者和后者是完全不同的。
如我们当前进程只想创建这一个子进程,那么前两者传 NULL 即可,因为这个子进程可以直接获得自己的全部信息(不仅仅是进程或线程的句柄,要什么有什么),完全没必要从父进程那里继承自己的句柄,那样就太搞笑了。
但是如果这个子进程想要继承父进程的其他内核对象的句柄表,那么 bInheritHandles 就一定要传 TRUE 。


浙公网安备 33010602011771号