第2章:对象和句柄
Windows 是一个基于对象(Object-based)的操作系统,提供了大量内核对象(Kernel Object)类型来承载其大部分功能。进程、线程和文件就是对象类型的几个例子。本章介绍关于内核对象的通用理论,而不深入讨论任何特定的对象类型。
内核对象
Windows 内核暴露了多种对象类型,供用户模式(User-mode)进程、内核自身以及内核模式(Kernel-mode)驱动程序使用。这些对象类型实例是系统(内核)空间中的数据结枟,由对象管理器(Object Manager,执行体的一部分)根据用户模式或内核模式代码的请求创建和管理。内核对象采用引用计数(Reference Counting)机制,因此只有当指向对象的最后一个引用被释放时,对象才会被销毁并从内存中移除。
Windows 内核支持大量对象类型。你可以使用 Sysinternals 的 WinObj 工具(以管理员身份运行),找到 ObjectTypes 目录来查看它们。这些对象类型按其可见性和用途可以分为以下几类:
- 通过 Windows API 导出到用户模式的类型:互斥体(Mutex)、信号量(Semaphore)、文件(File)、进程(Process)、线程(Thread)、定时器(Timer)等。
- 在 Windows Driver Kit(WDK)中为设备驱动程序编写者记录的类型,但不向用户模式导出:设备(Device)、驱动程序(Driver)、回调(Callback)。
- 即使在 WDK 中也未记录的类型(截至本文撰写时),仅供内核内部使用:分区(Partition)、键控事件(Keyed Event)、核心消息传输(Core Messaging)。
由于内核对象驻留在系统空间中,用户模式无法直接访问它们。应用程序必须使用一种称为句柄(Handle)的间接访问机制。句柄提供了以下几个优点:
- 对象类型数据结构的任何未来变化都不会影响客户端。
- 可以通过安全检查(Security Access Check)来控制对对象的访问。
- 句柄是进程私有的(Process-private)—— 在某个进程的上下文中指向某个对象的句柄,在另一个进程的上下文中没有任何意义。
内核对象是引用计数的。对象管理器维护一个句柄计数(Handle Count)和一个指针计数(Pointer Count),它们的总和等于总引用计数。一旦用户模式客户端不再需要某个对象,就应该调用 CloseHandle 来关闭句柄。之后,代码应认为该句柄无效。尝试通过已关闭的句柄访问对象将会失败,GetLastError 返回 ERROR_INVALID_HANDLE(6)。通常,客户端不知道对象是否已被销毁。如果引用计数降为零,对象管理器就会删除该对象。
句柄值(Handle Value)是 4 的倍数,第一个有效的句柄是 4。0 永远不是一个有效的句柄值。这个机制在 64 位系统上没有变化。
从逻辑上讲,句柄相当于进程中维护的句柄表(Handle Table)中的一个条目数组的索引,该句柄表逻辑上指向位于系统空间中的内核对象。存在各种 Create* 和 Open* 函数用于创建/打开对象并获取句柄。如果创建或打开失败,返回的句柄通常是 NULL(0)。一个显著的例外是 CreateFile,它在失败时返回 INVALID_HANDLE_VALUE(-1)。
例如,CreateMutex 允许创建一个新的互斥体或按名称打开一个现有的互斥体。成功时,它返回该互斥体的句柄。返回值为零意味着句柄无效且调用失败。反之,OpenMutex 尝试打开一个具有指定名称的互斥体的句柄——如果该互斥体不存在,调用就会失败。
如果函数成功并且提供了名称,返回的句柄可能指向一个新创建的对象,也可能指向一个同名的已有对象。代码可以通过调用 GetLastError 并与 ERROR_ALREADY_EXISTS 比较来检查。如果相等,说明这不是一个新对象,而是指向一个已有对象的另一个句柄。
运行单实例进程
ERROR_ALREADY_EXISTS 场景的一个常见用途是将可执行程序限制为单个进程实例。该技术使用一个有名称的内核对象(通常是互斥体,尽管任何可命名的对象类型都可以),以一个特定的名称创建。如果该对象已存在,说明另一个实例正在运行,因此当前进程可以退出。
"SingleInstance" 演示应用程序(使用 WTL 构建为基于对话框的应用程序)展示了这一点。如果启动多个实例,第一个窗口会记录来自新实例的消息,然后新实例退出。
WinMain 函数——创建互斥体:
HANDLE hMutex = ::CreateMutex(nullptr, FALSE, L"SingleInstanceMutex");
if (!hMutex) {
CString text;
text.Format(L"Failed to create mutex (Error: %d)", ::GetLastError());
::MessageBox(nullptr, text, L"Single Instance", MB_OK);
return 0;
}创建互斥体失败的情况应该极其罕见。最可能的原因是有另一个具有相同名称的内核对象(且该对象不是互斥体)存在。
检查互斥体是否已经存在:
if (::GetLastError() == ERROR_ALREADY_EXISTS) {
NotifyOtherInstance();
return 0;
}如果该对象在 CreateMutex 调用之前就已存在,一个辅助函数会向现有实例发送一条消息,然后该进程退出。
NotifyOtherInstance 函数:
#define WM_NOTIFY_INSTANCE (WM_USER + 100)
void NotifyOtherInstance() {
auto hWnd = ::FindWindow(nullptr, L"Single Instance");
if (!hWnd) {
::MessageBox(nullptr, L"Failed to locate other instance window",
L"Single Instance", MB_OK);
return;
}
::PostMessage(hWnd, WM_NOTIFY_INSTANCE, ::GetCurrentProcessId(), 0);
::ShowWindow(hWnd, SW_NORMAL);
::SetForegroundWindow(hWnd);
}该函数使用 FindWindow 按窗口标题搜索——并不理想,但在这个例子中足够用了。它发送一条带有当前进程 ID 的自定义消息。
CMainDlg 中的消息映射(来自 MainDlg.h):
BEGIN_MSG_MAP(CMainDlg)
MESSAGE_HANDLER(WM_NOTIFY_INSTANCE, OnNotifyInstance)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
COMMAND_ID_HANDLER(IDCANCEL, OnCancel)
END_MSG_MAP()OnNotifyInstance 处理函数:
LRESULT CMainDlg::OnNotifyInstance(UINT, WPARAM wParam, LPARAM, BOOL &) {
CString text;
text.Format(L"Message from another instance (PID: %d)", wParam);
AddText(text);
return 0;
}AddText 辅助函数:
void CMainDlg::AddText(PCWSTR text) {
CTime dt = CTime::GetCurrentTime();
m_List.AddString(dt.Format(L"%T") + L": " + text);
}m_List 是一个 CListBox,WTL 对 Windows 列表框控件的封装。
句柄
句柄间接地指向内核空间中的一个小数据结构,该结构存储了该句柄的相关信息。每个句柄条目包含以下内容:
- 指向实际对象的指针——在 32 位系统上,对象地址是 8 的倍数;在 64 位系统上是 16 的倍数(低位用于标志位,对齐有利于提高 CPU 访问速度)。
- 访问掩码(Access Mask)——指示使用该句柄可以执行哪些操作(决定了句柄的权限)。
- 三个标志位:继承标志(Inheritance Flag)、防止关闭标志(Protect-from-close Flag)和关闭时审计标志(Audit-on-close Flag)。
访问掩码是一个位掩码,其中每个为 "1" 的位表示允许使用该句柄执行的某个特定操作。访问掩码在创建对象或打开已有对象以创建句柄时设置。如果是创建对象,调用者通常会获得完整访问权限。如果是打开对象,调用者会指定所需的访问掩码,但未必能获得。
例如,要终止一个进程,应用程序必须调用 OpenProcess,并(至少)带 PROCESS_TERMINATE 访问权限,以获取该进程的句柄。
按 PID 终止进程的例子:
bool KillProcess(DWORD pid) {
HANDLE hProcess = ::OpenProcess(PROCESS_TERMINATE, FALSE, pid);
if (!hProcess)
return false;
BOOL success = ::TerminateProcess(hProcess, 1);
::CloseHandle(hProcess);
return success != FALSE;
}OpenProcess 的原型:
HANDLE OpenProcess(
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ DWORD dwProcessId
);由于这是一次打开操作,对象已经存在,客户端需要指定所需的访问掩码。访问掩码有两大类访问位:通用访问位(Generic Access Bits)和特定访问位(Specific Access Bits,详见第 16 章)。上面的例子使用了 PROCESS_TERMINATE。其他可用的位包括 PROCESS_QUERY_INFORMATION、PROCESS_VM_OPERATION 等。
客户端代码应该只请求它打算对对象执行的操作所需的权限。请求比需要的更多可能会失败;请求更少则显然会不够用。
句柄标志
- 继承(Inheritance):用于句柄继承,这是一种在协作进程之间共享对象的机制(将在第 3 章讨论)。
- 关闭时审计(Audit on close):指示在关闭此句柄时是否应将一条审计条目写入安全日志。很少使用,默认关闭。
- 防止关闭(Protect from close):阻止句柄被关闭。
CloseHandle返回FALSE,GetLastError返回ERROR_INVALID_HANDLE(6)。在调试器下,会引发一个异常,消息为:"0xC0000235: NtClose was called on a handle that was protected from close via NtSetInformationObject"。很少有用。
SetHandleInformation 函数:
#define HANDLE_FLAG_INHERIT 0x00000001
#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002
BOOL SetHandleInformation(
_In_ HANDLE hObject,
_In_ DWORD dwMask,
_In_ DWORD dwFlags
);设置"防止关闭"位:
::SetHandleInformation(h, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);清除同一位:
::SetHandleInformation(h, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0);GetHandleInformation 函数:
BOOL GetHandleInformation(
_In_ HANDLE hObject,
_Out_ LPDWORD lpdwFlags
);Process Explorer 的句柄视图
Sysinternals 的 Process Explorer 工具可以显示特定进程中已打开的句柄。导航到你感兴趣的进程,确保下方窗格可见(View 菜单 -> Show Lower Pane),然后切换到 "Handles" 视图。
各列说明:
- Handle:句柄值本身,仅对该进程有意义。相同的值可能指向不同的对象或为空。
- Type:对象类型名称(对应 WinObj 的 Object Types 目录)。
- Object Address:实际对象结构的内核地址(64 位系统上以零的十六进制数字结尾,32 位系统上以 "8" 或 "0" 结尾)。对于调试很有用——如果两个句柄指向相同的地址,它们引用的就是同一个对象。
- Access:访问掩码,以十六进制值显示。
- Decoded Access:常见对象类型的访问掩码位的字符串表示(作者个人为 Process Explorer 实现了这一列)。
默认情况下,Process Explorer 只显示有名称对象的句柄。要查看所有句柄,请在 View 菜单中启用 "Show Unnamed Handles and Mappings"。
这里"名称"(Name)这个术语比看起来更复杂。Process Explorer 中的"有名称"对象包括:
- Process 和 Thread 对象:以其唯一 ID 显示。
- File 对象:显示文件名或文件对象所指向的设备名称——并非真正的名称。
- Key 对象:显示注册表键路径——并非真正的名称。
- Directory 对象:显示逻辑路径,并非真正意义上的对象名称。
- Token 对象:显示存储在令牌中的用户名。
在 WinObj 中浏览时,不会出现文件或键对象,这印证了这些对象确实不能有名称。
句柄计数
进程句柄表中的句柄总数可以在 Process Explorer 和任务管理器(Task Manager)中作为列显示。请注意,显示的数字是句柄计数(Handle Count)而非对象计数(Object Count),因为多个句柄可以引用同一个对象。
在 Process Explorer 中双击某个句柄条目会打开一个对话框,显示对象属性(而非句柄属性)。它显示了基本的对象信息,与句柄条目中的数据重复。此外还会显示该对象有多少个打开的句柄以及引用计数。
内核对象查看器
作者的 "KernelObjectView" 工具(可从 https://github.com/zodiacon/AllTools 获取)可以在给定时刻显示对象和句柄计数。它会按对象类型显示总对象数和总句柄数,可按任意列排序。
伪句柄
某些句柄具有特殊值且不能被关闭,这些被称为伪句柄(Pseudo-Handles)。对伪句柄调用 CloseHandle 总是会失败。返回伪句柄的函数包括:
GetCurrentProcess()——返回调用进程的伪句柄(值 -1)GetCurrentThread()——返回调用线程的伪句柄(值 -2)GetCurrentProcessToken()——返回调用进程令牌的伪句柄(值 -4)GetCurrentThreadToken()——返回调用线程令牌的伪句柄(值 -5)GetCurrentThreadEffectiveToken()——返回调用线程有效令牌的伪句柄(值 -6,如果线程令牌可用则使用,否则使用进程令牌)
后三个(令牌伪句柄)仅在 Windows 8 及更高版本上受支持,其访问掩码仅限于 TOKEN_QUERY 和 TOKEN_QUERY_SOURCE。
句柄的 RAII
及时关闭不再使用的句柄至关重要。不这样做会导致句柄泄漏(Handle Leak),句柄数量会不受控制地增长。
C++ 的 RAII(Resource Acquisition Is Initialization,资源获取即初始化)惯用法有助于管理句柄。其核心思想:将句柄封装在一个类型中,当该封装的析构函数被调用时,确保句柄被关闭。
一个简单的 RAII 句柄封装:
struct Handle {
explicit Handle(HANDLE h = nullptr) : _h(h) {}
~Handle() { Close(); }
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
Handle(Handle&& other) : _h(other._h) {
other._h = nullptr;
}
Handle& operator=(Handle&& other) {
if (this != &other) {
Close();
_h = other._h;
other._h = nullptr;
}
return *this;
}
operator bool() const {
return _h != nullptr && _h != INVALID_HANDLE_VALUE;
}
HANDLE Get() const {
return _h;
}
void Close() {
if (_h) {
::CloseHandle(_h);
_h = nullptr;
}
}
private:
HANDLE _h;
};拷贝构造函数和拷贝赋值运算符被删除,因为复制一个可能有多个拥有者的句柄会产生问题。移动操作转移所有权。bool 操作符将 0 和 INVALID_HANDLE_VALUE 都视为无效。
使用示例:
Handle hMyEvent(::CreateEvent(nullptr, TRUE, FALSE, nullptr));
if (!hMyEvent) {
return;
}
::SetEvent(hMyEvent.Get());
Handle hOtherEvent(std::move(hMyEvent));
::ResetEvent(hOtherEvent.Get());虽然编写这样一个封装是可行的,但使用已有的库往往更好。
使用 WIL
微软的 Windows Implementation Library(WIL)提供了类似功能,可在 GitHub 上作为 NuGet 包获取。
将 WIL 添加到一个项目中,就像添加任何 NuGet 包一样。在 Visual Studio 中,右键点击 "References",选择 "Manage NuGet Packages...",搜索 "wil"。完整的包名是 "Microsoft.Windows.ImplementationLibrary"。
RAII 句柄封装位于 <wil/resource.h> 中。
使用 WIL 的相同代码:
#include <wil/resource.h>
void DoWork() {
wil::unique_handle hMyEvent(::CreateEvent(nullptr, TRUE, FALSE, nullptr));
if (!hMyEvent) {
return;
}
::SetEvent(hMyEvent.get());
auto hOtherEvent(std::move(hMyEvent));
::ResetEvent(hOtherEvent.get());
}wil::unique_handle 封装了 HANDLE,并在析构时调用 CloseHandle,其设计参照了 std::unique_ptr<>。内部的 HANDLE 通过 get() 访问。要替换值(并关闭旧句柄),使用 reset;调用不带参数的 reset() 会关闭句柄并清空封装。
创建对象
用于创建新对象的函数共享一些常见的参数。CreateMutex 和 CreateEvent 可作为示例:
HANDLE CreateMutex(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
_In_ BOOL bInitialOwner,
_In_opt_ LPCTSTR lpName);
HANDLE CreateEvent(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
_In_ BOOL bManualReset,
_In_ BOOL bInitialState,
_In_opt_ LPCTSTR lpName);两者都接受一个 SECURITY_ATTRIBUTES(安全属性)参数,这是所有创建函数共有的:
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES;nLength 应设置为结构体的大小——这是一种常见的版本控制技术。SECURITY_ATTRIBUTES 结构体自第一个 Windows NT 版本以来一直未发生变化。
lpSecurityDescriptor 可以指向一个安全描述符(Security Descriptor)对象,指定谁可以对对象执行哪些操作(详见第 16 章)。
bInheritHandle 是继承位,允许设置继承而无需调用 SetHandleInformation。
设置继承来创建事件的例子:
SECURITY_ATTRIBUTES sa = { sizeof(sa) };
sa.bInheritHandle = TRUE;
HANDLE hEvent = ::CreateEvent(&sa, TRUE, FALSE, nullptr);
DWORD flags;
::GetHandleInformation(hEvent, &flags); // 设置 flags = 1将 SECURITY_ATTRIBUTES 设置为 NULL 会使继承位保持清除状态。从安全角度来看,NULL 意味着"默认安全",基于进程访问令牌中的安全描述符(详见第 16 章)。
对象名称
某些对象类型可以具有基于字符串的名称。这些名称允许通过相应的 Open 函数按名称打开对象。并非所有对象都有名称——进程和线程改用 ID,因此 OpenProcess 和 OpenThread 需要数字标识符。
当一个创建函数以名称调用时:
- 如果不存在具有该名称的对象,则创建一个新对象。
- 如果已存在具有该名称的对象,则打开现有对象。
- 在后一种情况下,
GetLastError返回ERROR_ALREADY_EXISTS,且影响创建的参数(如SECURITY_ATTRIBUTES)被忽略。
提供给创建函数的名称并非最终名称。在经典(桌面)进程中,名称会被加上前缀 \Sessions\x\BaseNamedObjects\,其中 x 是调用者的会话 ID。如果会话 ID 为零,前缀为 \BaseNamedObjects\。对于在应用容器(App Container,通常是 UWP 进程)中的进程,前缀更为复杂,包含一个唯一的应用容器 SID:\Sessions\x\AppContainerNamedObjects\。
对象名称是会话相关(Session-relative)的(对于应用容器则是包相关的)。要进行跨会话共享,请在会话 0 中创建对象,并使用 Global\ 前缀——例如 Global\MyMutex。应用容器不能使用会话 0 的对象命名空间。
WinObj 展示了完整的对象管理器命名空间层次结构。这个完整的结构驻留在内存中,由对象管理器根据需要操作。无名称对象不属于这个结构,因此 WinObj 只显示以名称创建的对象。
WinObj 中显示的 "Directories" 实际上是目录对象(Directory Objects)——作为逻辑容器的内核对象。
共享内核对象
内核对象句柄是进程私有的。一个进程不能简单地将一个句柄值传递给另一个进程,因为在目标进程的句柄表中,这个值可能指向一个不同的对象,或者为空。
共有三种共享机制:
- 通过名称共享
- 通过句柄继承共享——将在第 3 章讨论
- 通过复制句柄共享
通过名称共享
当可行时(意味着对象能够并且确实有名称),这是最简单的选择。协作进程使用相同的对象名称调用相应的创建函数。第一个调用者创建对象,后续调用打开指向同一对象的额外句柄。
"BasicSharing" 示例通过一个用于在进程间共享内存的内存映射文件(Memory-mapped File)对象来演示这一点。运行两个或更多实例可以允许在进程之间共享文本数据。
在 Process Explorer 中,查看 Section 对象(内存映射文件的内核名称),会出现一个名为 "MySharedMemory" 的 section(带有基于会话的前缀)。该对象有两个打开的句柄。共享内存大小为 4KB。两个进程指向同一个对象(相同的 Object Address),但句柄值不同。
创建文件映射(在 WM_INITDIALOG 中):
m_hSharedMem = ::CreateFileMapping(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE,
0, 1 << 12, L"MySharedMemory");
if (!m_hSharedMem) {
AtlMessageBox(m_hWnd, L"Failed to create/open shared memory", IDR_MAINFRAME);
EndDialog(IDCANCEL);
}最后一个参数是对象名称。如果这是第一个进程,对象会被创建。后续调用会产生指向同一对象的额外句柄。大小设置为 4KB(1 << 12)。
在 WM_DESTROY 上关闭句柄:
if (m_hSharedMem)
::CloseHandle(m_hSharedMem);写入共享内存:
void* buffer = ::MapViewOfFile(m_hSharedMem, FILE_MAP_WRITE, 0, 0, 0);
if (!buffer) {
AtlMessageBox(m_hWnd, L"Failed to map memory", IDR_MAINFRAME);
return 0;
}
CString text;
GetDlgItemText(IDC_TEXT, text);
::wcscpy_s((PWSTR)buffer, text.GetLength() + 1, text);
::UnmapViewOfFile(buffer);从共享内存中读取:
void* buffer = ::MapViewOfFile(m_hSharedMem, FILE_MAP_READ, 0, 0, 0);
if (!buffer) {
AtlMessageBox(m_hWnd, L"Failed to map memory", IDR_MAINFRAME);
return 0;
}
SetDlgItemText(IDC_TEXT, (PCWSTR)buffer);
::UnmapViewOfFile(buffer);通过句柄复制共享
对于没有名称或不能有名称的对象,复制句柄可能是解决方案。这种方法没有固有的限制(安全方面除外),几乎适用于任何内核对象,无论是命名的还是未命名的,在任何时间都可以使用。它的缺点是:这是实现难度最高的共享方法。
DuplicateHandle 函数:
BOOL DuplicateHandle(
_In_ HANDLE hSourceProcessHandle,
_In_ HANDLE hSourceHandle,
_In_ HANDLE hTargetProcessHandle,
_Outptr_ LPHANDLE lpTargetHandle,
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ DWORD dwOptions);复制需要一个源进程、一个源句柄和一个目标进程。成功时,一个新的句柄条目会被写入目标进程的句柄表中,指向与源句柄相同的对象。
参数详解:
hSourceProcessHandle:源进程的句柄,需要PROCESS_DUP_HANDLE访问权限。可使用GetCurrentProcess()表示调用者的进程。hSourceHandle:要复制的源句柄,在源进程上下文中有效。hTargetProcessHandle:目标进程的句柄,同样需要PROCESS_DUP_HANDLE访问权限。lpTargetHandle:结果句柄(在目标进程中有效)。dwDesiredAccess:新句柄的访问掩码(如果设置了DUPLICATE_SAME_ACCESS,此参数将被忽略)。bInheritHandle:新句柄是否可继承。dwOptions:标志位——DUPLICATE_SAME_ACCESS和DUPLICATE_CLOSE_SOURCE(复制后关闭源句柄,不增加句柄计数)。
简单的进程内复制示例:
HANDLE hJob = ::CreateJobObject(nullptr, nullptr);
HANDLE hJob2;
::DuplicateHandle(::GetCurrentProcess(), hJob, ::GetCurrentProcess(), &hJob2,
JOB_OBJECT_ASSIGN_PROCESS | JOB_OBJECT_TERMINATE, FALSE, 0);在 Process Explorer 中可以看到差异:一个句柄有完整访问权限,复制后的句柄只有指定的访问掩码。
复制到另一个进程:
HANDLE DuplicateToProcess(HANDLE hSource, DWORD pid) {
HANDLE hProcess = ::OpenProcess(PROCESS_DUP_HANDLE, FALSE, pid);
if (!hProcess)
return nullptr;
HANDLE hTarget = nullptr;
::DuplicateHandle(::GetCurrentProcess(), hSource, hProcess,
&hTarget, 0, FALSE, DUPLICATE_SAME_ACCESS);
::CloseHandle(hProcess);
return hTarget;
}复杂性不在于复制本身,而在于如何向目标进程传达何时复制了句柄以及新句柄的值是多少。调用者知道创建的句柄值,但目标进程不知道。必须通过另一种进程间通信(IPC)方式来传达这些信息。
私有对象命名空间
有名称的内核对象有一些弊端:
- 不相关的进程可能会创建具有相同名称的对象,导致失败或意外的对象共享。
- 名称是可见的(在工具中和通过编程方式),因此另一个进程可以"劫持"或干扰该对象。无名对象则要隐蔽得多。
自 Windows Vista 起,引入了私有对象命名空间(Private Object Namespaces),允许只有合作的进程才能知道某个对象的完整名称。工具和 API 无法揭示其完整名称。
"PrivateSharing" 示例在 "BasicSharing" 的基础上进行了增强——内存映射文件对象的名称现在位于一个私有命名空间中。Process Explorer 只会显示名称的一部分。随意尝试查找 "MySharedMem" 的代码将无法找到它。
创建私有命名空间
步骤 1:创建边界描述符(Boundary Descriptor)
HANDLE CreateBoundaryDescriptor(
_In_ LPCTSTR Name,
_In_ ULONG Flags); // 当前未使用有两个函数用于限制通过该描述符创建的私有命名空间的访问:
BOOL AddSIDToBoundaryDescriptor(
_Inout_ HANDLE* BoundaryDescriptor,
_In_ PSID RequiredSid);
BOOL AddIntegrityLabelToBoundaryDescriptor(
_Inout_ HANDLE* BoundaryDescriptor,
_In_ PSID IntegrityLabel);两者都接受边界描述符句柄的地址和一个 SID。AddSIDToBoundaryDescriptor 通常使用一个组 SID,允许该组中的所有用户访问命名空间。AddIntegrityLabelToBoundaryDescriptor 设置一个最低完整性级别(Integrity Level),用于打开命名空间中的对象的进程。
步骤 2:创建私有命名空间
HANDLE CreatePrivateNamespace(
_In_opt_ LPSECURITY_ATTRIBUTES lpPrivateNamespaceAttributes,
_In_ LPVOID lpBoundaryDescriptor,
_In_ LPCWSTR lpAliasPrefix);注意此处的类型不一致——边界描述符是 void* 而非 HANDLE,这是一个微小的 API 不一致。边界描述符不是一个内核对象,尽管返回了一个 HANDLE;它有自己的 DeleteBoundaryDescriptor 函数。
如果命名空间已存在,CreatePrivateNamespace 会失败,此时必须改用 OpenPrivateNamespace:
HANDLE OpenPrivateNamespaceW(
_In_ LPVOID lpBoundaryDescriptor,
_In_ LPCWSTR lpAliasPrefix);
BOOLEAN ClosePrivateNamespace(
_In_ HANDLE Handle,
_In_ ULONG Flags); // 0 或 PRIVATE_NAMESPACE_FLAG_DESTROY另一个不一致之处:ClosePrivateNamespace 返回 BOOLEAN(typedef 为 BYTE),而不是标准的 BOOL。
一旦命名空间被创建或打开,有名称的对象就以 alias\name 的形式来创建,其中 "alias" 是 lpAliasPrefix 的值。
PrivateSharing 应用程序代码
对话框类成员:
private:
wil::unique_handle m_hSharedMem;
HANDLE m_hBD{ nullptr }, m_hNamespace{ nullptr };对内存映射文件句柄使用了 WIL 的 unique_handle,边界描述符和命名空间则用了原始句柄。
对话框创建时的设置:
// 创建边界描述符
m_hBD = ::CreateBoundaryDescriptor(L"MyDescriptor", 0);
BYTE sid[SECURITY_MAX_SID_SIZE];
auto psid = reinterpret_cast<PSID>(sid);
DWORD sidLen;
::CreateWellKnownSid(WinBuiltinUsersSid, nullptr, psid, &sidLen);
::AddSIDToBoundaryDescriptor(&m_hBD, psid);
// 创建私有命名空间
m_hNamespace = ::CreatePrivateNamespace(nullptr, m_hBD, L"MyNamespace");
if (!m_hNamespace) {
m_hNamespace = ::OpenPrivateNamespace(m_hBD, L"MyNamespace");
}
m_hSharedMem.reset(::CreateFileMapping(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, 1 << 12, L"MyNamespace\\MySharedMem"));向边界描述符添加了一个 SID——WinBuiltinUsersSid,涵盖所有标准用户。更严格的 SID(如管理员组)可以限制访问。
在 WM_DESTROY 上进行清理:
if (m_hNamespace)
::ClosePrivateNamespace(m_hNamespace, 0);
if (m_hBD)
::DeleteBoundaryDescriptor(m_hBD);WIL 包装器
WIL 没有为边界描述符和私有命名空间提供内置的包装器,但创建它们并不困难:
namespace wil {
static void close_private_ns(HANDLE h) {
::ClosePrivateNamespace(h, 0);
};
using unique_private_ns = unique_any_handle_null_only<decltype(
&close_private_ns), close_private_ns>;
using unique_bound_desc = unique_any_handle_null_only<decltype(
&::DeleteBoundaryDescriptor), ::DeleteBoundaryDescriptor>;
}"PrivateSharing2" 项目与 "PrivateSharing" 类似,但对所有句柄都使用了这些 WIL 包装器,包括 MapViewOfFile 返回的指针。
PrivateSharing2 中的读取函数:
wil::unique_mapview_ptr<void> buffer(::MapViewOfFile(
m_hSharedMem.get(), FILE_MAP_READ, 0, 0, 0));
if (!buffer) {
AtlMessageBox(m_hWnd, L"Failed to map memory", IDR_MAINFRAME);
return 0;
}
SetDlgItemText(IDC_TEXT, (PCWSTR)buffer.get());用户对象和 GDI 对象
除了内核对象之外,Windows 还使用用户对象(User Objects)和 GDI 对象(GDI Objects)。任务管理器可以为每个进程显示这两种对象的计数。
用户对象
用户对象包括窗口(HWND)、菜单(HMENU)和钩子(HHOOK)。它们的句柄具有以下特性:
- 没有引用计数。第一个销毁对象的调用者就会消除该对象。
- 句柄值在一个窗口站(Window Station)范围内有效。一个窗口站包含剪贴板、桌面和原子表,因此句柄可以在共享桌面的应用程序之间自由传递。
GDI 对象
GDI(Graphics Device Interface,图形设备接口)是 Windows 最初的图形 API。常见的 GDI 对象包括:设备上下文(HDC)、画笔(HPEN)、画刷(HBRUSH)、位图(HBITMAP)等。它们的特性:
- 没有引用计数。
- 句柄值仅在创建进程内有效。
- 不能在进程之间共享。
总结
本章介绍了内核对象以及如何通过句柄来访问和共享它们。没有深入探讨特定的对象类型,因为这些将在后续章节中讲述。下一章将深入介绍最著名的内核对象——进程。