第一部分,必备知识

第3章 内核对象

概述: 内核对象是Windows操作系统用来管理进程、线程、文件等系统资源的数据结构。它们由内核分配和拥有,通过句柄访问,具有安全描述符、使用计数等特性,是理解Windows编程的核心概念。

什么是内核对象

内核对象是操作系统内核分配的一块内存,由内核拥有和管理,而不是由应用程序拥有。每个内核对象都有一个安全描述符(描述谁可以访问该对象)和一个使用计数(记录有多少个进程正在使用该对象)。常见的内核对象包括进程、线程、文件、事件、信号量、互斥量等。应用程序无法直接访问内核对象的内存,只能通过Windows提供的API和返回的句柄来操作这些对象。

//常见的内核对象创建函数,它们都返回HANDLE类型的句柄

//创建进程内核对象
HANDLE CreateProcess(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes, //安全属性
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation //返回进程和线程句柄
);

//创建线程内核对象
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, //安全属性
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);

//创建事件内核对象
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性
BOOL bManualReset,
BOOL bInitialState,
LPCTSTR lpName
);

//创建文件内核对象
HANDLE CreateFile(
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes, //安全属性
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);

内核对象的使用计数和生命周期

内核对象的生命周期由使用计数(usage count)控制。当创建一个内核对象时,使用计数被设为1。当另一个进程获得该对象的访问权时,使用计数递增;当进程不再需要该对象时,应调用CloseHandle递减使用计数。当使用计数变为0时,内核会销毁该对象并释放内存。即使创建该对象的进程终止,只要其他进程还在使用该对象,对象就不会被销毁。这种机制确保了内核对象可以在进程间共享。

//内核对象的使用计数管理

//创建事件对象,使用计数 = 1
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (hEvent == NULL) {
//创建失败处理
DWORD dwError = GetLastError();
}

//打开已存在的命名事件对象(使用计数递增)
HANDLE hEvent2 = OpenEvent(EVENT_ALL_ACCESS, FALSE, TEXT("MyEvent"));
if (hEvent2 != NULL) {
//现在该事件对象的使用计数 = 2
}

//当不再需要内核对象时,必须调用CloseHandle关闭句柄
//这会递减使用计数,如果计数变为0,内核销毁该对象
BOOL CloseHandle(HANDLE hObject);

//使用示例
if (!CloseHandle(hEvent)) {
//关闭失败(极少发生)
DWORD dwError = GetLastError();
}

//注意:关闭句柄后,hEvent仍然包含原来的值,但已成为无效句柄
//不应再使用,最好将其设为NULL避免误用
hEvent = NULL;

/*
重要提示:
1. 关闭句柄不会强制终止关联的进程或线程
2. 进程终止时,系统会自动关闭该进程打开的所有句柄
3. 但显式关闭句柄是良好的编程习惯,避免资源泄漏
4. 句柄泄漏会导致使用计数永不归零,内核对象永远无法释放
*/

内核对象的安全性

每个内核对象都有一个安全描述符(SECURITY_ATTRIBUTES结构),用于指定谁可以访问该对象以及拥有什么权限。安全描述符包含所有者的SID(安全标识符)、DACL(自主访问控制列表,指定允许或拒绝哪些用户的访问)和SACL(系统访问控制列表,用于审计)。创建内核对象时可以通过lpSecurityAttributes参数指定安全属性,传入NULL则使用默认安全属性(仅创建者拥有完全访问权限)。

//安全属性结构
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength; //结构体大小
LPVOID lpSecurityDescriptor; //安全描述符
BOOL bInheritHandle; //句柄是否可继承
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES;

//创建带有安全描述符的内核对象示例
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = pSD; //指向安全描述符的指针
sa.bInheritHandle = FALSE; //句柄不可继承

HANDLE hEvent = CreateEvent(&sa, FALSE, FALSE, TEXT("SecureEvent"));

/*
访问权限检查:
当线程尝试访问内核对象时,系统会检查线程的访问令牌和对象的安全描述符。
如果DACL允许访问,则操作成功;否则,访问被拒绝,函数返回失败并设置
LastError为ERROR_ACCESS_DENIED。

常见内核对象访问权限:
- 进程:PROCESS_ALL_ACCESS, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE等
- 线程:THREAD_ALL_ACCESS, THREAD_SUSPEND_RESUME, THREAD_TERMINATE等
- 文件:GENERIC_READ, GENERIC_WRITE, GENERIC_EXECUTE, GENERIC_ALL等
*/

跨进程共享内核对象

Windows提供了三种机制在不同进程间共享内核对象:句柄继承、命名对象和复制句柄。句柄继承允许子进程继承父进程的句柄表中的可继承句柄。命名对象通过给对象指定全局唯一的名称,其他进程可以通过OpenXxx函数按名称打开。复制句柄则使用DuplicateHandle函数将一个进程的句柄复制到另一个进程。这些方法使得进程间可以安全地共享和同步资源。

//方法一:句柄继承
//父进程创建可继承的句柄
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE; //关键:设置为可继承

HANDLE hEvent = CreateEvent(&sa, FALSE, FALSE, NULL);

//创建子进程时,设置bInheritHandles为TRUE
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
CreateProcess(
TEXT("Child.exe"),
NULL,
NULL,
NULL,
TRUE, //继承父进程句柄
0,
NULL,
NULL,
&si,
&pi
);

//子进程通过命令行或共享内存获取继承的句柄值
//子进程知道hEvent的句柄值(例如通过命令行传递的整数值)
HANDLE hInheritedEvent = (HANDLE)atoi(argv[1]);

//方法二:命名对象
//进程A创建命名事件
HANDLE hNamedEvent = CreateEvent(NULL, FALSE, FALSE, TEXT("Global\\MyEvent"));
//如果对象已存在,GetLastError返回ERROR_ALREADY_EXISTS

//进程B打开已存在的命名事件
HANDLE hNamedEvent2 = OpenEvent(EVENT_ALL_ACCESS, FALSE, TEXT("Global\\MyEvent"));

//方法三:复制句柄
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle, //源进程句柄
HANDLE hSourceHandle, //要复制的源句柄
HANDLE hTargetProcessHandle, //目标进程句柄
LPHANDLE lpTargetHandle, //接收复制后的句柄
DWORD dwDesiredAccess, //目标句柄的访问权限
BOOL bInheritHandle, //目标句柄是否可继承
DWORD dwOptions //选项
);

//使用DuplicateHandle将句柄从进程A复制到进程B
HANDLE hProcessB = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessIdB);
HANDLE hEventInProcessB;
DuplicateHandle(
GetCurrentProcess(), //源进程(当前进程)
hEvent, //源句柄
hProcessB, //目标进程
&hEventInProcessB, //目标句柄
0,
FALSE,
DUPLICATE_SAME_ACCESS //保持相同访问权限
);
//然后通过某种IPC机制(如消息、共享内存)将hEventInProcessB的值告诉进程B

总结:

W内核对象是Windows操作系统管理资源的核心机制,由内核拥有并通过句柄访问。每个内核对象都有安全描述符和使用计数,确保安全和正确的生命周期管理。使用内核对象时必须通过CloseHandle关闭句柄以递减使用计数,避免资源泄漏。进程间可以通过句柄继承、命名对象和DuplicateHandle函数共享内核对象。理解内核对象是后续学习进程、线程、同步等高级主题的基础。