Reactos 第 4 章 对象管理 — 4.3 句柄和句柄表(Handle & Handle Table)

第 4 章 对象管理 --- 4.3 句柄和句柄表(Handle & Handle Table)

4.3.0 框架图

以下 ASCII 图展示了 Windows/ReactOS 句柄体系的核心结构:从用户态 HANDLE 值的组成,到内核中三级页表式结构,再到一次完整的「打开 → 返回句柄 → 用户态使用 → 内核还原为对象指针」流程。

复制代码
┌────────────────────────────────────────────────────────────────────────────┐
│                    用户态 HANDLE 值(32-bit / 64-bit)                       │
├────────────┬─────────────┬──────────────┬────────────┬────────────────────┤
│  KernelFlag│  HighIndex  │   MidIndex   │ LowIndex   │ TagBits(低2位)     │
│  (仅64位)  │             │              │            │                    │
└────────────┴─────────────┴──────────────┴────────────┴────────────────────┘

                                  │
                                  │  系统调用:NtCreateEvent/OpenFile 等
                                  ▼

┌────────────────────────────────────────────────────────────────────────────┐
│                         HANDLE_TABLE(每个进程一份)                          │
├────────────────────────────────────────────────────────────────────────────┤
│  ULONG_PTR TableCode             ; 低2位 = 层级(0/1/2), 高位=表指针           │
│  PEPROCESS  QuotaProcess          ; 配额进程                                │
│  PVOID      UniqueProcessId       ; 进程 ID                                 │
│  EX_PUSH_LOCK HandleTableLock[4]  ; 4 把分段锁                              │
│  LIST_ENTRY HandleTableList       ; 全局句柄表链表                          │
│  ULONG      FirstFree             ; 空闲链表头                              │
│  ULONG      NextHandleNeedingPool ; 下一次需分配页的句柄值                    │
│  LONG       HandleCount           ; 已使用句柄数                             │
└────────────────────────────────────────────────────────────────────────────┘
                                   │
                                   │  TableCode & ~3 → 根表指针
                                   ▼
┌────────────────────────────────────────────────────────────────────────────┐
│                        三级页表式结构(类似 x86 页表)                         │
│                                                                            │
│  Level 0 (单页)     Level 1 (两级)          Level 2 (三级)                   │
│  ┌──────────┐      ┌──────────┐            ┌──────────┐                      │
│  │ Entry[0] │      │ PageTbl* │──┐         │ MidTbl*  │──┐                   │
│  │ Entry[1] │      │ PageTbl* │  │         │ MidTbl*  │  │                   │
│  │   ...    │      │  ...     │  │         │  ...     │  │                   │
│  │Entry[N-1]│      │ PageTbl* │──┤         │ MidTbl*  │  │                   │
│  └──────────┘      └──────────┘  │         └──────────┘  │                   │
│                                  ▼                       ▼                   │
│                           ┌──────────┐           ┌──────────┐                │
│                           │ Entry[0] │           │ PageTbl* │──┐             │
│                           │ Entry[1] │           │ PageTbl* │  │             │
│                           │  ...     │           │  ...     │  │             │
│                           └──────────┘           └──────────┘  │             │
│                                                                 ▼             │
│                                                          ┌──────────┐         │
│                                                          │ Entry[0] │         │
│                                                          │ Entry[1] │         │
│                                                          │  ...     │         │
│                                                          └──────────┘         │
└────────────────────────────────────────────────────────────────────────────┘

                                   │
                                   │  LowIndex 索引 Entry
                                   ▼

┌────────────────────────────────────────────────────────────────────────────┐
│                      HANDLE_TABLE_ENTRY(每个句柄 16 字节)                   │
├────────────────────────────────────────────────────────────────────────────┤
│  union { PVOID Object; ULONG_PTR ObAttributes; ... }   ; 对象头指针 + 标志位 │
│  union { ULONG GrantedAccess; LONG NextFreeTableEntry; } ; 访问掩码 / 空闲链 │
└────────────────────────────────────────────────────────────────────────────┘
                                   │
                                   │  & ~7  → POBJECT_HEADER
                                   ▼

┌────────────────────────────────────────────────────────────────────────────┐
│                          OBJECT_HEADER → 对象体                               │
│  PointerCount / HandleCount / Type / NameInfoOffset / ...  → PVOID ObjectBody │
└────────────────────────────────────────────────────────────────────────────┘


╔══════════════════════════════ 完整调用流 ══════════════════════════════════╗
║                                                                            ║
║  用户态程序                                                               ║
║    │                                                                      ║
║    ├─ CreateEvent(NULL, FALSE, FALSE, NULL);                              ║
║    │  │                                                                   ║
║    │  └──▶ kernel32!CreateEventW ─▶ ntdll!NtCreateEvent ─┐               ║
║    │                                                     │               ║
║  ──────────────────── 系统调用边界 ───────────────────────               ║
║    │                                                     │               ║
║  内核                                                                     ║
║    │                                                     │               ║
║    ├─ NtCreateEvent(...)                                                ║
║    │   ├─ ObCreateObject(...)  → 分配 Event 对象体 + OBJECT_HEADER        ║
║    │   ├─ ObInsertObject(...)                                             ║
║    │   │   └─ ObpCreateHandle(...)                                        ║
║    │   │       └─ ExpAllocateHandleTableEntry → ExCreateHandle            ║
║    │   │           └─ 从空闲链表取 Entry / 必要时分配新页表                 ║
║    │   │           └─ 写入 Entry.Object = POBJECT_HEADER                  ║
║    │   │           └─ Entry.GrantedAccess = EVENT_ALL_ACCESS              ║
║    │   └─ 返回 HANDLE 值(TagBits + Low/Mid/High Index)                  ║
║    │                                                                      ║
║    │                                                                      ║
║    ├─ 用户态使用 HANDLE: SetEvent(hEvent);                               ║
║    │   └─ ntdll!NtSetEvent ──▶ 内核                                       ║
║    │        └─ ObReferenceObjectByHandle(hEvent, ...)                    ║
║    │            ├─ 检查 KernelFlag → 选择当前进程 / 系统句柄表              ║
║    │            └─ ExpLookupHandleTableEntry → 根据 High/Mid/Low 定位     ║
║    │            └─ 提取 Entry.Object → 去除低 3 位 TagBits → POBJECT_HEADER ║
║    │            └─ OBJECT_TO_OBJECT_HEADER → 检查 Type 是否匹配            ║
║    │            └─ 检查 GrantedAccess & DesiredAccess                    ║
║    │            └─ InterlockedIncrement(PointerCount)                     ║
║    │            └─ 返回对象体指针 PKEVENT                                 ║
║    │                                                                      ║
║    ├─ 用户态关闭句柄: CloseHandle(hEvent);                               ║
║        └─ ntdll!NtClose ──▶ 内核                                          ║
║             └─ ObpCloseHandleTableEntry                                    ║
║                 └─ ExDestroyHandle → 将 Entry 挂回空闲链表                 ║
║                 └─ InterlockedDecrement(HandleCount)                      ║
║                 └─ HandleCount == 0 时调用对象类型的 CloseProcedure        ║
║                 └─ ObDereferenceObject → PointerCount == 0 → 释放内存     ║
║                                                                            ║
╚════════════════════════════════════════════════════════════════════════════╝

框架图深度解读

这幅图展示了句柄系统的完整架构,从用户态的 HANDLE 值到内核态的对象指针。理解这幅图的关键在于把握三个核心概念:

第一:HANDLE 值的编码方式

图中顶部的 HANDLE 值分解展示了句柄是如何编码的:

  • TagBits(低 2 位):始终为 0(或特殊标志),用于快速验证句柄合法性
  • LowIndex:在叶子页表中的索引,范围 0~511(每页 512 个 Entry)
  • MidIndex:在中间页表中的索引,范围 0~511
  • HighIndex:在根页表中的索引,范围 0~511
  • KernelFlag(仅 64 位):标识是否为内核句柄

这种编码方式与 x86 的虚拟地址结构完全同构:

  • x86 虚拟地址:PageDirIndex(10) + PageTblIndex(10) + PageOffset(12)
  • HANDLE 值:HighIndex + MidIndex + LowIndex + TagBits

第二:三级页表式结构的动态扩展

句柄表采用三级结构,但并非一开始就是三级:

  • Level 0(单页):句柄数 ≤ 512 时,直接使用单页存储所有 Entry
  • Level 1(两级):句柄数 > 512 时,分配一个根页表指向多个叶子页表
  • Level 2(三级):句柄数 > 512×512 时,分配中间页表层

这种动态扩展机制使得句柄表可以支持从几个句柄到数百万句柄的平滑过渡,而不会浪费内存。

第三:句柄表项(HANDLE_TABLE_ENTRY)的双用途

每个 Entry 有两种状态:

  • 已使用状态Object 字段指向 OBJECT_HEADERGrantedAccess 存储权限掩码
  • 空闲状态NextFreeTableEntry 指向下一个空闲 Entry,形成空闲链表

这种设计使得句柄分配和释放都是 O(1) 操作:

  • 分配:从 FirstFree 取出第一个空闲 Entry
  • 释放:将 Entry 插入到空闲链表头部

完整调用流的三个阶段

图中底部的完整调用流展示了句柄的三个生命周期阶段:

  1. 创建阶段(左侧):

    • ObCreateObject 分配对象内存
    • ObpCreateHandle 在句柄表中分配 Entry
    • 返回 HANDLE 值给用户态
  2. 使用阶段(中间):

    • 用户态传入 HANDLE
    • ObReferenceObjectByHandle 查找 Entry
    • 验证权限、递增引用计数
    • 返回对象指针给内核使用
  3. 关闭阶段(右侧):

    • NtClose 触发句柄关闭
    • 将 Entry 挂回空闲链表
    • 递减引用计数,可能触发对象销毁

4.3.0.1 设计意图

核心问题:用户态进程不能、也不应该直接看到或操作内核对象的内存地址。一方面,地址空间隔离(用户态无法访问内核虚拟地址)阻止了它;另一方面,安全策略(不同进程对同一内核对象拥有不同权限)要求必须有一层间接引用。
设计哲学 :「句柄(HANDLE)」是一种 能力(Capability) 设计:持有一个句柄即表示拥有对底层对象的特定权限(GrantedAccess),且只有通过这个句柄(由内核验证)才能操作对象。这比「返回指针 + 权限检查」更安全------用户态根本没有伪造指针的机会。
本节定位 :我们将自底向上地剖析 ReactOS 中 HANDLE 值编码 → HANDLE_TABLE 三级结构 → HANDLE_TABLE_ENTRY 字段 → 句柄创建/引用/关闭全流程 → 内核句柄表 → 跨进程句柄复制 等完整话题,所有结构均引用 ReactOS 源码。


4.3.1 为什么需要句柄

从安全隔离的角度

用户态进程运行在自己的虚拟地址空间中,内核态对象体位于系统地址空间。即使用户态能够偶然得到一个内核虚拟地址,MMU 也会阻止它访问(Ring 3 无法访问 Ring 0 页)。因此必须由内核在内核态「代为访问」对象,并通过一个整数索引(即句柄)让用户态在系统调用中引用之。

句柄作为能力(Capability)

句柄不仅仅是索引,它还携带:

  1. 安全边界 :内核在每次 ObReferenceObjectByHandle 时验证句柄值的合法性(是否位于已分配页、Entry 是否被使用、权限位是否足够)。
  2. 抽象封装 :用户态不需要知道 KEVENTEPROCESSFILE_OBJECT 等结构的布局,它们统一由 HANDLE 表示。
  3. 跨进程共享:同一内核对象可以同时被多个进程以不同句柄值引用(例如父子进程共享事件对象)。每个进程的句柄表各自独立,权限也可以不同。

与 Unix 文件描述符(fd)的对比

特性 Windows 句柄 HANDLE Unix 文件描述符 fd
本质 内核对象的间接引用 打开文件表的索引
可继承 OBJ_INHERIT 标志决定 FD_CLOEXEC 反向决定
跨进程传递 NtDuplicateObject(显式复制) SCM_RIGHTS Unix socket / fork 继承
类型感知 创建时指定类型,引用时检查类型 一切皆文件,不检查「类型」
权限掩码 GrantedAccess 存在每个 Entry 中 由打开时 flags 决定,存于 struct file
内核句柄 有系统进程句柄表,供驱动使用 由内核自己直接操作 struct file*

4.3.2 HANDLE_TABLE:三级页表式结构

结构定义

HANDLE_TABLE 定义在 sdk/include/ndk/extypes.h:787-828(file:///d:/reactos/sdk/include/ndk/extypes.h#L787-L828):

c 复制代码
typedef struct _HANDLE_TABLE
{
    ULONG_PTR TableCode;                   // 低 2 位:层级 (0/1/2);高位:实际表指针
    PEPROCESS QuotaProcess;                // 配额进程(向谁收取配额)
    PVOID UniqueProcessId;                 // 进程 ID
    EX_PUSH_LOCK HandleTableLock[4];       // 4 把分段锁,支持并发
    LIST_ENTRY HandleTableList;            // 挂入全局 HandleTableListHead
    EX_PUSH_LOCK HandleContentionEvent;    // 用于 Entry 锁竞争时的等待
    PHANDLE_TRACE_DEBUG_INFO DebugInfo;    // 调试信息
    LONG ExtraInfoPages;                   // 额外审计页
    ULONG FirstFree;                       // 空闲链表头(HANDLE 值)
    ULONG LastFree;                        // 空闲链表尾
    ULONG NextHandleNeedingPool;           // 下一个需分配页的句柄
    LONG HandleCount;                      // 已使用句柄数(原子增减)
    union {                                // 标志位
        ULONG Flags;
        UCHAR StrictFIFO : 1;              // 是否严格 FIFO(缺省 0 = LIFO 重用)
    };
} HANDLE_TABLE, *PHANDLE_TABLE;

每个 EPROCESS 拥有一个 ObjectTable 字段指向其 HANDLE_TABLE。系统进程(PID 4)额外有一份内核句柄表 ObpKernelHandleTable(见 ntoskrnl/ob/obhandle.c:20(file:///d:/reactos/ntoskrnl/ob/obhandle.c#L20-L20))。

为什么用三级索引

这与 x86 虚拟地址 → 物理地址的映射(Page Directory → Page Table → Page Frame)完全同构。设计目的是 稀疏性 + 可扩展性

  • Level 0(单页) :刚创建的进程只有极少句柄,只需一页(PAGE_SIZE 字节)就能容纳 PAGE_SIZE / sizeof(HANDLE_TABLE_ENTRY) 个条目,足够日常使用(x86 下 ≈ 256 个)。
  • Level 1(两级) :句柄数超过一页后,扩展为一级指针表(Mid Table),每个表项指向一个底层的 Entry 页。每个 Mid Table 是一页,可容纳 PAGE_SIZE / sizeof(PVOID) 个指针。
  • Level 2(三级) :再次超过时,在最上层加一级 High Table(High Index → Mid Table 指针),这样整个空间可以映射到系统允许的最多句柄数(约 16M 句柄,受限于 MAX_HIGH_INDEX)。

与 x86 页表的对比如下:

句柄表层级 角色 类比 x86 页表
TableCode & 3 == 2 → 三级 HighIndex → MidTable* Page Directory Index → PDE*
TableCode & 3 == 1 → 两级 MidIndex → PageTable* Page Table Index → PTE*
LowIndex → EntryLowIndex LowIndex → Entry* Offset within Page

EXHANDLE:HANDLE 值的位字段解析

HANDLE 值本身不是一个简单索引,它被编码为多个位域。定义见 ntoskrnl/include/internal/ex.h:89-108(file:///d:/reactos/ntoskrnl/include/internal/ex.h#L89-L108):

c 复制代码
// 位宽宏(ex.h:78-88)
#define HANDLE_LOW_BITS     (PAGE_SHIFT - 3)   // x86: 9 位
#define HANDLE_HIGH_BITS    (PAGE_SHIFT - 2)   // x86: 10 位
#define HANDLE_TAG_BITS     2                   // 恒为 2
#define HANDLE_INDEX_BITS   (HANDLE_LOW_BITS + 2 * HANDLE_HIGH_BITS)
#define KERNEL_FLAG_BITS    (sizeof(ULONG_PTR) * 8 - HANDLE_INDEX_BITS - HANDLE_TAG_BITS)

typedef union _EXHANDLE
{
    struct
    {
        ULONG_PTR TagBits:      HANDLE_TAG_BITS;  // 低 2 位:TagBits(快速验证)
        ULONG_PTR Index:        HANDLE_INDEX_BITS; // 真正的线性索引
        ULONG_PTR KernelFlag:   KERNEL_FLAG_BITS;  // 最高位:内核句柄标志
    };
    struct
    {
        ULONG_PTR TagBits2:     HANDLE_TAG_BITS;
        ULONG_PTR LowIndex:     HANDLE_LOW_BITS;   // 页内索引
        ULONG_PTR MidIndex:     HANDLE_HIGH_BITS;  // 中间层索引
        ULONG_PTR HighIndex:    HANDLE_HIGH_BITS;  // 顶层索引
        ULONG_PTR KernelFlag2:  KERNEL_FLAG_BITS;
    };
    HANDLE GenericHandleOverlay;                   // 与 HANDLE 兼容
    ULONG_PTR Value;                               // 原始整数值
    ULONG AsULONG;                                 // 32 位视图
} EXHANDLE, *PEXHANDLE;

以 32 位 x86 为例(PAGE_SHIFT=12):

复制代码
32-bit HANDLE:
  31 30 29 ... 21 20 ... 11 10 9 ... 2  1 0
 └────┬──────┘ └─────┬─────┘ └───┬───┘ └┬┘
      │              │            │      └── TagBits       (2 位, 恒 0)
      │              │            └──────── LowIndex        (9 位 = 0..511)
      │              └──────────────────── MidIndex        (10 位 = 0..1023)
      └─────────────────────────────────── HighIndex/KernelFlag
                                          (HighIndex 占 10 位,KernelFlag 在 32 位下为 0)

在 64 位系统中,KernelFlag 占若干高位,当最高位置位时表示这是内核句柄(Kernel Handle),指向系统句柄表而非进程句柄表。用户态试图引用它会直接返回 STATUS_INVALID_HANDLE

各级条目数宏(LOW/MID/HIGH)

c 复制代码
// 见 ntoskrnl/include/internal/ex.h:155-164
#define LOW_LEVEL_ENTRIES   (PAGE_SIZE / sizeof(HANDLE_TABLE_ENTRY))
#define MID_LEVEL_ENTRIES   (PAGE_SIZE / sizeof(PHANDLE_TABLE_ENTRY))
#define HIGH_LEVEL_ENTRIES  (16777216 / (LOW_LEVEL_ENTRIES * MID_LEVEL_ENTRIES))
#define MAX_LOW_INDEX       LOW_LEVEL_ENTRIES
#define MAX_MID_INDEX       (MID_LEVEL_ENTRIES * LOW_LEVEL_ENTRIES)
#define MAX_HIGH_INDEX      (MID_LEVEL_ENTRIES * MID_LEVEL_ENTRIES * LOW_LEVEL_ENTRIES)

由于 sizeof(HANDLE_TABLE_ENTRY) == 16(见下一节),在 x86 4KB 页下:

层级 每页条目数 索引位宽 容量
LOW_LEVEL_ENTRIES 4096 / 16 = 256 LowIndex(8~9 位) 一页 Entry
MID_LEVEL_ENTRIES 4096 / 4 = 1024(指针大小) MidIndex(10 位) 1024 × 256 = 262144 句柄
HIGH_LEVEL_ENTRIES 由 16M / 262144 推导 HighIndex(10 位) 最多约 16M 句柄

TableCode 的特殊编码

HANDLE_TABLE.TableCode 字段的低 2 位同时承担了「表层级」角色:

TableCode & 3 含义
0 Level 0 单页:TableCode & ~3 直接指向 Entry 页
1 Level 1 两级:TableCode & ~3 指向 Mid Table(指针数组),每个指针 → Entry 页
2 Level 2 三级:TableCode & ~3 指向 High Table,每个指针 → Mid Table → Entry 页

这个编码技巧避免了增加额外字段,同时使得 ExpLookupHandleTableEntry 可以用一段 switch/case 统一处理三种规模。

从 HANDLE 值到 Entry 的定位算法

核心函数 ExpLookupHandleTableEntryntoskrnl/ex/handle.c:41-103(file:///d:/reactos/ntoskrnl/ex/handle.c#L41-L103):

c 复制代码
PHANDLE_TABLE_ENTRY
NTAPI
ExpLookupHandleTableEntry(IN PHANDLE_TABLE HandleTable, IN EXHANDLE Handle)
{
    ULONG TableLevel;
    ULONG_PTR TableBase;
    PHANDLE_TABLE_ENTRY HandleArray, Entry;
    PVOID *PointerArray;

    Handle.TagBits = 0;                                 // 清除 tag,仅保留索引

    if (Handle.Value >= HandleTable->NextHandleNeedingPool)  return NULL;  // 越界

    TableBase = HandleTable->TableCode;
    TableLevel = (ULONG)(TableBase & 3);                 // 取层级
    TableBase &= ~3;                                      // 清低位,得到真实指针

    PointerArray = (PVOID*)TableBase;
    HandleArray  = (PHANDLE_TABLE_ENTRY)TableBase;

    switch (TableLevel)
    {
        case 2:
            PointerArray = PointerArray[Handle.HighIndex];  // 查 High Table
            ASSERT(PointerArray != NULL);
            // fall through
        case 1:
            HandleArray = PointerArray[Handle.MidIndex];    // 查 Mid Table
            ASSERT(HandleArray != NULL);
            // fall through
        case 0:
            Entry = &HandleArray[Handle.LowIndex];          // 查 Low 表项
            break;
        default:
            ASSERT(FALSE);
            Entry = NULL;
    }
    return Entry;
}

该函数的复杂度为 O(1),最多 3 次指针解引用。它也与 x86 的页表遍历过程在结构上是等价的:

复制代码
虚拟地址 → [PD Index] → PDE → [PT Index] → PTE → [Offset] → 物理页
HANDLE    → [HighIndex] → MidTable → [MidIndex] → EntryPage → [LowIndex] → Entry

4.3.3 HANDLE_TABLE_ENTRY:句柄表项详解

8/16 字节条目的精确定义

在 ReactOS 中,HANDLE_TABLE_ENTRY 定义在 sdk/include/ndk/extypes.h:766-785(file:///d:/reactos/sdk/include/ndk/extypes.h#L766-L785):

c 复制代码
typedef struct _HANDLE_TABLE_ENTRY
{
    union
    {
        PVOID Object;                    // 对象头指针(OBJECT_HEADER*)
        ULONG_PTR ObAttributes;          // 句柄属性(OBJ_INHERIT 等);低 3 位用作锁/标志
        PHANDLE_TABLE_ENTRY_INFO InfoTable;
        ULONG_PTR Value;                 // 原始值,用于原子交换
    };
    union
    {
        ULONG GrantedAccess;             // 授予的访问掩码
        struct { USHORT GrantedAccessIndex; USHORT CreatorBackTraceIndex; };
        LONG NextFreeTableEntry;         // 当条目空闲时,指向空闲链表中的下一项
    };
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;

第一个 ULONG_PTR:Object 指针 + TagBits + 锁位

  • Object 指针 :实际指向 OBJECT_HEADER(而不是对象体)。引用方需要用 OBJECT_TO_OBJECT_HEADER / ObpGetHandleObject 之类的宏进行转换。
  • 低 3 位(TagBits / Lock Bits)
    • ReactOS 中 EXHANDLE_TABLE_ENTRY_LOCK_BIT 宏(ex.h:149(file:///d:/reactos/ntoskrnl/include/internal/ex.h#L149-L149))定义了 1 作为锁位,但实际锁定逻辑通过 InterlockedCompareExchangePointerObject 字段整体进行原子操作------当 EXHANDLE_TABLE_ENTRY_LOCK_BIT 被清除时表示「已被锁定」,其他 CPU 若并发操作将自旋直到解锁者通过 InterlockedOr 再次置位并唤醒等待者。见 ExpLockHandleTableEntry / ExUnlockHandleTableEntryhandle.c:882-940(file:///d:/reactos/ntoskrnl/ex/handle.c#L882-L940))。
    • 使用低位的原因:OBJECT_HEADER 总是至少 8 字节对齐,真实对象指针的低 3 位必然为 0,因此可以借用来编码标志。

第二个 ULONG:GrantedAccess 与空闲链表

  • 使用中GrantedAccess(访问掩码),例如 EVENT_ALL_ACCESSFILE_READ_DATA | FILE_WRITE_DATA 等。这是 per-handle 的权限------同一 FILE_OBJECT 被不同进程打开时,各自句柄的 GrantedAccess 可以不同。
  • 空闲时NextFreeTableEntry 作为 LONG,承载下一个空闲条目的 HANDLE 值(或 0 表示链表结束)。这让整个句柄表支持 LIFO 空闲链表------最近被释放的句柄值会被最快重新分配,提高缓存命中率。

为什么用 16 字节(而不是更大)

  • 空间折中:一个典型的 x86 桌面应用通常打开数百到数千个句柄。每个条目 16 字节 × 10000 条目 ≈ 160KB,处于可接受的开销。
  • 性能折中 :Entry 正好占用 2 个指针大小(2×sizeof(PVOID) + 2×ULONG → 注意 64 位下是 16 字节),对齐良好,CPU 缓存友好。
  • 字段刚好够用:一个条目只需要对象指针 + 访问掩码 + 锁位,不需要更多。
Entry 结构设计的精妙之处

HANDLE_TABLE_ENTRY 的设计体现了多种优化技巧:

1. 指针低位复用(TagBits)

Object 字段的低 3 位被复用为标志位:

  • 位 0:锁标志(Lock Bit)
  • 位 1-2:其他属性标志

这种复用之所以可行,是因为:

  • OBJECT_HEADER 总是至少 8 字节对齐
  • 真实指针的低 3 位必然为 0
  • 因此可以安全地借用这些位来编码额外信息

2. Union 的双用途设计

第二个 Union 字段有两种用途:

  • 已分配状态GrantedAccess 存储权限掩码
  • 空闲状态NextFreeTableEntry 指向下一个空闲 Entry

这种设计避免了为空闲 Entry 分配额外的链表节点------Entry 本身就是链表节点。

3. 原子操作支持

Entry 的锁定通过 InterlockedCompareExchangePointer 实现:

c 复制代码
// 锁定 Entry
PVOID OldValue = Entry->Object;
if (InterlockedCompareExchangePointer(&Entry->Object, OldValue | LOCK_BIT, OldValue) == OldValue) {
    // 成功锁定
}

这种无锁设计避免了传统锁的开销,同时保证了多核环境下的正确性。

Entry 的生命周期

复制代码
空闲链表 → ExpAllocateHandleTableEntry → 分配 → 写入 Object / GrantedAccess
                                                    │
                                                    ▼
                                            使用中(被 ObReferenceObjectByHandle 引用)
                                                    │
                                                    ▼
                                           ObpCloseHandleTableEntry → ExDestroyHandle
                                                    │
                                                    ▼
                                      Entry.Object = NULL;插入空闲链表尾部

空闲链表由 HANDLE_TABLE.FirstFree / LastFree 维护(见 ExpFreeHandleTableEntry handle.c:277-327(file:///d:/reactos/ntoskrnl/ex/handle.c#L277-L327))。

EXHANDLE 的位编码与快速验证

EXHANDLE(见 4.3.2)之所以把 TagBits 放在最低 2 位而不是把 Index 紧凑地直接映射到 0, 1, 2, ...,就是为了让 非法句柄值(如 NULL=0x0INVALID_HANDLE_VALUE=(HANDLE)-1)自动落在可被检测的 TagBits 范围之外------这允许内核以简单的位运算拒绝无效值,而不必真的去查三级表。


4.3.4 ObpCreateHandle:句柄的创建

典型的「对象创建 → 返回句柄」完整流程

NtCreateEvent 为例(内部会走 ObCreateObject → ObInsertObject → ObpCreateHandle):

  1. Step 1 :分配对象体 ObCreateObject。从分页/非分页池分配 OBJECT_HEADER + sizeof(KEVENT),初始化 PointerCount = 1HandleCount = 0Type = ExEventObjectType,填充 NameInfoOffset 等元数据。
  2. Step 2 :插入对象目录(ObInsertObject)。若是命名对象(\BaseNamedObjects\MyEvent)则在目录中建立名字 → 对象头的映射;否则匿名。
  3. Step 3ObpCreateHandle → 调用底层 ExCreateHandle,从当前进程的 HANDLE_TABLE 分配一个空闲条目。
  4. Step 4 :填充 Entry.Object = ObjectHeader(含标志位)、Entry.GrantedAccess = GrantedAccess
  5. Step 5 :把对应的 HANDLE 值(TagBits = 0、Index = 该条目在线性空间中的位置)通过系统调用返回值写入用户态。

ObpCreateHandle 的内部实现

核心代码见 ntoskrnl/ob/obhandle.c:1495-1680(file:///d:/reactos/ntoskrnl/ob/obhandle.c#L1495-L1680),简化的伪代码:

c 复制代码
NTSTATUS ObpCreateHandle(OB_OPEN_REASON OpenReason, PVOID Object,
                         POBJECT_TYPE Type, PACCESS_STATE AccessState,
                         ULONG AdditionalReferences, ULONG HandleAttributes,
                         POBP_LOOKUP_CONTEXT Context, KPROCESSOR_MODE AccessMode,
                         PVOID *ReturnedObject, PHANDLE ReturnedHandle)
{
    // 1. 类型一致性检查(如果调用方传了 Type)
    if (Type && ObjectHeader->Type != Type) return STATUS_OBJECT_TYPE_MISMATCH;

    // 2. 如果是内核句柄(OBJ_KERNEL_HANDLE),使用系统句柄表并 Attach 到系统进程
    if (HandleAttributes & OBJ_KERNEL_HANDLE) {
        HandleTable = ObpKernelHandleTable;
        if (PsGetCurrentProcess() != PsInitialSystemProcess) {
            KeStackAttachProcess(&PsInitialSystemProcess->Pcb, &ApcState);
            AttachedToProcess = TRUE;
        }
    } else {
        HandleTable = PsGetCurrentProcess()->ObjectTable;
    }

    // 3. 更新对象头计数(HandleCount、Process 句柄计数、CreatorInfo、Type 统计)
    Status = ObpIncrementHandleCount(Object, AccessState, AccessMode,
                                     HandleAttributes, PsGetCurrentProcess(), OpenReason);

    // 4. 写入 GrantedAccess
    DesiredAccess = AccessState->RemainingDesiredAccess | AccessState->PreviouslyGrantedAccess;
    GrantedAccess = DesiredAccess & (ObjectType->TypeInfo.ValidAccessMask | ACCESS_SYSTEM_SECURITY);
    NewEntry.GrantedAccess = GrantedAccess;

    // 5. 释放 lookup context(对象现在已经通过 HandleTable 被引用)
    if (Context) ObpReleaseLookupContext(Context);

    // 6. 真正分配句柄条目(底层:ExpAllocateHandleTableEntry)
    Handle = ExCreateHandle(HandleTable, &NewEntry);

    // 7. 如果是内核句柄,设置最高位
    if (KernelHandle) Handle = ObMarkHandleAsKernelHandle(Handle);

    // 8. 返回句柄给调用方(最终将写入用户态输出参数)
    *ReturnedHandle = Handle;
    return STATUS_SUCCESS;
}

ExCreateHandle(底层)

ExCreateHandle 位于 ntoskrnl/ex/handle.c:825-854(file:///d:/reactos/ntoskrnl/ex/handle.c#L825-L854),它做两件事:

  1. ExpAllocateHandleTableEntry 从空闲链表取一个空闲条目(必要时调用 ExpAllocateHandleTableEntrySlow 分配新页表并扩展 NextHandleNeedingPool);
  2. 写入对象指针 + GrantedAccess,并解锁条目。

进程配额检查

ObpChargeQuotaForObject 在打开时向 QuotaProcess(通常即当前进程)收取池配额,若配额耗尽则失败。这限制了单个进程通过打开大量句柄耗尽系统池内存的能力。详见 obhandle.c:429-484(file:///d:/reactos/ntoskrnl/ob/obhandle.c#L429-L484)。

句柄创建流程的设计分析

句柄创建流程涉及多个层次的协作,体现了分层设计的思想:

第一层:系统调用层(NtCreateXxx)

系统调用处理函数负责:

  • 参数验证(用户态指针、缓冲区大小)
  • 调用对象管理器的创建接口
  • 将句柄返回给用户态

第二层:对象管理器层(ObpCreateHandle)

对象管理器负责:

  • 权限计算(GrantedAccess
  • 配额检查(ObpChargeQuotaForObject
  • 调用句柄表管理器分配 Entry

第三层:句柄表管理器层(ExCreateHandle)

句柄表管理器负责:

  • 从空闲链表分配 Entry
  • 必要时扩展页表结构
  • 写入 Entry 内容

关键设计决策:为什么先检查配额再分配句柄?

配额检查在句柄分配之前执行,原因:

  1. 避免资源浪费:如果配额不足,不应该分配句柄 Entry
  2. 简化错误处理:配额失败可以直接返回错误,无需回滚句柄分配
  3. 公平性:配额检查确保资源分配的公平性

关键设计决策:为什么使用 LIFO 空闲链表?

句柄表使用 LIFO(后进先出)空闲链表:

  1. 缓存友好:最近释放的 Entry 很可能还在 Cache 中
  2. 减少碎片:频繁使用的 Entry 集中在少数页中
  3. 简化实现 :LIFO 只需维护一个 FirstFree 指针

4.3.5 ObReferenceObjectByHandle:句柄查对象

完整转换流程

用户态把一个 HANDLE 值作为系统调用参数传入内核时,内核通过 ObReferenceObjectByHandle 将其还原为对象指针并引用。核心流程:

  1. Step 1 验证句柄合法性
    • HandleToLong(Handle) < 0 → 可能是特殊伪句柄 NtCurrentProcess()(-1)、NtCurrentThread()(-2),或是内核句柄标志位。
    • 伪句柄直接在当前 EPROCESS/ETHREAD 上增加 PointerCount 并返回。
    • 内核句柄(KernelFlag 置位)仅允许来自 KernelMode,否则直接返回 STATUS_INVALID_HANDLE
  2. Step 2 定位 Entry :调用 ExMapHandleToPointer(内部调用 ExpLookupHandleTableEntry + ExpLockHandleTableEntry)定位并锁定条目。
  3. Step 3 取出 Object :从 Entry.Object 得到 POBJECT_HEADER,用 OBJECT_TO_OBJECT_HEADER/ObpGetHandleObject 还原对象头指针(去除低 3 位标志)。
  4. Step 4 得到对象体Object = &ObjectHeader->Body
  5. Step 5 验证权限 :检查 Entry.GrantedAccess & DesiredAccess == DesiredAccess。若调用方是用户态但权限不足 → STATUS_ACCESS_DENIED
  6. Step 6 验证对象类型 :若调用方传入了 ObjectType(例如 PsProcessType),则要求 ObjectHeader->Type == ObjectType,否则 → STATUS_OBJECT_TYPE_MISMATCH
  7. Step 7 增加 PointerCountInterlockedIncrement(&ObjectHeader->PointerCount)
  8. Step 8 解锁 Entry 并返回对象体指针

实现细节(ObReferenceObjectByHandle)

函数位于 ntoskrnl/ob/obref.c:492-...(file:///d:/reactos/ntoskrnl/ob/obref.c#L492-L630)。关键片段:

c 复制代码
NTSTATUS ObReferenceObjectByHandle(HANDLE Handle, ACCESS_MASK DesiredAccess,
                                   POBJECT_TYPE ObjectType, KPROCESSOR_MODE AccessMode,
                                   PVOID *Object, POBJECT_HANDLE_INFORMATION HandleInfo)
{
    *Object = NULL;

    // 处理伪句柄:NtCurrentProcess() / NtCurrentThread()
    if (HandleToLong(Handle) < 0) {
        if (Handle == NtCurrentProcess()) { /* 对当前 EPROCESS 计数并返回 */ }
        else if (Handle == NtCurrentThread()) { /* 对当前 ETHREAD 计数并返回 */ }
        else if (AccessMode == KernelMode) {
            // 内核态:把它看作内核句柄表中的索引
            Handle = ObKernelHandleToHandle(Handle);
            HandleTable = ObpKernelHandleTable;
        } else {
            return STATUS_INVALID_HANDLE;   // 用户态不允许引用内核句柄
        }
    } else {
        HandleTable = PsGetCurrentProcess()->ObjectTable;
    }

    // 进入临界区、查句柄表并上锁
    KeEnterCriticalRegion();
    HandleEntry = ExMapHandleToPointer(HandleTable, Handle);
    if (HandleEntry) {
        ObjectHeader = ObpGetHandleObject(HandleEntry);
        GrantedAccess = HandleEntry->GrantedAccess;

        // 类型检查
        if (ObjectType && ObjectHeader->Type != ObjectType) {
            ExUnlockHandleTableEntry(HandleTable, HandleEntry);
            KeLeaveCriticalRegion();
            return STATUS_OBJECT_TYPE_MISMATCH;
        }

        // 权限检查(内核态绕过)
        if (AccessMode != KernelMode && (~GrantedAccess & DesiredAccess)) {
            ExUnlockHandleTableEntry(HandleTable, HandleEntry);
            KeLeaveCriticalRegion();
            return STATUS_ACCESS_DENIED;
        }

        // 引用计数 +1
        InterlockedIncrementSizeT(&ObjectHeader->PointerCount);

        // 返回句柄信息(若调用方要求)
        if (HandleInfo) {
            HandleInfo->HandleAttributes = HandleEntry->ObAttributes & OBJ_HANDLE_ATTRIBUTES;
            HandleInfo->GrantedAccess = GrantedAccess;
        }

        ExUnlockHandleTableEntry(HandleTable, HandleEntry);
        KeLeaveCriticalRegion();
        *Object = &ObjectHeader->Body;
        return STATUS_SUCCESS;
    }
    KeLeaveCriticalRegion();
    return STATUS_INVALID_HANDLE;
}

错误路径一览

错误 来源
STATUS_INVALID_HANDLE 句柄值超出 NextHandleNeedingPool、或对应 Entry 的 Object 为 NULL(已关闭)
STATUS_ACCESS_DENIED GrantedAccess 缺少 DesiredAccess 要求的位
STATUS_OBJECT_TYPE_MISMATCH ObjectHeader->Type != ObjectType
句柄查找机制的设计分析

ObReferenceObjectByHandle 是用户态与内核态之间最重要的桥梁函数。它的设计体现了多种安全原则:

1. 多层验证机制

查找过程包含多层验证:

  • 句柄合法性验证:检查句柄值是否在有效范围内
  • Entry 状态验证:检查 Entry 是否已分配(Object != NULL)
  • 权限验证:检查 GrantedAccess 是否满足 DesiredAccess
  • 类型验证:检查对象类型是否匹配

每一层验证都有明确的失败原因,便于调试和错误报告。

2. 引用计数的原子操作

引用计数通过 InterlockedIncrement 递增:

c 复制代码
InterlockedIncrementSizeT(&ObjectHeader->PointerCount);

这种原子操作保证了多线程环境下的正确性:

  • 多个线程可以同时引用同一对象
  • 引用计数不会出现竞态条件
  • 对象不会在使用中被意外释放

3. 临界区保护

函数使用临界区(KeEnterCriticalRegion / KeLeaveCriticalRegion)保护:

  • 防止 APC(异步过程调用)打断查找过程
  • 保证查找操作的原子性
  • 避免死锁和竞态条件

4. 伪句柄的特殊处理

伪句柄(NtCurrentProcess = -1, NtCurrentThread = -2)有特殊处理路径:

  • 不需要查找句柄表
  • 直接返回当前进程/线程对象
  • 不创建新的句柄 Entry

这种设计避免了为"当前进程/线程"创建额外句柄的开销。

5. 内核态的权限绕过

AccessMode == KernelMode 时,权限检查被绕过:

c 复制代码
if (AccessMode != KernelMode && (~GrantedAccess & DesiredAccess)) {
    return STATUS_ACCESS_DENIED;
}

这允许内核代码以任意权限访问对象,简化了内核实现。


4.3.6 句柄的关闭与继承

ObCloseHandle 实现

用户态调用 CloseHandle(hXxx) 最终走 kernel32!CloseHandle → ntdll!NtClose → ObCloseHandle → ObpCloseHandle → ObpCloseHandleTableEntry。核心代码:

c 复制代码
// [obhandle.c:3377-3406] ObCloseHandle / NtClose
NTSTATUS NTAPI ObCloseHandle(HANDLE Handle, KPROCESSOR_MODE AccessMode)
{
    return ObpCloseHandle(Handle, AccessMode);
}

NTSTATUS NTAPI NtClose(HANDLE Handle)
{
    return ObpCloseHandle(Handle, ExGetPreviousMode());
}

ObpCloseHandleTableEntryobhandle.c:683-779(file:///d:/reactos/ntoskrnl/ob/obhandle.c#L683-L779))的关闭流程:

  1. Entry.Object 得到 OBJECT_HEADER
  2. 若对象类型注册了 OkayToCloseProcedure,调用它以允许驱动否决关闭(例如某些文件系统过滤);
  3. 检查 OBJ_PROTECT_CLOSE(通过 GrantedAccess & ObpAccessProtectCloseBit 判断)------若用户态尝试关闭受保护句柄则返回 STATUS_HANDLE_NOT_CLOSABLE,在调试版本甚至会触发异常;
  4. ExDestroyHandle:将 Entry 归还空闲链表;
  5. ObpDecrementHandleCount:对象头 HandleCount--;如果为 0 且该类型维护了 Process 句柄计数,同时清理进程条目;调用 CloseProcedure
  6. ObDereferenceObjectPointerCount--,归零时调用 DeleteProcedure 并释放池内存。

OBJ_INHERIT 标志与句柄继承

句柄属性标志位于 sdk/include/ndk/obtypes.h:30-51(file:///d:/reactos/sdk/include/ndk/obtypes.h#L30-L51):

c 复制代码
#define OBJ_INHERIT           0x00000002L   // 子进程创建时继承
#define OBJ_PERMANENT         0x00000010L   // 对象永久存在(即使 HandleCount=0)
#define OBJ_EXCLUSIVE         0x00000020L   // 独占打开
#define OBJ_CASE_INSENSITIVE  0x00000040L
#define OBJ_OPENIF            0x00000080L
#define OBJ_KERNEL_HANDLE     0x00000200L   // 插入系统句柄表
#define OBJ_FORCE_ACCESS_CHECK 0x00000400L

NtCreateUserProcess 路径中,若父进程句柄表中某个条目带有 OBJ_INHERIT,会在子进程句柄表中复制一个条目(新的 HANDLE 值、相同 Object 指针、继承而来的 GrantedAccess),并对对象头 HandleCount 累加。

为什么 CloseHandle 可从用户态直接调用?

NtClose 不检查「这个句柄是谁打开的」------它只验证「该 HANDLE 值是否是当前进程句柄表中一个有效条目」。任何持有该句柄的代码都可以关闭它,这是能力模型的核心属性:能力持有者对能力有最终处置权 (但 OBJ_PROTECT_CLOSE 可以禁止关闭,受调试器/驱动用途使用)。


4.3.7 内核句柄(Kernel Handle)

什么是内核句柄

内核句柄引用的是 系统进程句柄表 ObpKernelHandleTable(见 obhandle.c:20(file:///d:/reactos/ntoskrnl/ob/obhandle.c#L20-L20)),而不是当前进程的句柄表。内核组件(驱动、对象管理器自身)经常需要:

  • 在不关联到任何用户进程的情况下持有对对象的引用;
  • 不希望句柄被用户态 NtClose 误关闭;
  • 跨进程访问对象(例如文件系统缓存对 FILE_OBJECT 的引用)。

KernelFlag 位与 ObMarkHandleAsKernelHandle

  • 内核句柄在 32 位系统上由最高位(或 64 位系统的 KernelFlag 字段)标识。任何用户态把它传入 ObReferenceObjectByHandle 时,因为 AccessMode != KernelMode,直接返回 STATUS_INVALID_HANDLE(见 ObReferenceObjectByHandle 的 early-exit 分支)。
  • ObMarkHandleAsKernelHandle 在创建内核句柄时把标识位置位;ObKernelHandleToHandle 逆向去除该标志,得到真正的索引值去查系统句柄表。
  • ObIsKernelHandleobhandle.c:3523-3529(file:///d:/reactos/ntoskrnl/ob/obhandle.c#L3523-L3529))仅检查 KernelFlag 位。

内核句柄的特殊规则

  • 只能在内核态引用;
  • 不受「当前进程退出时清理句柄表」影响(因为它在系统进程句柄表里);
  • 需要内核代码显式调用 ZwClose / ObCloseHandle 关闭,否则会造成句柄泄漏。

4.3.8 概念解释(术语表)

术语 定义 所在文件
HANDLE 用户态对内核对象的间接引用;本质是一个位编码的索引(见 EXHANDLE ntoskrnl/include/internal/ex.h:89-108(file:///d:/reactos/ntoskrnl/include/internal/ex.h#L89-L108)
HANDLE_TABLE 每个进程一份的根结构,维护三级页表和空闲链表、句柄计数、表级锁 sdk/include/ndk/extypes.h:787-828(file:///d:/reactos/sdk/include/ndk/extypes.h#L787-L828)
HANDLE_TABLE_ENTRY 每个句柄一个条目,存储对象指针 + 低 3 位锁标志;GrantedAccess;空闲时存 NextFree sdk/include/ndk/extypes.h:766-785(file:///d:/reactos/sdk/include/ndk/extypes.h#L766-L785)
EXHANDLE HANDLE 的位字段视图:TagBits / LowIndex / MidIndex / HighIndex / KernelFlag ntoskrnl/include/internal/ex.h:89-108(file:///d:/reactos/ntoskrnl/include/internal/ex.h#L89-L108)
TableCode HANDLE_TABLE 的第一个字段,把表指针与层级(0/1/2)编码在同一 ULONG_PTR 同上
NextHandleNeedingPool 下一次分配新页时起始句柄值;用于判断 ExpLookupHandleTableEntry 越界 HANDLE_TABLE
FirstFree / LastFree 空闲条目链表的头/尾;新分配从 FirstFree 取,关闭时插入 HANDLE_TABLE
TagBits EXHANDLE 低 2 位,保持为 0,使 NULL=0 等非法值可被快速检测 EXHANDLE
GrantedAccess 该句柄持有的访问掩码,由打开时的安全检查决定;per-handle HANDLE_TABLE_ENTRY 的第二个 union
OBJ_INHERIT 句柄属性标志,表示该句柄在子进程创建时被继承 sdk/include/ndk/obtypes.h:34(file:///d:/reactos/sdk/include/ndk/obtypes.h#L34-L34)
OBJ_KERNEL_HANDLE 句柄属性标志,创建时插入系统句柄表,置位 KernelFlag sdk/include/ndk/obtypes.h:40(file:///d:/reactos/sdk/include/ndk/obtypes.h#L40-L40)
ProtectFromClose 通过 GrantedAccess 特定位实现;禁止用户态 NtClose 关闭该句柄 ObpCloseHandleTableEntry 中检测
Kernel Handle 最高位(KernelFlag)置位的句柄,指向系统进程句柄表;仅供内核态 ObMarkHandleAsKernelHandle
Handle Leak(句柄泄漏) 句柄被创建后从未关闭,导致 HandleCount 持续增长,耗尽句柄表池配额 进程退出时会整体释放句柄表
ExpAllocateHandleTableEntrySlow 句柄表扩容路径:分配新页 / 升级到下一层级并更新 NextHandleNeedingPool ntoskrnl/ex/handle.c:500-644(file:///d:/reactos/ntoskrnl/ex/handle.c#L500-L644)

4.3.9 为什么要这样设计

Q1:为什么不直接把对象指针返回给用户态?

  • 用户态没有访问内核地址空间的权限(Ring 3 不能访问 Ring 0 页)。

  • 即使能访问,也无法强制安全检查------用户态可以跳过检查直接读写对象,等于放弃所有安全模型。

  • 对象地址会随着对象释放/重分配变「野」,用户态无从得知句柄是否仍然有效。

  • 内核对象池可能被压缩/重排(Windows/ReactOS 虽不做对象池移动,但对象被释放后的内存可立即重用为他物)。句柄通过内核维护的一层间接引用解决了「悬挂指针」问题。
    Q2:为什么句柄表用三级结构(而不是数组或链表)?

  • 稀疏性:大多数进程只打开少量句柄(< 100)。如果句柄表是一个大数组预分配 16M × 16 字节 = 256MB,完全不可行。

  • 可扩展性:极端进程(大型数据库、I/O 密集型服务)可能需要打开数万个句柄。三级结构平滑支持从 256 → 256k → 16M 的扩展。

  • O(1) 查找:与链表相比,三级索引的查找时间与句柄总数无关。

  • 与页表同构:内核对页表式结构已很熟悉;TableCode 低 2 位编码层级的技巧省掉了额外字段。
    Q3:为什么 GrantedAccess 存在于每个 Entry 中,而不是对象头里?

  • 权限是 per-handle 的 :同一对象可以被多个进程以不同权限打开(例如进程 A 以 GENERIC_READ 打开文件 F,进程 B 以 GENERIC_ALL 打开同一文件)。若把权限放在对象头里,就无法区分它们。

  • 继承与复制语义NtDuplicateObject 允许以不同权限复制句柄到目标进程,这必须以 per-handle 方式存储。

  • 最小权限原则 :即使拥有对对象的完全权限,打开时也应该只申请需要的权限。每个句柄独立记录 GrantedAccess,正是实现该原则的基础。

Q4:为什么需要 Kernel Handle(单独的系统句柄表)?

  • 生命周期解耦 :驱动打开的对象不应随某个用户进程退出而被关闭。系统句柄表属于 PsInitialSystemProcess,永不退出。
  • 安全隔离 :用户态无法引用内核句柄(由 KernelFlag 位拦截),防止用户无意或恶意关闭驱动句柄。
  • 跨进程一致:驱动可以在任意线程上下文中使用同一个内核句柄值,无需担心当前进程的句柄表是否不同。

4.3.10 增强子节:句柄表的并发与锁

设计意图

句柄表是每个进程最频繁被触及的内核结构之一:线程创建/退出、文件打开/关闭、事件/互斥体引用......多个线程可能同时对句柄表进行分配、引用、关闭操作。如果句柄表被单一的全局自旋锁保护,高并发场景下锁竞争将成为严重瓶颈。ReactOS 的设计目标是:在保证正确性的前提下,尽可能降低锁粒度与持有时间。

锁层次一览

ReactOS 使用三层并发机制来保护句柄表:

层次 锁机制 用途 源码位置
1. 表级分段锁 EX_PUSH_LOCK HandleTableLock[4] 保护空闲链表、表扩容、HandleCount 更新。共 4 把,按句柄值哈希到不同锁以减少冲突 HANDLE_TABLE
2. Entry 级锁位 EXHANDLE_TABLE_ENTRY_LOCK_BIT(Object 字段低 1 位) 保护单个 Entry 的字段读写;使用 Interlocked 原子操作,不占额外空间 ntoskrnl/ex/handle.c:882-940(file:///d:/reactos/ntoskrnl/ex/handle.c#L882-L940)
3. 等待/唤醒 EX_PUSH_LOCK HandleContentionEvent 当 Entry 被锁而无法立刻获取时,线程在此等待;解锁者通过它唤醒等待者 HANDLE_TABLE

Entry 级锁的精妙之处

ExpLockHandleTableEntry 通过对 Entry.Object 字段进行 InterlockedCompareExchangePointer 来原子地清除 EXHANDLE_TABLE_ENTRY_LOCK_BIT。这样的设计:

  • 零额外空间 :锁位寄生在 Object 指针的最低位上(因为 OBJECT_HEADER 是 8 字节对齐的,最低 3 位总是 0);
  • 细粒度:两个线程若分别访问不同句柄,根本不会相互阻塞;
  • 无锁常见路径 :对于只是查 Entry 的路径(例如 NtDuplicateObject)只需要锁定所触及的 Entry,不需要拿整表锁;
  • 可重入/可嵌套:因为每个 Entry 有自己独立的锁位。

简化的锁定逻辑:

c 复制代码
BOOLEAN ExpLockHandleTableEntry(PHANDLE_TABLE HandleTable,
                                PHANDLE_TABLE_ENTRY HandleTableEntry)
{
    for (;;) {
        OldValue = *(volatile LONG_PTR *)&HandleTableEntry->Object;
        if (OldValue & EXHANDLE_TABLE_ENTRY_LOCK_BIT) {
            // 未被锁 → 清除锁位以表示锁定
            NewValue = OldValue & ~EXHANDLE_TABLE_ENTRY_LOCK_BIT;
            if (InterlockedCompareExchangePointer(
                    &HandleTableEntry->Object,
                    (PVOID)NewValue, (PVOID)OldValue) == (PVOID)OldValue) {
                return TRUE;    // 成功获取锁
            }
        } else {
            if (!OldValue) return FALSE;  // Entry 已空闲(对象指针为 NULL)
            // 已被他人锁住 → 阻塞等待
            ExpBlockOnLockedHandleEntry(HandleTable, HandleTableEntry);
        }
    }
}

VOID ExUnlockHandleTableEntry(PHANDLE_TABLE HandleTable,
                              PHANDLE_TABLE_ENTRY HandleTableEntry)
{
    // 重新置位 LOCK_BIT(=1 表示解锁)并唤醒等待者
    InterlockedOr((PLONG)&HandleTableEntry->Value, EXHANDLE_TABLE_ENTRY_LOCK_BIT);
    ExfUnblockPushLock(&HandleTable->HandleContentionEvent, NULL);
}

表级锁与空闲链表

ExpAllocateHandleTableEntryntoskrnl/ex/handle.c:681-797(file:///d:/reactos/ntoskrnl/ex/handle.c#L681-L797))在从 FirstFree 取条目时会使用分段锁 HandleTableLock[Index % 4]。这把 PushLock 只在修改空闲链表与 HandleCount 时持有,持有时间非常短。

Rundown 机制(进程退出时清理所有句柄)

当进程准备退出(PspExitProcess)时,系统需要释放它的整个句柄表。这是一个典型的 Rundown 场景:

  1. 阻止新句柄的创建(不再允许从 FirstFree 分配);
  2. 遍历所有已使用句柄,对每个条目执行 ObpCloseHandleTableEntry(递减 HandleCount、关闭对象);
  3. 调用 ExDestroyHandleTable 释放 HANDLE_TABLE 本体及其所有 Entry 页;
  4. 最后通知对象管理器释放所有仅被该进程引用的对象。

为什么 Entry 用 lock bit 而不是 SpinLock 保护整个表?

这是一个经典的「空间 vs 竞争」折中:

  • 如果用一个全局 SpinLock 保护整个 HANDLE_TABLE,在多线程进程中锁的持有者在等待池分配或执行复杂对象清理时会阻塞所有其他句柄操作,扩展性很差。
  • 如果为每个 Entry 都分配一个独立的 SpinLock(4-8 字节),对 16M 句柄表而言就是数百 MB 额外开销。
  • Entry lock bit 折中方案 :利用 Object 指针天然冗余的低位来编码锁状态。既获得了 Entry 级的并发性,又不花费额外空间。代价是锁被持有时必须等待(因为没有独立的等待队列头部,需要使用 HandleContentionEvent 聚合等待)。

4.3.11 增强子节:句柄泄漏检测与调试

什么是句柄泄漏

句柄泄漏 指进程持续创建句柄(打开文件/事件/键/线程等)却忘记调用 CloseHandle / NtClose 释放,导致:

  • HANDLE_TABLE.HandleCount 单调增长;
  • 空闲链表永远得不到补充,系统不断分配新的 Entry 页;
  • 内核池(Paged Pool)被耗尽;
  • 关联的内核对象(FILE_OBJECTKEY_VALUE_ENTRY 等)无法被释放,加剧内存占用。

典型症状:Process Explorer 中看到某个进程的 Handles 列在持续增长而不回落;系统日志出现 STATUS_NO_MEMORYSTATUS_INSUFFICIENT_RESOURCES

WinDbg / kd 调试扩展命令

在 Windows 内核调试器或 ReactOS 调试环境中可用的典型命令:

复制代码
!handle <handle> <flags> <process>   ; 查看单个句柄或所有句柄
!htrace <process> -enable            ; 开启句柄堆栈跟踪(需要 OBJECT_HANDLE_TRACE_INFO)
!htrace <process> -diff              ; 对比两个时间点的句柄差异,找出泄漏点
!object <address>                    ; 查看 OBJECT_HEADER 及其类型/名称
!process <eprocess> 0                ; 列出进程 EPROCESS 及其句柄表信息

ReactOS 源码中 HANDLE_TRACE_DEBUG_INFO 结构(见 sdk/include/ndk/extypes.h:751-759(file:///d:/reactos/sdk/include/ndk/extypes.h#L751-L759))提供了 DebugInfo 扩展字段,用于记录句柄创建时的调用栈。

Process Explorer / Process Hacker

Process Explorer(Sysinternals)与 Process Hacker 可显示:

  • 每个进程打开的句柄数量、类型分布(File/Key/Thread/Event/Mutant...);
  • 单个句柄的 GrantedAccess、对象名称、在内核中的地址;
  • 句柄随时间变化的图表(便于发现泄漏趋势)。

OBJECT_HEADER 中的计数

OBJECT_HEADER 维护两个独立的引用计数(见 sdk/include/ndk/obtypes.h:485-505(file:///d:/reactos/sdk/include/ndk/obtypes.h#L485-L505)):

c 复制代码
typedef struct _OBJECT_HEADER
{
    LONG_PTR PointerCount;      // 指针引用数(每 ObReferenceObject +1)
    union {
        LONG_PTR HandleCount;   // 句柄引用数(每 ObpCreateHandle +1)
        volatile PVOID NextToFree;
    };
    POBJECT_TYPE Type;
    UCHAR NameInfoOffset;
    UCHAR HandleInfoOffset;
    UCHAR QuotaInfoOffset;
    UCHAR Flags;
    ...
    QUAD Body;                  // 对象体(KEVENT / FILE_OBJECT / ...)
} OBJECT_HEADER, *POBJECT_HEADER;
  • HandleCount 表示该对象被多少个句柄引用;当它归零时,对象将被「断开连接」(从命名空间移除、关闭文件链接等)。
  • PointerCount 表示对象头被引用的总次数(包含句柄引用 + 内部 ObReferenceObject 指针引用);当它归零时,对象体与对象头被释放回池。

对象类型 OBJECT_TYPE 还维护了全系统 TotalNumberOfObjectsTotalNumberOfHandles 计数器(见 sdk/include/ndk/obtypes.h:386-393(file:///d:/reactos/sdk/include/ndk/obtypes.h#L386-L393)),可用于判断是「哪种对象在泄漏」。

NtQueryInformationObject 的 HandleFlag 信息类

NtQueryInformationObject(见 ObjectBasicInformationObjectHandleFlagInformation 等信息类)允许用户态或内核查询对象的基本属性。OBJECT_HANDLE_ATTRIBUTE_INFORMATIONsdk/include/ndk/obtypes.h:270-275(file:///d:/reactos/sdk/include/ndk/obtypes.h#L270-L275))返回指定句柄是否可继承、是否受保护关闭:

c 复制代码
typedef struct _OBJECT_HANDLE_ATTRIBUTE_INFORMATION
{
    BOOLEAN Inherit;            // 是否设置了 OBJ_INHERIT
    BOOLEAN ProtectFromClose;   // 是否受 ProtectFromClose 保护
} OBJECT_HANDLE_ATTRIBUTE_INFORMATION, *POBJECT_HANDLE_ATTRIBUTE_INFORMATION;

为什么 NtClose 一个无效句柄会崩溃(在调试版中)

调试构建下 NtClose 对无效句柄不是返回错误而是引发用户态异常(或 KeBugCheckEx),这是一个有意的设计:让开发人员更早地发现句柄泄漏/误用。正常的 Release 构建中则返回 STATUS_INVALID_HANDLE 错误码让程序自行处理。


4.3.12 增强子节:DuplicateHandle 与跨进程句柄传递

NtDuplicateObject 的实现流程

用户态 DuplicateHandle(hSourceProc, hSource, hTargetProc, &hTarget, access, inherit, options) 在底层调用 NtDuplicateObject,后者在 ntoskrnl/ob/obhandle.c:3408-3521(file:///d:/reactos/ntoskrnl/ob/obhandle.c#L3408-L3521) 实现。核心流程:

  1. 验证源进程句柄ObReferenceObjectByHandle(SourceProcessHandle, PROCESS_DUP_HANDLE, PsProcessType, ...) ------ 调用者必须对源进程拥有 PROCESS_DUP_HANDLE 权限。
  2. 验证目标进程句柄 (如果提供):同样要求对目标进程拥有 PROCESS_DUP_HANDLE 权限。
  3. 查源句柄对应的对象 :在源进程句柄表中定位 SourceHandle,得到 OBJECT_HEADERGrantedAccess
  4. 在目标进程句柄表中创建新条目 :调用 ObpCreateHandle(或底层 ExCreateHandle),写入目标进程自己的 HANDLE_TABLE_ENTRY
  5. 写入输出句柄 :如果提供了 TargetHandle 输出参数,将新句柄值写入用户态缓冲区(受 SEH 保护)。
  6. 清理与引用计数处理ObDereferenceObject 源进程/目标进程对象。

简化伪代码:

c 复制代码
NTSTATUS NtDuplicateObject(HANDLE SourceProcessHandle, HANDLE SourceHandle,
                           HANDLE TargetProcessHandle, PHANDLE TargetHandle,
                           ACCESS_MASK DesiredAccess, ULONG HandleAttributes, ULONG Options)
{
    // 1. 引用源进程(需要 PROCESS_DUP_HANDLE 权限)
    Status = ObReferenceObjectByHandle(SourceProcessHandle, PROCESS_DUP_HANDLE,
                                       PsProcessType, PreviousMode,
                                       (PVOID*)&SourceProcess, NULL);
    if (!NT_SUCCESS(Status)) return Status;

    // 2. 引用目标进程(如果提供)
    if (TargetProcessHandle) {
        Status = ObReferenceObjectByHandle(TargetProcessHandle, PROCESS_DUP_HANDLE,
                                           PsProcessType, PreviousMode,
                                           (PVOID*)&TargetProcess, NULL);
    } else {
        TargetProcess = NULL;  // 表示目标为当前进程(或特殊语义)
    }

    // 3. 在目标进程中创建新句柄
    Status = ObDuplicateObject(SourceProcess, SourceHandle, TargetProcess,
                               &NewHandle, DesiredAccess, HandleAttributes,
                               Options, PreviousMode);

    // 4. 把新句柄值写回用户态
    if (TargetHandle && NT_SUCCESS(Status)) {
        _SEH2_TRY {
            *TargetHandle = NewHandle;
        } _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER) {
            Status = _SEH2_GetExceptionCode();
        } _SEH2_END;
    }

    // 5. 释放对源/目标进程的引用
    if (TargetProcess) ObDereferenceObject(TargetProcess);
    ObDereferenceObject(SourceProcess);
    return Status;
}

Options 标志位

Options 参数由下列标志按位 OR 组合:

标志 含义
0 --- 复制句柄;新句柄将拥有 DesiredAccess 权限(可以比源句柄更严格)
DUPLICATE_CLOSE_SOURCE 0x00000001 复制后自动关闭源句柄(相当于原子「移动」句柄)
DUPLICATE_SAME_ACCESS 0x00000002 忽略 DesiredAccess 参数,使用源句柄原有的 GrantedAccess
DUPLICATE_SAME_ATTRIBUTES 0x00000004 复制时保留源句柄的 OBJ_INHERIT / OBJ_PROTECT_CLOSE 等属性

跨进程句柄传递的用途

  • 父子进程通信:父进程创建一对命名/匿名管道(或 Section 对象),把子端句柄复制到子进程句柄表中,从而建立 IPC 通道。
  • 服务向应用传递文件句柄:系统服务以 SYSTEM 权限打开一个特权文件,然后把文件句柄复制到普通用户应用的句柄表中,实现「代理打开」。
  • 调试器 :Windows 调试器需要对被调试进程的句柄进行枚举与操作(读写内存、挂起线程),通过 DuplicateHandle 从目标进程复制出线程/内存句柄。

为什么不能直接传递 HANDLE 值?(为什么需要 NtDuplicateObject?)

一个看似简单的问题------为什么不能直接把源进程中 hFile = 0x24 的值作为参数传递给目标进程,让目标进程直接使用它?答案是:句柄值是进程私有的索引值,不是全局标识符。

两个进程中的 HANDLE = 0x24 指向各自 HANDLE_TABLE 中完全不同的 HANDLE_TABLE_ENTRY,进而指向完全不同的内核对象。跨进程传递句柄必须由内核亲自做两件事:

  1. 在源进程句柄表中查找 HANDLE 值 → 得到对象指针;
  2. 在目标进程句柄表中分配一个新 Entry → 写入同一对象指针,并递增 HandleCount

NtDuplicateObject 正是完成这两个步骤的唯一合法路径。


4.3.13 小结(增强版)

本节核心知识点回顾

  1. HANDLE 是一个位编码的索引值 ,由 EXHANDLE 解析;最低 2 位 TagBits 恒为 0,使得 NULL / INVALID_HANDLE_VALUE 等非法值可被快速识别;KernelFlag 高位标识内核句柄。
  2. HANDLE_TABLE 使用三级页表式结构TableCode 的低 2 位编码层级(0/1/2),高位为实际指针;稀疏地支持从 256 → 256k → 16M 句柄。
  3. HANDLE_TABLE_ENTRY 每个条目 16 字节 :第一个指针大小字段存 Object 指针(低 3 位兼做锁位);第二个 32 位字段在使用时存 GrantedAccess,空闲时存空闲链表 NextFreeTableEntry
  4. 句柄创建走 ObpCreateHandle → ExCreateHandle → ExpAllocateHandleTableEntry ;句柄引用走 ObReferenceObjectByHandle → ExpLookupHandleTableEntry;句柄关闭走 ObCloseHandle → ObpCloseHandleTableEntry → ExDestroyHandle
  5. 内核句柄(Kernel Handle) 使用独立的 ObpKernelHandleTable,由 KernelFlag 位标识,用户态无法引用。
  6. 句柄表并发 :表级分段 PushLock(4 把)+ Entry 级 lock bit(在 Object 指针低位)的双层锁设计,在保证正确性的前提下实现高并发。
  7. 句柄泄漏 :未关闭句柄导致 HandleCount 持续增长;可通过 !handle、Process Explorer、对象类型计数器进行定位。
  8. 跨进程句柄传递 :只能通过 NtDuplicateObject;不能直接传递 HANDLE 值,因为它是进程私有索引。

对照表 1:HANDLE 值各字段含义(32 位系统)

位段 宽度 含义
TagBits 0-1 恒为 0;用于快速识别 NULL 等非法句柄
LowIndex 2-10 Entry 在 Entry 页中的索引(9 位,0-511)
MidIndex 11-20 Mid Table 索引(10 位,0-1023);Level 0 时未使用
HighIndex 21-30 High Table 索引(10 位);Level 0/1 时未使用
KernelFlag 31 内核句柄标志;0=普通句柄,1=内核句柄

对照表 2:句柄操作 API vs 内核实现函数

Win32 API / Nt 系列 对象管理器层 执行体层(Ex) 功能
打开对象(如 CreateFileW ObInsertObject → ObpCreateHandle ExCreateHandle → ExpAllocateHandleTableEntry 在当前进程句柄表创建新条目并返回 HANDLE 值
NtSetInformationObject / 使用句柄 ObReferenceObjectByHandle ExpLookupHandleTableEntry 查句柄表、验证权限和类型、递增 PointerCount、返回对象体指针
CloseHandle(h) ObCloseHandle → NtClose → ObpCloseHandleTableEntry ExDestroyHandle → ExpFreeHandleTableEntry 定位 Entry、调用对象类型 CloseProcedure、递减 HandleCount、将 Entry 归还给空闲链表
DuplicateHandle NtDuplicateObject → ObDuplicateObject → ObpCreateHandle ExCreateHandle 从源进程句柄表查对象、在目标进程创建新句柄条目

后续章节展望(4.4 对象的创建 · 4.5 常用内核函数)

本节详细讨论了句柄表的数据结构、生命周期和并发控制。然而,对象管理器的知识体系在「4.3 句柄表」之后并未结束。第 4 章还包含以下两节深入内容,它们是读者真正上手阅读 ReactOS 源码时最常查阅的章节:

4.4 对象的创建(Object Creation)

这一节将把前面 4.1 ~ 4.3 分别讨论的「对象头」「对象类型」「句柄表」三者串联起来,形成一条完整的调用链:

  • ObCreateObjectObInsertObject :一个内核对象如何从分配池内存、初始化 OBJECT_HEADER、插入对象目录,到最终生成用户态句柄的完整流程。
  • 参数验证与安全检查OBJECT_ATTRIBUTES 如何被解析、DesiredAccess 如何被校验、安全描述符如何被应用。
  • 配额扣除与还原 :对象创建时如何从进程配额中扣除 NonPagedPoolCharge / PagedPoolCharge;失败时如何正确回滚。
  • 创建信息(CreateInfo)与审计OB_CREATE_INFO 结构如何记录本次创建的元信息;哪些类型会触发审计日志。
  • 多阶段创建的代码追踪 :对比「NtCreateEvent」「NtCreateFile」「NtCreateProcess」三类典型对象创建 API,归纳它们在对象管理器层的共性与差异。

4.5 几个常用的内核函数

阅读 ReactOS 源码时,以下三个函数是读者在 ntoskrnl/ob/ 与其他子系统(psiose)中最频繁遇到的「基础设施函数」。本节将对它们逐行拆解:

  • 4.5.1 ObReferenceObjectByHandle() --- 用户态 HANDLE → 内核对象指针的标准转换函数。它在 NtReadFileNtWaitForSingleObjectNtQueryInformationProcess 等几乎所有接受句柄参数的系统调用中被第一行调用。本节将逐行分析其参数验证、Entry 查找、权限检查、类型检查、引用计数递增的完整流程,并特别关注「调用者必须显式 ObDereferenceObject」这一容易出错的规则。

  • 4.5.2 ObReferenceObjectByPointer() --- 在内核态已有对象体指针但需要把它「转换为带引用计数的可靠指针」时使用。典型场景:驱动程序内部拿到对象指针后,在访问前调用一次 ObReferenceObjectByPointer 以安全地递增 PointerCount,防止对象在操作期间被释放。本节将讨论它与 ObReferenceObjectByHandle 的差异------不查句柄表、不走权限检查、但必须保证对象指针的类型正确性。

  • 4.5.3 ObpLookupEntryDirectory() --- 名称查找引擎的核心。在 4.1 节讨论命名空间时已提及,但本节将从代码层面逐段解析:如何在 OBJECT_DIRECTORY 的 37 个 Hash Bucket 中查找指定名称;如何处理大小写不敏感的比较;如何在找到 OBJECT_DIRECTORY_ENTRY 后递增 LookupContext;如何在查找失败时回退创建新条目;以及 OBP_LOOKUP_CONTEXT 如何缓存查找上下文以加速「先查后创建」的操作。

这三节读完后,读者应当能够独立阅读 ReactOS 中任何一条涉及对象管理的调用链,例如 NtCreateFile 内部如何调用 ObCreateObjectObpLookupObjectNameObInsertObjectObpCreateHandle 的完整链路。

相关推荐
Selina K2 小时前
C中日历时间转换
c语言·开发语言
故渊at2 小时前
第二板块:Android 四大组件标准化学理 | 第六篇:四大组件架构总论与 Manifest 规范
android·架构·zygote·manifest·四大组件
李燚2 小时前
erlang_migrate 架构拆解:behaviour 驱动的多数据库迁移引擎
数据库·postgresql·架构·erlang·migrate·behaviour·erlang_migrate
Chase_______2 小时前
【Java基础 | 15】集合框架(中):Set、HashSet、TreeSet 与哈希表
java·windows·散列表
caimouse2 小时前
Windows NT 内核架构(主通用模型)流 NT 5.x/10+
windows·架构
caimouse3 小时前
Reactos 第 3 章 内存管理 — 【中篇】Hyperspace、系统空间、API 与异常
c语言·开发语言·windows·架构
ysu_03143 小时前
leetcode数据结构与算法1~4
c语言·数据结构·学习·算法·leetcode
zzz_23683 小时前
【RabbitMQ】面试系列 · 第三期:从线上故障到架构选型
面试·架构·rabbitmq
提子拌饭1334 小时前
爆发效果技术——基于鸿蒙PC Electron框架实现
华为·架构·electron·开源·harmonyos·鸿蒙·鸿蒙系统