Reactos 第 4 章 对象管理 — 4.1 对象与对象目录

第 4 章 对象管理 --- 4.1 对象与对象目录

4.1.0 框架图

下图展示了从用户态创建一个内核对象(以事件对象 Event 为例)到最终返回句柄的完整流程。整个流程横跨用户态、系统调用层、对象管理器与内核内存池四个层次:

复制代码
┌──────────────────────────────────────────────────────────────────────────┐
│                         用户态 (User Mode)                                │
│                                                                          │
│   HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, L"MyEvent");           │
│                              │                                           │
│                              ▼ 系统调用 (syscall)                         │
├──────────────────────────────────────────────────────────────────────────┤
│                         内核态 (Kernel Mode)                              │
│                                                                          │
│   NtCreateEvent(...)                                                     │
│        │                                                                 │
│        ▼                                                                 │
│   ObCreateObject(ProbeMode, EventType, ObjectAttrs, ..., &ObjectBody)    │
│        │                                                                 │
│        ├───① 计算总分配大小: Header + Info块 + Body                       │
│        ├───② ExAllocatePoolWithTag(NonPagedPool, TotalSize, 'even')      │
│        ├───③ 初始化 OBJECT_HEADER (PointerCount=1, HandleCount=0, ...)   │
│        └───④ 返回指向 Body 的指针 (*Object = &Header->Body)              │
│                                                                          │
│                              │                                           │
│                              ▼ 后续继续执行:                              │
│   ┌──────────────────────────────────────────────────────────────────┐   │
│   │    KeInitializeEvent(&Header->Body, ...)  // 初始化对象体内部字段   │   │
│   └──────────────────────────────────────────────────────────────────┘   │
│                              │                                           │
│                              ▼                                           │
│   ObInsertObject(ObjectBody, ..., &Handle)                               │
│        │                                                                 │
│        ├───① 在对象目录中查找名称 "\BaseNamedObjects\MyEvent"            │
│        ├───② 若名称已存在 → STATUS_OBJECT_NAME_COLLISION                 │
│        ├───③ 若名称不存在 → ObpInsertEntryDirectory 插入到 Hash Bucket    │
│        └───④ 在进程句柄表中分配句柄 (Handle Table Entry)                  │
│                                                                          │
│                              │                                           │
│                              ▼                                           │
│   return STATUS_SUCCESS;  // 返回句柄值 (如 0x48) 到用户态                │
│                              │                                           │
│                              ▼                                           │
│   ┌──────────────────────────────────────────────────────────────────┐   │
│   │  用户态返回后 hEvent = 0x48,后续使用 WaitForSingleObject(hEvent) │   │
│   │  → NtWaitForSingleObject → ObReferenceObjectByHandle → 取对象指针│   │
│   └──────────────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────────────┘


  内存布局(低地址 → 高地址,示意):
  ┌──────────────────────────────────────────────────────────────┐
  │ [CreatorInfo] [NameInfo] [HandleInfo] [QuotaInfo] [Header] [Body]│
  │    (可选)      (可选)     (可选)     (可选)      固定      可变│
  │◄────────────────────负偏移量定位──────────────────►              │
  │                         Header.NameInfoOffset = 距离 Header 的字节数│
  └──────────────────────────────────────────────────────────────┘

  对象目录(Hash 表):
  ┌──────────┐
  │ Bucket 0 │→ Entry{Object: "\BaseNamedObjects"} → Entry → NULL
  ├──────────┤
  │ Bucket 1 │→ NULL
  ├──────────┤
  │   ...    │
  ├──────────┤
  │ Bucket N │→ Entry{Object: "MyEvent"} → Entry{Object: "OtherEvent"} → NULL
  └──────────┘
  (共 37 个 Bucket)

图解说明

  1. 用户态 → 内核态CreateEvent 是 Win32 API,内部通过 ntdll!NtCreateEvent 进入内核。内核中 NtCreateEvent 是系统调用处理函数。从图中可以看出,用户态看到的 HANDLE 只是一个整型值(如 0x48),而内核态把它映射到完整的 OBJECT_HEADER + KEVENT 对象体结构。

  2. 对象创建ObCreateObject(定义于 ntoskrnl/ob/oblife.c(file:///d:/reactos/ntoskrnl/ob/oblife.c#L1037-L1132))负责从池中分配 OBJECT_HEADER + 各 Info 块 + 对象体(Body),并返回 Body 指针。这里的关键点在于一次分配 ------Header、Info 块、Body 在同一次 ExAllocatePoolWithTag 调用中连成一片连续内存,而不是各自独立分配。这避免了碎片化,同时也保证了 Header 与 Body 之间有固定的位置偏移。

  3. 对象初始化 :对象体内部字段(如事件对象的 KEVENT 结构)由类型专属的初始化函数完成(例如 KeInitializeEvent)。这一步在 ObCreateObject 返回之后由调用方执行------对象管理器只负责"外壳"(Header + Info 块 + Body 的分配),不关心 Body 内部的具体语义。

  4. 命名空间插入ObInsertObject 会调用 ObpLookupObjectNameObpInsertEntryDirectory 将对象插入到 OBJECT_DIRECTORY 的 Hash Bucket 链中。图中的 37 个 Bucket 数组表示一个 Hash 表------每个 Bucket 指向一个单链表。\BaseNamedObjects\MyEvent 经过 ObpLookupObjectName 先逐段解析出 \BaseNamedObjects 目录,再在其中查找 MyEvent。这是一个逐层遍历的过程,完全类比文件系统的路径解析。

  5. 句柄分配 :在当前进程的句柄表(Handle Table)中分配一个 Entry,将其指向该对象的 OBJECT_HEADER。在第 4.3 节中我们会看到,句柄表采用三层索引结构(类似 x86 页表),每个 Entry 8 字节,包含 Object 指针 + GrantedAccess 权限掩码。用户态得到的 0x48 只是一个索引值,并不包含任何直接的地址信息------这使得用户态无法猜测其他对象的地址。

  6. 使用与回收 :用户态通过句柄操作对象;关闭句柄时 ObDereferenceObject 递减 PointerCount,归零后调用 ObpDeleteObject 释放内存。注意图中的虚线箭头(从用户态到对象的间接引用)表示安全边界------用户态永远无法直接拿到对象内核态指针,只能通过句柄表这个"安全网关"间接引用。

图中的内存布局部分 展示了 CreatorInfoNameInfoHandleInfoQuotaInfo 四个 Info 块在 Header 之前的排列顺序。这些 Info 块通过 Header->NameInfoOffsetUCHAR 偏移量定位,偏移量的单位是字节。值得注意的一点是:这些 Info 块不一定都存在 。只有对象在创建时被判定"需要"某个 Info 块,它才会被包含在一次性分配中。例如匿名对象不会有 NameInfo,不计配额的进程内对象不会有 QuotaInfo。这体现了"按需分配,避免浪费"的设计原则。

Hash 表部分 展示了对象目录内部的 37 个 Bucket。\BaseNamedObjects 是一个中间目录对象,它自身也有一个 37-Bucket 的 Hash 表------对象目录是嵌套的 ,每个子目录都是一个独立的 OBJECT_DIRECTORY 实例。这种嵌套结构与文件系统的目录嵌套完全同构,语义上也完全一致。


4.1.0.1 设计意图

核心问题

操作系统中有大量不同种类的内核资源:进程、线程、文件、事件、互斥量、信号量、定时器、管道、设备对象、驱动对象、注册表键、段对象、端口...... 如果每种对象都用一套独立的数据结构和管理方式,会导致以下问题:

  1. 命名冲突 :不同模块使用完全不同的字符串查找逻辑,难以支持全局唯一名称(如命名事件 Global\\MyEvent)。
  2. 生命周期混乱:每个模块自行实现引用计数,容易出现竞态和泄漏。
  3. 安全策略分散:访问检查、审计、配额限制散落在各处,难以统一管理。
  4. 类型安全缺失 :用户态传入的 HANDLE 仅为一个整数值,内核需要机制防止误用。
  5. 调试与诊断困难:没有统一的查询接口,难以回答"系统中当前有哪些打开的对象"。

设计哲学

对象管理器(Object Manager,Ob)的设计哲学可以概括为**"在操作系统内部建立一套统一的命名、生命周期、安全与类型系统"**,它借鉴了面向对象语言中"基类 / 派生类"的思想,但用纯 C 语言实现:

  • OBJECT_HEADER 是所有对象的"公共基类",提供引用计数、标志位、安全描述符等公共字段。
  • 具体对象体(Body) 是"派生类",如 EPROCESSFILE_OBJECTKEVENT,通过 OBJECT_TO_OBJECT_HEADER(body) 从对象体指针回溯到对象头。
  • OBJECT_TYPE 是"类型对象",描述一类对象的元信息(名称、回调、池标记等)。
  • 对象目录(OBJECT_DIRECTORY 是内核中的"文件系统",提供统一的命名空间。

本节定位

本节聚焦于对象管理器的基础结构与命名空间,涵盖:

  • 对象头 OBJECT_HEADER 的内存布局与 Info 块机制;
  • 对象创建 (ObCreateObject) 与引用计数 (ObReferenceObjectByPointer / ObDereferenceObject) 的生命周期;
  • 对象目录的 Hash 查找与命名空间遍历 (ObpLookupObjectName, ObpInsertEntryDirectory);
  • 符号链接与 \??\ 目录的设备名映射。

对象类型(OBJECT_TYPE 的详细结构、回调机制)与句柄表(Handle Table 的实现细节)将在后续小节(4.2、4.3)深入。


4.1.1 为什么需要「对象管理器」

从用户态代码出发

考虑以下三段常见的用户态 C 代码:

c 复制代码
// 示例 1:创建一个命名事件,供跨进程共享
HANDLE hEvent = CreateEventW(
    NULL,                   // 默认安全描述符
    FALSE,                  // 手动重置?否
    FALSE,                  // 初始状态:无信号
    L"Global\\MySharedEvent"  // 全局命名空间中的名称
);
WaitForSingleObject(hEvent, INFINITE);
CloseHandle(hEvent);

// 示例 2:打开一个文件
HANDLE hFile = CreateFileW(
    L"C:\\Users\\Me\\test.txt",
    GENERIC_READ,
    FILE_SHARE_READ,
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    NULL
);
ReadFile(hFile, ...);
CloseHandle(hFile);

// 示例 3:创建一个新进程
STARTUPINFOW si = {sizeof(si)};
PROCESS_INFORMATION pi;
CreateProcessW(L"C:\\Windows\\system32\\notepad.exe", ..., &si, &pi);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);

这三个操作看似毫无关联:事件、文件、进程。但它们共享如下一致的模式

模式 Win32 语义 内核实现
统一的"句柄"类型 HANDLEtypedef void *HANDLE 句柄表索引 + 标记位
一致的命名语义 CreateEventW 的最后一个参数是名称,CreateFileW 的第一个参数也是名称(路径) 对象命名空间 \??\\Device\HarddiskVolume1\...
一致的生命周期 CloseHandle 对任何句柄都有效 NtCloseObDereferenceObject 统一处理
一致的安全语义 SECURITY_ATTRIBUTES 统一作为首参数 ObpCaptureObjectCreateInformation 捕获 SD
一致的查询/诊断接口 GetHandleInformation, QueryObject NtQueryObject 基于 OBJECT_HEADER 的公共字段返回信息

如果没有对象管理器 ,那么 NtCreateEventNtCreateFileNtCreateProcess 每个都需要自行实现:

  • 字符串解析与名称查找(重复代码);
  • 引用计数(分散的 LONG volatile 字段,难以统一调试);
  • 安全描述符解析(在每个系统调用里都写一份 SeCaptureSecurityDescriptor);
  • 池分配与释放(无统一的配额计费)。

这就是 ReactOS 选择统一的「对象管理器」的根本原因。

对象管理器提供的统一服务

服务 用途 核心函数 / 结构
命名空间 允许内核对象像文件系统路径一样命名和查找 OBJECT_DIRECTORY, ObpLookupObjectName
引用计数 多线程安全的对象生命周期管理 PointerCount, ObReferenceObject, ObDereferenceObject
句柄表 将内核对象指针"包装"为对用户态安全的整数索引 HANDLE_TABLE, ExMapHandleToPointer
类型安全 验证句柄指向的对象类型是否匹配调用方的期望 OBJECT_HEADER.Type, ObReferenceObjectByHandle 的类型参数
安全描述符 为每个对象附加自主访问控制列表(DACL) OBJECT_HEADER.SecurityDescriptor, SeAccessCheck
审计 对对象的打开/关闭/删除等操作记录安全审计日志 ObpAuditObjectAccess(审计回调)
配额管理 限制单个进程可分配的分页/非分页池字节数 OBJECT_HEADER_QUOTA_INFO, PsChargePoolQuota
名称解析/符号链接 类似文件系统的 reparse 机制,用于 \??\C:\Device\HarddiskVolume1 OBJECT_SYMBOLIC_LINK, ObpParseSymbolicLink

对象管理器自身的内部组织架构

对象管理器的代码实现分布在 ntoskrnl\ob\ 目录下的约 13 个 C 文件中。从功能上,它可以划分为以下五个子模块:

第一层:对象生命周期管理(Object Lifecycle) --- 代码位于 oblife.c(file:///d:/reactos/ntoskrnl/ob/oblife.c) 和 obref.c(file:///d:/reactos/ntoskrnl/ob/obref.c)。这一层负责对象的创建(ObCreateObject)、销毁(ObpDeleteObject)、引用计数增减(ObReferenceObjectByPointerObDereferenceObject)、以及永久对象与延迟删除的异步清理。它是对象管理器的"心脏"------所有其他模块都通过它来获得或释放对象体指针。

第二层:命名空间管理(Namespace) --- 代码位于 obdir.c(file:///d:/reactos/ntoskrnl/ob/obdir.c)、obname.c(file:///d:/reactos/ntoskrnl/ob/obname.c) 和 oblink.c(file:///d:/reactos/ntoskrnl/ob/oblink.c)。这一层负责对象目录的 Hash 表操作(ObpInsertEntryDirectoryObpDeleteEntryDirectoryObpLookupEntryDirectory)、完整路径解析(ObpLookupObjectName)、符号链接(ObpParseSymbolicLink)、以及 DOS 设备映射(ObpCreateDosDevicesDirectory)。它是对象管理器的"肌肉"------支撑所有命名对象的查找与插入。

第三层:句柄表管理(Handle Table) --- 代码位于 obhandle.c(file:///d:/reactos/ntoskrnl/ob/obhandle.c) 和 ex\\handle.c(file:///d:/reactos/ntoskrnl/ex/handle.c)。这一层负责句柄的创建(ObpCreateHandle)、关闭(ObCloseHandle)、通过句柄查找对象(ObReferenceObjectByHandle)、以及句柄继承与跨进程传递(NtDuplicateObject)。它是对象管理器的"安全闸门"------用户态与内核对象之间的唯一通道。

第四层:安全审计(Security & Audit) --- 代码位于 obsecure.c(file:///d:/reactos/ntoskrnl/ob/obsecure.c) 和 obsdcach.c(file:///d:/reactos/ntoskrnl/ob/obsdcach.c)。这一层负责安全描述符的捕获、缓存和释放,以及对象访问的审计记录。它是对象管理器的"安检站"------每个对象被打开时都要通过它的检查。

第五层:初始化与关闭(Init & Shutdown) --- 代码位于 obinit.c(file:///d:/reactos/ntoskrnl/ob/obinit.c)。这一层负责系统启动时对象管理器的初始化------创建根目录、注册系统对象类型、创建关键命名空间入口。它是对象管理器的"地基"------所有其他模块在系统启动后才能正常运转。

这五层之间的关系可以总结为:初始化层为其他四层准备好运行环境 ;生命周期层提供对象的"生与死";命名空间层管理对象的"身份(名称)";句柄表层管理对象的"钥匙(句柄)";安全审计层管理对象的"门禁(权限)"。每个系统调用(如 NtCreateEvent)会依次经过句柄表层 → 命名空间层 → 安全审计层 → 生命周期层,完成一次完整的对象创建操作。

对模块内部实现来说,对象管理器的代码充分体现了分层与降级 的设计理念:每个函数只做一件事,上层函数通过调用下层函数组合复杂行为。例如 ObInsertObject 先调用 ObpLookupObjectName(查找名称),再调用 ObpCreateHandle(分配句柄),最后调用 ObpIncrementHandleCount(更新统计),每一步都有清晰的语义边界。


4.1.2 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;           // +0x00 指针引用计数
    union                            // +0x08 (x64) / +0x04 (x86)
    {
        LONG_PTR HandleCount;        //       句柄引用计数
        volatile PVOID NextToFree;   //       延迟删除链表指针(重用同一内存)
    };
    POBJECT_TYPE Type;               // +0x10 / +0x08  指向类型对象
    UCHAR NameInfoOffset;            // +0x18 / +0x0C  NameInfo 距 Header 的负偏移(字节),0 表示不存在
    UCHAR HandleInfoOffset;          // +0x19 / +0x0D  HandleInfo 距 Header 的负偏移
    UCHAR QuotaInfoOffset;           // +0x1A / +0x0E  QuotaInfo 距 Header 的负偏移
    UCHAR Flags;                     // +0x1B / +0x0F  OB_FLAG_* 标志位
    union                            // +0x20 / +0x10
    {
        POBJECT_CREATE_INFORMATION ObjectCreateInfo; // 创建过程中的临时信息
        PVOID QuotaBlockCharged;                     // 记账用的配额块指针
    };
    PSECURITY_DESCRIPTOR SecurityDescriptor; // +0x28 / +0x14  安全描述符
    QUAD Body;                       // +0x30 / +0x18  对象体开始位置(对齐占位符)
} OBJECT_HEADER, *POBJECT_HEADER;

注:QUADLONGLONG 的别名,作用是让 Body 字段起于 8 字节对齐边界,确保后续对象体结构自然对齐。

对象头在对象体之前的布局图示

复制代码
  低地址                                                       高地址
  ┌─────────┬──────────┬────────────┬───────────┬────────┬─────────────┐
  │ Creator │   Name   │   Handle   │   Quota   │ Header │    Body     │
  │  Info   │   Info   │    Info    │    Info   │        │ (EPROCESS,   │
  │ (可选)  │  (可选)  │  (可选)    │  (可选)   │ 固定   │ FILE_OBJECT, │
  │         │          │            │           │        │ KEVENT ...)  │
  └─────────┴──────────┴────────────┴───────────┴────────┴─────────────┘
             ▲                              ▲
             │                              │
             │  Header->NameInfoOffset      │  Header
             │  (字节数,例如 0x20)         │
             │                              │
             └────────────── 负偏移量 ──────┘

  Header 自身地址为 pHeader。则:
     NameInfo   = (POBJECT_HEADER_NAME_INFO)((PUCHAR)pHeader - pHeader->NameInfoOffset)
     HandleInfo = (POBJECT_HEADER_HANDLE_INFO)((PUCHAR)pHeader - pHeader->HandleInfoOffset)
     QuotaInfo  = (POBJECT_HEADER_QUOTA_INFO)((PUCHAR)pHeader - pHeader->QuotaInfoOffset)
     CreatorInfo= 固定在 Header 前面 (sizeof(OBJECT_HEADER_CREATOR_INFO)),
                  只在 Flags & OB_FLAG_CREATOR_INFO 时存在

OBJECT_TO_OBJECT_HEADER 宏

这是最重要的"指针回溯"宏,将对象体指针转换为对象头指针。定义于 sdk/include/ndk/obtypes.h:111-112(file:///d:/reactos/sdk/include/ndk/obtypes.h#L111-L112):

c 复制代码
#define OBJECT_TO_OBJECT_HEADER(o)  \
    CONTAINING_RECORD((o), OBJECT_HEADER, Body)

CONTAINING_RECORD(ptr, TYPE, Field) 是经典的 C 语言实现"从成员指针回溯容器指针"的惯用法:

c 复制代码
// 展开后等价于:
(POBJECT_HEADER)((PUCHAR)(o) - FIELD_OFFSET(OBJECT_HEADER, Body))

因为 BodyOBJECT_HEADER 中的偏移是固定的(由编译器决定,通常为 0x30 / 0x18),所以这是一条 O(1) 的减法运算------非常高效。

Info 块的负偏移量定位宏

CreatorInfo 通过 Flags 标志位判断外,其他三个 Info 块都以"负偏移量字节数 + NULL 判定"的方式定位。定义于 sdk/include/ndk/obtypes.h:114-129(file:///d:/reactos/sdk/include/ndk/obtypes.h#L114-L129):

c 复制代码
// NameInfo:存储名称与所在目录指针
#define OBJECT_HEADER_TO_NAME_INFO(h)                       \
    ((POBJECT_HEADER_NAME_INFO)(!(h)->NameInfoOffset ?      \
        NULL: ((PCHAR)(h) - (h)->NameInfoOffset)))

// HandleInfo:记录每个进程对该对象打开的句柄数
#define OBJECT_HEADER_TO_HANDLE_INFO(h)                     \
    ((POBJECT_HEADER_HANDLE_INFO)(!(h)->HandleInfoOffset ?  \
        NULL: ((PCHAR)(h) - (h)->HandleInfoOffset)))

// QuotaInfo:记录池配额计费与独占进程指针
#define OBJECT_HEADER_TO_QUOTA_INFO(h)                      \
    ((POBJECT_HEADER_QUOTA_INFO)(!(h)->QuotaInfoOffset ?    \
        NULL: ((PCHAR)(h) - (h)->QuotaInfoOffset)))

// CreatorInfo:调试信息(类型链表、创建者进程 PID)
#define OBJECT_HEADER_TO_CREATOR_INFO(h)                    \
    ((POBJECT_HEADER_CREATOR_INFO)(!((h)->Flags &           \
        OB_FLAG_CREATOR_INFO) ? NULL: ((PCHAR)(h) -         \
        sizeof(OBJECT_HEADER_CREATOR_INFO))))

设计要点

  • UCHAR 存储偏移量,意味着 Info 块的总大小不得超过 255 字节------这恰好足够容纳 4 个 Info 块(每个 ~40 字节)。
  • Offset == 0 时返回 NULL,表示该对象没有对应 Info 块。
  • CreatorInfo 的位置在 Header 紧前面,所以使用 sizeof(OBJECT_HEADER_CREATOR_INFO) 作为固定负偏移,而非 UCHAR 字段。

OB_FLAG_* 标志位

定义于 sdk/include/ndk/obtypes.h:97-104(file:///d:/reactos/sdk/include/ndk/obtypes.h#L97-L104):

标志位 含义
OB_FLAG_CREATE_INFO 0x01 对象处于创建过程中,ObjectCreateInfo 字段有效(而非 QuotaBlockCharged)。创建完成后会被清除并替换为记账指针。
OB_FLAG_KERNEL_MODE 0x02 该对象由内核模式代码创建,对用户态不可见/不可访问。
OB_FLAG_CREATOR_INFO 0x04 Header 前面存在 OBJECT_HEADER_CREATOR_INFO,记录创建者进程 ID、回溯索引、类型链表节点。
OB_FLAG_EXCLUSIVE 0x08 对象被一个进程独占(例如独占打开的文件)。此时 OBJECT_HEADER_QUOTA_INFO.ExclusiveProcess 指向独占进程。
OB_FLAG_PERMANENT 0x10 永久对象,即使 PointerCount 降到 0 也不被释放,直到调用 NtMakeTemporaryObject 显式解除。典型用例:\ 根目录、\ObjectTypes\?? 等。
OB_FLAG_SECURITY 0x20 该对象存在安全描述符(SecurityDescriptor 字段非 NULL)。
OB_FLAG_SINGLE_PROCESS 0x40 该对象仅由单个进程打开(HandleInfo.SingleEntry 而非数据库)。
OB_FLAG_DEFER_DELETE 0x80 标记对象为延迟删除。删除不立即执行,而是挂入工作队列由 ObpReaperWorkItem 异步清理。

4.1.3 对象的创建:ObCreateObject 全流程

函数签名

ObCreateObject 是对象创建的核心入口,定义于 ntoskrnl/ob/oblife.c:1037-1132(file:///d:/reactos/ntoskrnl/ob/oblife.c#L1037-L1132):

c 复制代码
NTSTATUS
NTAPI
ObCreateObject(
    IN KPROCESSOR_MODE ProbeMode OPTIONAL,     // 探测模式:用户态传入需要 SEH
    IN POBJECT_TYPE Type,                       // 对象类型(如 PsProcessType, IoFileObjectType)
    IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,  // 对象属性(名称、SD、RootDirectory)
    IN KPROCESSOR_MODE AccessMode,              // 访问模式(KernelMode / UserMode)
    IN OUT PVOID ParseContext OPTIONAL,         // 解析上下文(供 ParseProcedure 使用)
    IN ULONG ObjectSize,                        // 对象体大小(如 sizeof(EPROCESS))
    IN ULONG PagedPoolCharge OPTIONAL,          // 分页池配额(默认用 Type 的默认值)
    IN ULONG NonPagedPoolCharge OPTIONAL,       // 非分页池配额
    OUT PVOID *Object                           // 输出:对象体指针
);

内部流程分解

  1. 步骤 1:分配并填充 OBJECT_CREATE_INFORMATION

    ObCreateObject 首先通过 lookaside list 分配一个 OBJECT_CREATE_INFORMATION(OCI)缓冲区,然后调用 ObpCaptureObjectCreateInformation(见 ntoskrnl/ob/oblife.c:453-598(file:///d:/reactos/ntoskrnl/ob/oblife.c#L453-L598)):

    • ObjectAttributes 中提取 RootDirectoryAttributes 标志;
    • 捕获 SecurityDescriptor 并计算其大小计入配额;
    • 捕获安全 QoS(Quality of Service);
    • 捕获对象名称到独立缓冲区(后续放入 NameInfo)。
  2. 步骤 2:从池中分配 Header + Info 块 + Body

    实际分配由 ObpAllocateObject(见 ntoskrnl/ob/oblife.c:609-869(file:///d:/reactos/ntoskrnl/ob/oblife.c#L609-L869))完成。该函数:

    • 按条件计算是否需要 4 个 Info 块:

      复制代码
      NameInfo   : ObjectName->Buffer != NULL  ? 1 : 0
      HandleInfo : Type->MaintainHandleCount   ? 1 : 0
      QuotaInfo  : 配额与类型默认值不同 / OBJ_EXCLUSIVE / 用户态创建 ? 1 : 0
      CreatorInfo: Type->MaintainTypeList      ? 1 : 0
    • 计算总大小 = sizeof(CreatorInfo) + sizeof(NameInfo) + sizeof(HandleInfo) + sizeof(QuotaInfo) + FIELD_OFFSET(OBJECT_HEADER, Body) + ObjectSize

    • 调用 ExAllocatePoolWithTag(PoolType, TotalSize, Tag) 一次性分配;

    • 分别初始化各 Info 块并递增 ObpObjectsCreatedObpObjectsWithNameObpObjectsWithPoolQuotaObpObjectsWithHandleDBObpObjectsWithCreatorInfo 等全局计数器;

    • 设置 Header 字段:PointerCount = 1HandleCount = 0Type = ObjectTypeFlags = OB_FLAG_CREATE_INFO | ...*InfoOffset = 计算得到的偏移量

    • 递增 Type->TotalNumberOfObjects

  3. 步骤 3:返回 Body 指针

    *Object = &Header->Body;,即把"对象头之后的第一个字节"返回给调用者,调用者将其作为具体对象(如 PEVENTPFILE_OBJECT)使用。

  4. 步骤 4:永久对象特权检查

    如果 Header->Flags & OB_FLAG_PERMANENT,且调用方来自用户态,会进行 SeCreatePermanentPrivilege 特权检查------普通用户无法创建永久对象。

设计解读:为什么 ObCreateObject 只做"壳"不做"肉"?

初学者在看到 ObCreateObject 的步骤分解后,可能会有一个疑问:为什么对象管理器只分配 Header + Info 块 + Body 的空间、初始化的却只有 Header 的公共字段(PointerCount、Type、Flags),而不初始化 Body 内部的任何字段?

这背后的设计哲学是关注点分离(Separation of Concerns)

对象管理器是"通用基础设施",它不知道某个 Body 的具体类型语义------FILE_OBJECTFileNameDeviceObject 等字段;KEVENTHeader(DISPATCHER_HEADER)字段;EPROCESSImageFileNamePebAddressSpace 等几十个字段。如果对象管理器试图去初始化所有这些字段,它就需要"了解"每一个类型------这会导致紧耦合、巨大的 switch-case 语句、以及每次新增类型都需要修改对象管理器。

正确的分工方式应该是:

  1. 对象管理器 (Ob 模块)只负责所有对象共用的部分------分配内存、设置引用计数、关联类型指针、挂 Info 块、插入命名空间。这些对所有类型都一样。
  2. 类型创建函数 (如 NtCreateEvent 中的 KeInitializeEventNtCreateProcess 中的 PspInitializeProcess)负责初始化 Body 中的类型专属字段。

这种分工的直接体现是:ObCreateObject 的参数中需要调用者传入 ObjectSize(Body 大小),但不需要调用者描述"Body 的结构是什么"------对象管理器不知道也不关心 Body 的结构,它只需要知道 Body 在内存中占多少字节。

所以完整的创建流程是:

复制代码
// ① 对象管理器:分配 "壳"
ObCreateObject(Type, ..., sizeof(TYPE_BODY), ..., &Body);
// ② 调用方自己:初始化 "肉"
((PTYPE_BODY)Body)->Field1 = ...;
((PTYPE_BODY)Body)->Field2 = ...;
// ③ 对象管理器:插入命名空间和分配句柄
ObInsertObject(Body, ..., &Handle);

这 3 步的分工明确:① = 我管内存和公共字段;② = 你管类型特有字段;③ = 我管身份和钥匙

设计解读:为什么一次性分配 Header + Info 块 + Body?

另一个值得探讨的问题是:为什么所有 Info 块要和 Header、Body 挤在同一次池分配中?为什么不在需要 NameInfo 时再单独分配?

答案是避免碎片化与简化释放路径

  • 释放的原子性ObpDeallocateObjectoblife.c:37-143(file:///d:/reactos/ntoskrnl/ob/oblife.c#L37-L143))在执行时只需调用一次 ExFreePoolWithTag 就能释放整个块。如果 NameInfo 和 Header 分属两次分配,释放时要释放两次------这意味着释放路径上多一个"判断哪些 Info 块独立分配了"的逻辑分支。
  • 内存局部性 :Header 和 NameInfo 在同一个 ExAllocatePoolWithTag 调用中分配,它们在物理内存中相邻------访问 Header 时 NameInfo 很可能在同一个 Cache Line 上。这对 ObpLookupEntryDirectory 这种频繁访问 NameInfo 的路径来说是一个可观的性能收益。
  • 碎片控制:如果 N 次独立分配,一个对象占用 N 个池块;而一次分配后,整个对象(Header + Info + Body)是一个连续的块,释放后它是一个连续的"空洞",更容易被后续的大分配复用。

只有 HandleInfo 有一个例外------当对象第一次被多个进程同时打开时,SingleEntry 可能升级为 HandleCountDatabase(一个更复杂的数据结构)。这个升级是受控的、额外的池分配,且只在持有句柄表锁的情况下执行------是"小而受控"的例外。

  1. 后续:类型专属的 Open/Delete 回调

    完整的对象创建流程中,在 ObCreateObject 返回后,调用方还需要调用 ObInsertObject 将其插入命名空间和句柄表。此过程中对象管理器会调用类型的 OpenProcedure(记录句柄打开计数)。删除时调用 DeleteProcedure

关键代码片段:ObpAllocateObject 的分配逻辑

以下代码摘自 ntoskrnl/ob/oblife.c:715-835(file:///d:/reactos/ntoskrnl/ob/oblife.c#L715-L835),展示了如何一次性分配多块结构并用负偏移量反向定位:

c 复制代码
// 计算最终 Header 大小(Header 之前的 Info 块累积之和 + Header 自身到 Body 的偏移)
FinalSize = QuotaSize + HandleSize + NameSize + CreatorSize
          + FIELD_OFFSET(OBJECT_HEADER, Body);

// 从池中一次性分配(含对象体大小 ObjectSize)
Header = ExAllocatePoolWithTag(PoolType, FinalSize + ObjectSize, Tag);
if (!Header) return STATUS_INSUFFICIENT_RESOURCES;

// 依次设置各 Info 块指针并向前移动 Header
if (QuotaSize) {
    QuotaInfo = (POBJECT_HEADER_QUOTA_INFO)Header;
    QuotaInfo->PagedPoolCharge = ObjectCreateInfo->PagedPoolCharge;
    QuotaInfo->NonPagedPoolCharge = ObjectCreateInfo->NonPagedPoolCharge;
    QuotaInfo->SecurityDescriptorCharge = ObjectCreateInfo->SecurityDescriptorCharge;
    QuotaInfo->ExclusiveProcess = NULL;
    Header = (POBJECT_HEADER)(QuotaInfo + 1);   // 向前"跳过"QuotaInfo
}
if (HandleSize) {
    HandleInfo = (POBJECT_HEADER_HANDLE_INFO)Header;
    HandleInfo->SingleEntry.HandleCount = 0;
    Header = (POBJECT_HEADER)(HandleInfo + 1);  // 再向前"跳过"HandleInfo
}
if (NameSize) {
    NameInfo = (POBJECT_HEADER_NAME_INFO)Header;
    NameInfo->Name = *ObjectName;
    NameInfo->Directory = NULL;
    NameInfo->QueryReferences = 1;
    Header = (POBJECT_HEADER)(NameInfo + 1);    // 再向前"跳过"NameInfo
}
if (CreatorSize) {
    CreatorInfo = (POBJECT_HEADER_CREATOR_INFO)Header;
    CreatorInfo->CreatorBackTraceIndex = 0;
    CreatorInfo->CreatorUniqueProcess = PsGetCurrentProcessId();
    InitializeListHead(&CreatorInfo->TypeList);
    Header = (POBJECT_HEADER)(CreatorInfo + 1); // 再向前"跳过"CreatorInfo
}

// 设置偏移量(这些是最终 Header 相对各 Info 块的字节数)
if (QuotaSize)  Header->QuotaInfoOffset  = (UCHAR)(QuotaSize + HandleSize + NameSize + CreatorSize);
if (HandleSize) Header->HandleInfoOffset = (UCHAR)(HandleSize + NameSize + CreatorSize);
if (NameSize)   Header->NameInfoOffset   = (UCHAR)(NameSize + CreatorSize);

// 初始化 Header 公共字段
Header->Flags = OB_FLAG_CREATE_INFO;
if (CreatorSize) Header->Flags |= OB_FLAG_CREATOR_INFO;
if (HandleSize)  Header->Flags |= OB_FLAG_SINGLE_PROCESS;
Header->PointerCount = 1;
Header->HandleCount  = 0;
Header->Type = ObjectType;
Header->ObjectCreateInfo = ObjectCreateInfo;
Header->SecurityDescriptor = NULL;

注意这段代码的顺序至关重要:

  • 分配顺序(低地址→高地址)QuotaInfo → HandleInfo → NameInfo → CreatorInfo → OBJECT_HEADER → Body
  • 但偏移量计算是反向的:最终 Header 位置向前"跳过 N 字节"到达 Info 块。

4.1.4 引用计数与生命周期

双计数器设计

每个 OBJECT_HEADER 维护两个独立的计数器:

计数器 类型 含义 递增时机 递减时机
PointerCount LONG_PTR 内核中持有对象体指针的引用数。此计数 ≥ HandleCount。 调用 ObReferenceObject(ByPointer/ByHandle),以及在句柄表中建立新句柄时各 +1 ObDereferenceObject、关闭句柄(对每句柄 -1)
HandleCount LONG_PTR(与 NextToFree 构成 union) 当前进程表中指向此对象的句柄总数。仅在类型声明了 MaintainHandleCount 时维护。 新建句柄(ObpIncrementHandleCount 关闭句柄(ObpDecrementHandleCount

为什么需要两个计数器?

  • HandleCount 让内核能够回答"有多少个用户态句柄指向该对象"------用于调试、诊断,以及判断是否需要从命名空间中移除对象(当 HandleCount 归零时,ObpDeleteNameCheck 会尝试移除名称)。
  • PointerCount 是真正控制释放的计数器。当 PointerCount 从 1 → 0 时,对象被 ObpDeleteObject 释放。即使没有句柄打开,内核代码持有指针引用期间对象仍然有效。

ObReferenceObjectByPointer

ntoskrnl/ob/obref.c:379-405(file:///d:/reactos/ntoskrnl/ob/obref.c#L379-L405):

c 复制代码
NTSTATUS
NTAPI
ObReferenceObjectByPointer(
    IN PVOID Object,
    IN ACCESS_MASK DesiredAccess,
    IN POBJECT_TYPE ObjectType,
    IN KPROCESSOR_MODE AccessMode
)
{
    POBJECT_HEADER Header;
    Header = OBJECT_TO_OBJECT_HEADER(Object);

    // 验证类型(对用户态调用强制校验,内核态可传 NULL 跳过)
    if ((Header->Type != ObjectType) &&
        ((AccessMode != KernelMode) || (ObjectType == ObpSymbolicLinkObjectType)))
    {
        return STATUS_OBJECT_TYPE_MISMATCH;
    }

    // 原子递增 PointerCount
    InterlockedIncrementSizeT(&Header->PointerCount);
    return STATUS_SUCCESS;
}

实现非常简洁:一次类型验证 + 一次原子递增。InterlockedIncrementSizeT 是跨平台的 InterlockedIncrement 封装,保证 SMP 安全。

ObDereferenceObject

ntoskrnl/ob/obref.c:316-353(file:///d:/reactos/ntoskrnl/ob/obref.c#L316-L353)(作为 ObfDereferenceObject 实现,然后 ObDereferenceObject 是其包装):

c 复制代码
LONG_PTR
FASTCALL
ObfDereferenceObject(IN PVOID Object)
{
    POBJECT_HEADER Header;
    LONG_PTR NewCount;

    Header = OBJECT_TO_OBJECT_HEADER(Object);

    // 健壮性检查:PointerCount 绝不应小于 HandleCount
    if (Header->PointerCount < Header->HandleCount) {
        DPRINT1("Misbehaving object: %wZ\n", &Header->Type->Name);
        return Header->PointerCount;
    }

    // 原子递减
    NewCount = InterlockedDecrementSizeT(&Header->PointerCount);
    if (!NewCount) {
        // 断言:HandleCount 也必须为 0(否则泄漏了句柄引用)
        ASSERT(Header->HandleCount == 0);

        // APC 禁用时不立即释放,以免在调度级别释放需要被动级的结构
        if (!KeAreAllApcsDisabled()) {
            ObpDeleteObject(Object, FALSE);
        } else {
            ObpDeferObjectDeletion(Header);  // 挂入 Reaper 链表
        }
    }
    return NewCount;
}

对象生命周期状态图

复制代码
  NtCreateEvent
      │
      ▼
  ┌──────────┐   ObReferenceObjectByPointer          ┌──────────┐
  │ Created  │ ─────────────────────────────────────▶│Referenced│
  │Pointer=1 │ ◀─────────────────────────────────────│Pointer>1 │
  │Handle=0  │   ObDereferenceObject(Pointer→1)      │Handle≥0  │
  └──────────┘                                       └──────────┘
       │                                                   ▲
       │ ObInsertObject                                    │ NtDuplicateObject
       ▼                                                   │ NtOpenFile
  ┌──────────┐                                   ┌──────────┐
  │  Named   │                                   │  Opened  │
  │InDirectory│──────── ObInsertObject (handle)─▶│(Handled) │
  │Directory  │                                   │Handle>0  │
  └──────────┘                                   └──────────┘
       │                                                 │
       │ ObpDeleteNameCheck (HandleCount→0 & !Permanent) │ NtClose
       │        从目录中移除名称                           ▼
       ▼                                          ┌──────────┐
  ┌──────────┐                                   │  Closed  │
  │Unnamed   │◀──────────────────────────────────│ Handle=0  │
  │ (floating)│                                   │  Pointer↓│
  └──────────┘                                   └──────────┘
       │                                                  │
       │ ObDereferenceObject                              │
       ▼                                                  │
  ┌──────────┐                                   ┌──────────┐
  │ Deleted  │◀── PointerCount → 0 ─────────────│Deref'ed  │
  │ (freed)  │    ObpDeleteObject + ExFreePool  │ Pointer=0 │
  └──────────┘                                   └──────────┘

  注:带有 OB_FLAG_PERMANENT 的对象会卡在 "Named" 状态,即使 PointerCount=0
      直到 NtMakeTemporaryObject 清除该标志。

4.1.5 对象目录(OBJECT_DIRECTORY)与命名空间

什么是对象目录

对象目录是对象管理器实现的"内核文件系统"。用户态看到的名称路径(如 \BaseNamedObjects\MyEvent\??\C:\Windows\Device\HarddiskVolume1)都在对象目录中被解析。

从概念上讲,对象目录与文件系统目录非常相似:

  • 每个目录是一个对象(类型为 Directory);
  • 目录条目将名称(Unicode 字符串)映射到另一个对象(可以是目录、文件对象、设备对象、符号链接等);
  • 名称查找按 \ 分隔符逐段解析;
  • 支持符号链接(reparse point)。

OBJECT_DIRECTORY 结构

定义于 sdk/include/ndk/obtypes.h:407-425(file:///d:/reactos/sdk/include/ndk/obtypes.h#L407-L425):

c 复制代码
typedef struct _OBJECT_DIRECTORY
{
    // 37 个 Hash Bucket 指针数组,每一项指向单链表(OBJECT_DIRECTORY_ENTRY*)
    struct _OBJECT_DIRECTORY_ENTRY *HashBuckets[NUMBER_HASH_BUCKETS];

    // 目录锁:用 push lock 实现共享/独占访问
    EX_PUSH_LOCK Lock;

    // 设备映射(仅 \??\ 类型目录使用):指向 DOS 设备映射
    struct _DEVICE_MAP *DeviceMap;

    // 会话 ID(用于隔离 Session 0 / Session 1 的 \??\ 目录)
    ULONG SessionId;
} OBJECT_DIRECTORY, *POBJECT_DIRECTORY;

Hash Bucket 的数量 由常量 NUMBER_HASH_BUCKETS = 37 定义(见 sdk/include/ndk/obtypes.h:158(file:///d:/reactos/sdk/include/ndk/obtypes.h#L158-L158)):

c 复制代码
#define NUMBER_HASH_BUCKETS  37

37 是一个质数,有助于减少 Hash 冲突。Windows 的早期版本使用相同的值(后续 Windows 版本动态调整)。

目录条目结构sdk/include/ndk/obtypes.h:398-405(file:///d:/reactos/sdk/include/ndk/obtypes.h#L398-L405)):

c 复制代码
typedef struct _OBJECT_DIRECTORY_ENTRY
{
    struct _OBJECT_DIRECTORY_ENTRY *ChainLink;  // 冲突链表 next 指针
    PVOID Object;                               // 指向对象体(不是 Header)
    ULONG HashValue;                            // 记录 hash 值,加速比较
} OBJECT_DIRECTORY_ENTRY, *POBJECT_DIRECTORY_ENTRY;

Hash 查找算法

Hash 算法位于 ObpLookupEntryDirectory(见 ntoskrnl/ob/obdir.c:156-327(file:///d:/reactos/ntoskrnl/ob/obdir.c#L156-L327))。核心思想:

c 复制代码
// 对名称中每个 WCHAR 做 hash
for (HashValue = 0; TotalChars; TotalChars--) {
    WCHAR CurrentChar = *Buffer++;
    HashValue += (HashValue << 1) + (HashValue >> 1);   // * 3,扩散位
    if (CurrentChar < 'a') {
        HashValue += CurrentChar;
    } else if (CurrentChar > 'z') {
        HashValue += RtlUpcaseUnicodeChar(CurrentChar);
    } else {
        HashValue += (CurrentChar - ('a' - 'A'));   // 小写转大写后相加
    }
}
// 取模得到 bucket 索引
HashIndex = HashValue % NUMBER_HASH_BUCKETS;  // % 37

注意:这是一个大小写不敏感 的 hash。对 ASCII 小写字母先转换为大写再累加;对非 ASCII 字符调用 RtlUpcaseUnicodeChar 做上变换。

随后执行链表线性查找(见 ntoskrnl/ob/obdir.c:231-253(file:///d:/reactos/ntoskrnl/ob/obdir.c#L231-L253)):

c 复制代码
while ((CurrentEntry = *AllocatedEntry)) {
    if (CurrentEntry->HashValue == HashValue) {       // 先比较 hash,快失败
        ObjectHeader = OBJECT_TO_OBJECT_HEADER(CurrentEntry->Object);
        HeaderNameInfo = OBJECT_HEADER_TO_NAME_INFO(ObjectHeader);
        // 再比较实际字符串(长度 + 内容)
        if ((Name->Length == HeaderNameInfo->Name.Length) &&
            RtlEqualUnicodeString(Name, &HeaderNameInfo->Name, CaseInsensitive)) {
            break;
        }
    }
    AllocatedEntry = &CurrentEntry->ChainLink;
}

找到后还有一个优化:把命中的条目移动到所在 bucket 的链表头部(见 ntoskrnl/ob/obdir.c:256-273(file:///d:/reactos/ntoskrnl/ob/obdir.c#L256-L273)),加速重复访问(基于局部性原理)。

Windows 标准命名空间路径一览

对象管理器初始化阶段(ObInitSystem,见 ntoskrnl/ob/obinit.c:203-435(file:///d:/reactos/ntoskrnl/ob/obinit.c#L203-L435))会创建以下关键目录:

路径 用途 创建位置
\ 根目录对象(Root Directory) obinit.c:312-332(file:///d:/reactos/ntoskrnl/ob/obinit.c#L312-L332)
\ObjectTypes 所有对象类型的目录(Type 对象注册于此) obinit.c:362-387(file:///d:/reactos/ntoskrnl/ob/obinit.c#L362-L387)
\KernelObjects 内核对象(跨会话可见,受限访问) obinit.c:343-360(file:///d:/reactos/ntoskrnl/ob/obinit.c#L343-L360)
\?? DOS 设备名目录(全局 \??\),每个会话还有一份私有副本 obname.c:174-280(file:///d:/reactos/ntoskrnl/ob/obname.c#L174-L280)
\DosDevices \?? 的符号链接(向后兼容) obname.c:258-273(file:///d:/reactos/ntoskrnl/ob/obname.c#L258-L273)
\Device 设备对象目录(由驱动程序 IoCreateDevice 注册) I/O 管理器
\Driver 驱动对象目录 I/O 管理器
\BaseNamedObjects 会话级命名事件/互斥量/信号量(由 BaseNamedObjects 提供) Win32k 子系统
\Windows Windows 子系统对象 Win32k
\Callback 回调对象目录 配置管理器
\FileSystem 文件系统驱动和过滤器 I/O 管理器
\Security 安全子系统对象(如 LSA 认证包) LSASS
\Registry 注册表根键对象(符号链接到 \Registry\Machine\Registry\User 配置管理器
命名空间的层次与交互关系

从上表可以看出,对象命名空间是一个以 \ 为根的树状结构,不同的子系统负责管理自己的分支:

  1. \ 根目录 --- 由对象管理器直接管理(ObInitSystem),是所有路径解析的起点。根目录下只有少数几个子目录(\ObjectTypes\??\KernelObjects 等),大多数系统目录都作为 \ 的直接子项创建。

  2. \?? 与 DOS 设备映射 --- 这是整个命名空间中最特殊的部分。当用户态程序调用 CreateFile(L"C:\\xxx") 时,I/O 管理器会在内部将 C: 展开为 \??\C:,然后经过 ObpLookupObjectName\??\ 前缀识别,跳转到当前会话的会话私有 \??\ 目录,在其中查找 C: 符号链接,最终透明重解析到 \Device\HarddiskVolume1。这种"间接"是内核设计中的关键抽象------下层的存储设备可以是 RAID、网络共享或 RAM Disk,但上层的 C: 盘符始终不变。

  3. \BaseNamedObjects 与会话隔离 --- 所有命名的事件、互斥体、信号量、计时器、作业对象都注册在 \BaseNamedObjects 下。它通常是"每个会话一个"------Session 0 的 \BaseNamedObjects 与 Session 1 的 \BaseNamedObjects 是不同目录,因此一个服务创建了一个名为"GlobalEvent"的命名事件后,交互式用户在同一名称下创建的事件是另一个独立的事件。这同样是隔离设计。

  4. \Device 与 I/O 管理器 --- 这个目录下的条目由设备驱动程序通过 IoCreateDevice 注册。每种类型的设备(磁盘、键盘、鼠标、显示)都有自己的名称(如 \Device\KeyboardClass0\Device\HarddiskVolume1)。\??\ 中的符号链接本质上是指向 \Device\ 下某个设备对象的别名。

  5. \ObjectTypes 与类型目录 --- 这是一个特殊的目录:它的条目不是普通对象,而是 OBJECT_TYPE 结构本身。也就是说,\ObjectTypes\Process 这个名称解析后的对象体是一个 OBJECT_TYPE 指针。这为调试工具(如 WinDbg 的 !object \ObjectTypes)提供了统一的类型信息查询入口。"类型对象本身也是对象"这一设计在本节 4.2 中会深入讨论。

  6. 跨命名空间的交互 --- 不同命名空间并非孤立存在。以 \Registry 为例:它本身是对象命名空间中的一个符号链接,但解析后会进入配置管理器(Configuration Manager) 的命名空间(注册表路径 \Registry\Machine\SYSTEM\CurrentControlSet\...)。同样地,\FileSystem 目录中的条目最终会走到文件系统驱动的 ParseProcedure。对象命名空间是整个内核命名空间的"统一入口",往下可以穿透到其他子系统的私有命名空间。

这种命名空间设计的一个突出优点是:用户态的路径操作无论是打开文件、创建事件还是访问注册表,形式上是统一的(CreateFile vs CreateEvent 都接收一个字符串路径),语义上也是统一的(都会经过 ObpLookupObjectName 的逐段解析),但底层行为由目标对象的类型回调(ParseProcedure、SecurityProcedure 等)决定。 这是"统一入口 + 多态行为"的典型实现。

根目录对象:ObpRootDirectoryObject

ObInitSystem 的 Phase 1 阶段(ntoskrnl/ob/obinit.c:306-337(file:///d:/reactos/ntoskrnl/ob/obinit.c#L306-L337)),系统创建 \ 根目录并保存在全局变量 ObpRootDirectoryObject 中:

c 复制代码
RtlInitUnicodeString(&Name, L"\\");
InitializeObjectAttributes(&ObjectAttributes, &Name,
                           OBJ_CASE_INSENSITIVE | OBJ_PERMANENT,
                           NULL, SePublicDefaultUnrestrictedSd);
NtCreateDirectoryObject(&Handle, DIRECTORY_ALL_ACCESS, &ObjectAttributes);
ObReferenceObjectByHandle(Handle, 0, ObpDirectoryObjectType,
                          KernelMode, (PVOID*)&ObpRootDirectoryObject, NULL);

注意根目录被标记为 OBJ_PERMANENT------它永不被自动释放,贯穿系统整个生命周期。


4.1.6 名称查找:ObpLookupObjectName

函数签名

ObpLookupObjectName 是名称解析的核心函数,见于 ntoskrnl/ob/obname.c:444-1201(file:///d:/reactos/ntoskrnl/ob/obname.c#L444-L1201)。这是对象管理器中最复杂的函数之一(约 700 行),处理:

  • 路径分段解析;
  • \??\ 前缀的 DOS 设备名识别;
  • 符号链接(reparse)的透明解析(最多 30 层,避免循环引用);
  • 对象创建时的"查找+插入"语义(当 InsertObject != NULL 时);
  • 访问检查与审计。

路径解析算法

给定一条路径,例如 \??\C:\Windows\System32\notepad.exe,解析过程如下:

复制代码
  路径字符串:\??\C:\Windows\System32\notepad.exe
                │
                ▼  前缀识别 \??\  →  切换到 DosDevicesDirectory
                DosDevicesDirectory(会话私有)
                │
                ▼  解析下一段 "C:"  →  查找符号链接
                SymbolicLink -> \Device\HarddiskVolume1
                │
                ▼  reparse(重新解析目标路径 + 剩余路径)
                \Device\HarddiskVolume1
                │
                ▼  继续解析 "\Windows\System32\notepad.exe"
                ParseProcedure(FileObject ParseRoutine)
                │
                ▼  返回 FILE_OBJECT*

在代码中,这由一个双层循环实现(见 ntoskrnl/ob/obname.c:755-1157(file:///d:/reactos/ntoskrnl/ob/obname.c#L755-L1157)):

c 复制代码
// 外层:重解析循环(最多 MaxReparse = 30 次)
while (Reparse && MaxReparse) {
    RemainingName = LocalName;   // 剩余未解析路径
    Reparse = FALSE;

    // 内层:逐段解析循环
    while (TRUE) {
        // 跳过开头的 '\'
        if ((RemainingName.Length) && (RemainingName.Buffer[0] == OBJ_NAME_PATH_SEPARATOR)) {
            RemainingName.Buffer++;
            RemainingName.Length -= sizeof(OBJ_NAME_PATH_SEPARATOR);
        }

        // 切分出下一段名称(下一个 '\' 之前的部分)
        ComponentName = RemainingName;
        while (RemainingName.Length && RemainingName.Buffer[0] != OBJ_NAME_PATH_SEPARATOR) {
            RemainingName.Buffer++;
            RemainingName.Length -= sizeof(OBJ_NAME_PATH_SEPARATOR);
        }
        ComponentName.Length -= RemainingName.Length;

        if (!ComponentName.Length) { Status = STATUS_OBJECT_NAME_INVALID; break; }

        // 在当前 Directory 中查找 ComponentName
        Object = ObpLookupEntryDirectory(Directory, &ComponentName,
                                         Attributes, InsertObject ? FALSE : TRUE,
                                         LookupContext);
        if (!Object) {
            // 若有剩余路径 → 路径不存在;否则 → 对象名不存在
            if (RemainingName.Length) Status = STATUS_OBJECT_PATH_NOT_FOUND;
            else if (!InsertObject)    Status = STATUS_OBJECT_NAME_NOT_FOUND;
            // 若 InsertObject 非空,尝试创建并插入新条目
            ...
            break;
        }

        // 找到对象后,检查其类型是否有 ParseProcedure
        // 如果是目录(Directory 类型没有 ParseProcedure),继续下层解析
        // 如果是符号链接 → ParseProcedure 会返回 STATUS_REPARSE 以驱动外层循环
        // 如果是文件对象等 → ParseProcedure 完成最终解析,返回成功
        ObjectHeader = OBJECT_TO_OBJECT_HEADER(Object);
        ParseRoutine = ObjectHeader->Type->TypeInfo.ParseProcedure;
        if (ParseRoutine) {
            // 调用 ParseProcedure
            Status = ParseRoutine(Object, ObjectType, AccessState,
                                  AccessCheckMode, Attributes,
                                  ObjectName, &RemainingName,
                                  ParseContext, SecurityQos,
                                  &Object);
            if (Status == STATUS_REPARSE || Status == STATUS_REPARSE_OBJECT) {
                Reparse = TRUE;   // 触发外层 reparse 循环
                --MaxReparse;
                ...
                break;
            }
            ...
            break;
        } else if (!RemainingName.Length) {
            // 没有剩余路径且无需 parse → 成功命中目标对象
            Status = ObReferenceObjectByPointer(Object, 0, ObjectType, AccessMode);
            break;
        } else {
            // 非目录对象却还有剩余路径 → 类型不匹配
            Status = STATUS_OBJECT_TYPE_MISMATCH;
            Object = NULL;
            break;
        }
    }
}

OBP_LOOKUP_CONTEXT 的作用

定义于 sdk/include/ndk/obtypes.h:510-518(file:///d:/reactos/sdk/include/ndk/obtypes.h#L510-L518):

c 复制代码
typedef struct _OBP_LOOKUP_CONTEXT
{
    POBJECT_DIRECTORY Directory;  // 当前查找所在的目录
    PVOID Object;                 // 找到的对象(或插入的对象)
    ULONG HashValue;              // 本次查找计算的 hash 值
    USHORT HashIndex;             // 所在 bucket 索引
    BOOLEAN DirectoryLocked;      // 是否已持有目录锁(共享或独占)
    ULONG LockStateSignature;     // 锁状态签名,用于调试检测
} OBP_LOOKUP_CONTEXT, *POBP_LOOKUP_CONTEXT;

其设计目的:

  1. 缓存 hash 值:避免在同一次查找中反复计算名称 hash。
  2. 保持目录锁:查找→插入是一个原子操作。通过 context 持有锁,可以避免"查找后到插入前有其他线程插入同名条目"的 TOCTOU 竞态。
  3. 简化 reparse:每次 reparse 重置 context。

典型调用模式:

c 复制代码
OBP_LOOKUP_CONTEXT Context;
ObpInitializeLookupContext(&Context);
ObpAcquireLookupContextLock(&Context, ParentDirectory);   // 共享锁
Object = ObpLookupEntryDirectory(ParentDirectory, &Name, ..., &Context);
if (!Object) {
    // 需将锁升级为独占以插入
    ObpInsertEntryDirectory(ParentDirectory, &Context, NewHeader);
}
ObpReleaseLookupContext(&Context);

查找失败的两种处理

调用场景 InsertObject 失败后的动作 返回状态
打开已有对象 (ObOpenObjectByName) NULL 简单返回 STATUS_OBJECT_NAME_NOT_FOUND / STATUS_OBJECT_PATH_NOT_FOUND
创建新对象 (ObInsertObject) 非 NULL 在路径最后一段所在目录中创建新条目,将 InsertObject 挂上 成功:STATUS_SUCCESS;路径中段不存在:STATUS_OBJECT_PATH_NOT_FOUND

4.1.7 符号链接与 ??\ 目录

对象符号链接(Symbolic Link)

对象命名空间中的符号链接与文件系统符号链接在概念上非常相似:它是一个指向其他名称的对象。当 ObpLookupObjectName 遍历到符号链接时,它会透明地"跳转到"目标名称继续解析。

符号链接对象结构(sdk/include/ndk/obtypes.h:535-542(file:///d:/reactos/sdk/include/ndk/obtypes.h#L535-L542)):

c 复制代码
typedef struct _OBJECT_SYMBOLIC_LINK
{
    LARGE_INTEGER CreationTime;    // 创建时间(调试用)
    UNICODE_STRING LinkTarget;     // 目标路径
    UNICODE_STRING LinkTargetRemaining;  // 被附加到目标之后的剩余路径
    PVOID LinkTargetObject;        // 可选:缓存目标对象指针
    ULONG DosDeviceDriveIndex;     // 仅 DOS 设备符号链接使用(盘符索引)
} OBJECT_SYMBOLIC_LINK, *POBJECT_SYMBOLIC_LINK;

解析函数 ObpParseSymbolicLink(未在本节显示的源码位置)会:

  1. LinkTarget 替换当前路径;
  2. 附加原路径未解析部分(LinkTargetRemaining);
  3. 返回 STATUS_REPARSE,驱动外层循环重新解析。

??\ 目录的特殊角色

\??\(也称为"DosDevices 目录")是 Windows 中非常特殊的一层命名空间,它承担三项职责:

  1. DOS 盘符映射C:D:E: 等是 \??\ 下的符号链接,分别指向 \Device\HarddiskVolume1\Device\HarddiskVolume2、...。

  2. COM/LPT 端口映射COM1\Device\Serial0LPT1\Device\Parallel0

  3. 会话隔离 :每个 Terminal Services 会话都有自己的 \??\ 目录。这样 Session 1 的用户不能看到或干扰 Session 0 的 DOS 设备(但可通过 \GLOBAL??\ 显式访问全局目录)。

设备名映射机制(ObpCreateDosDevicesDirectory)

ntoskrnl/ob/obname.c:174-280(file:///d:/reactos/ntoskrnl/ob/obname.c#L174-L280)。系统启动时:

  1. 创建全局 \GLOBAL?? 目录(带保护 SD);
  2. 在其中创建 GLOBALROOT\ 的符号链接(用于显式绕过 \??\ 直达根);
  3. 在其中创建 Global\GLOBAL?? 的符号链接(允许 \??\Global\xxx 访问全局);
  4. 创建 \DosDevices\?? 的符号链接(NT 4 兼容);
  5. 将系统 device map 设置为此目录。

随后,每当一个新会话被创建时,Win32 子系统会调用 ObSetDeviceMap 为该会话建立一个私有 \??\ 目录,然后在其中为该会话创建的任何 DOS 设备(例如 subst Z: D:\MyFolder)挂载符号链接。

用户态路径到内核路径的展开示例

复制代码
  用户态调用:CreateFileW(L"C:\\Windows\\notepad.exe", ...)
       │
       ▼  NtCreateFile 接收 \??\C:\Windows\notepad.exe
       │
       ▼  ObpLookupObjectName:
           1) 前缀匹配 \??\ → 切换到 DosDevicesDirectory
           2) 解析 "C:" → 发现 SymbolicLink → target = \Device\HarddiskVolume1
           3) reparse → 路径变为 \Device\HarddiskVolume1\Windows\notepad.exe
           4) 解析 \Device → 目录对象
           5) 解析 HarddiskVolume1 → IoFileObjectType(设备对象,有 ParseProcedure)
           6) 调用 IopParseDevice(I/O 管理器的 ParseProcedure)→ 完成文件对象创建
       │
       ▼  返回 FILE_OBJECT 句柄

ObpLookupObjectName 对符号链接的处理:透明解析

在用户态看来,这整个过程是透明 的------调用者不会知道自己穿过了多少层符号链接。只有通过 NtQueryObject 查询对象类型、或使用特殊标志 OBJ_OPENLINK 打开符号链接本身时,才能感知到符号链接的存在。


4.1.8 概念解释

下表汇总本节核心术语:

术语 含义 数据结构 / 函数
对象头(Object Header) 所有内核对象共享的前缀结构,记录引用计数、类型、安全、Info 块偏移等公共字段 OBJECT_HEADER
对象体(Object Body) 对象头之后的类型专属数据。对外 API 返回的指针通常指向 Body,如 PFILE_OBJECTPEPROCESS EPROCESS, FILE_OBJECT, KEVENT, ...
Info 块(Name/Handle/Quota/Creator Info) 位于对象头之前(低地址方向)的可选扩展结构,按需分配,通过负偏移量定位 OBJECT_HEADER_NAME_INFO, OBJECT_HEADER_HANDLE_INFO, OBJECT_HEADER_QUOTA_INFO, OBJECT_HEADER_CREATOR_INFO
对象目录(Object Directory) 对象管理器的"内核文件系统"目录,将名称映射到对象 OBJECT_DIRECTORY, NtCreateDirectoryObject
Hash Bucket 对象目录内部的 hash 表槽位(共 37 个)。名称先 hash 再取模得到索引,冲突条目以链表串起来 OBJECT_DIRECTORY.HashBuckets[NUMBER_HASH_BUCKETS]
命名空间(Namespace) \ 根目录、各级子目录、符号链接构成的统一名称空间。典型路径如 \??\C:\Windows\BaseNamedObjects\MyEvent ObpRootDirectoryObject, ObpLookupObjectName
符号链接(Symbolic Link) 在对象命名空间中实现"别名→真实路径"的映射。驱动 \??\C:\Device\HarddiskVolume1 OBJECT_SYMBOLIC_LINK, ObpParseSymbolicLink
引用计数(Reference Count) 控制对象生命周期的原子计数器。PointerCount 归零时对象被释放 OBJECT_HEADER.PointerCount, ObReferenceObject, ObDereferenceObject
PointerCount vs HandleCount PointerCount 统计内核中所有持有对象指针的引用;HandleCount 统计用户态句柄数量。PointerCount ≥ HandleCount OBJECT_HEADER.PointerCount, OBJECT_HEADER.HandleCount
永久对象(Permanent Object) OB_FLAG_PERMANENT 标志。即使引用计数归零也不释放,直到 NtMakeTemporaryObject 解除。典型用例:根目录、\ObjectTypes OB_FLAG_PERMANENT, ObpSetPermanentObject

4.1.9 为什么要这样设计

Q1. 为什么用「对象头 + 对象体」而不是直接在对象体里存公共字段?

核心权衡:通用性 vs 类型灵活性

  • 通用性:所有对象都需要引用计数、类型指针、安全描述符。若让每个类型自行包含这些字段,既重复代码又难演进------未来修改公共字段需要改 N 个类型定义。
  • 类型灵活性 :通过 OBJECT_TO_OBJECT_HEADER(body) 宏,对象体对 Header 完全无知------FILE_OBJECT 的开发者不需要关心 Header 字段如何布局,只需调用 ObCreateObject(IoFileObjectType, ..., sizeof(FILE_OBJECT), ...)
  • Info 块的按需分配 :把可变扩展放在 Header 之前 (负偏移方向)而不是之后(正偏移方向),避免 Header 大小随对象类型变化------这样 CONTAINING_RECORD((body), OBJECT_HEADER, Body) 的偏移是编译期常量,所有类型共享一个实现。

反例 :如果把所有公共字段放在对象体开头(即 EPROCESS 第一个字段是 LONG_PTR RefCount),那每个类型都要手写 ObDereferenceObject(&eprocess->RefCount)------无法统一,也无法向现有类型追加新公共字段。

Q2. 为什么 Info 块要用「负偏移量」而不是用结构体指针数组?

核心权衡:内存占用 vs 查找速度 vs 一次性分配

方案对比:

方案 说明 优点 缺点
A. 负偏移量(当前设计) UCHAR Offset 字段记录 Info 块到 Header 的字节数。0 = 不存在 1) Header 固定小(3 个 UCHAR + 1 个 Flags UCHAR);2) 单次池分配 完成 Header+Info+Body,避免碎片;3) O(1) 定位 单个 Info 块大小之和必须 < 256 字节
B. 指针数组 PVOID InfoPointers[4]struct { PNAME_INFO Name; PHANDLE_INFO Handle; ... } 灵活,可容纳任意大小的 Info 块 1) 需要5 次独立池分配(Header + 4 个 Info 块)→ 碎片化、慢;2) 每个指针 8/4 字节,32 字节开销 vs 3 字节
C. 固定长度数组 所有对象都预留 4 个 Info 块空间 实现最简单 大部分对象不需要全部 Info 块,内存浪费巨大

ReactOS 选择方案 A。UCHAR 偏移量限制为 255 字节------但 4 个 Info 块的实际大小都很小(~40 字节/块),总和 ~160 字节 < 255,绰绰有余。

Q3. 为什么对象目录要用 37 个 Hash Bucket 而不是更大的表或 B 树?

核心权衡:查找速度 vs 内存开销 vs 实现复杂度

  • 37 是质数 :使 hash % 37 的分布更均匀(若使用合数,与 hash 算法叠加可能导致某些 bucket 永远空)。
  • 小表 + 链表冲突 :对象目录的典型条目数在几十到几百级别(一个 Session 的 \??\ 目录通常 < 100 条目),O(N) 链表扫描足够快。
  • 缓存到链表头部:命中条目被提升到 bucket 头部后,热点访问接近 O(1)。
  • 对比 B 树 :B 树适合百万级条目(如文件系统索引),但在内核路径名查找场景下实现代价高、锁粒度复杂 。Windows 后续版本的确引入了更大的动态表(ObpDirectoryEntry 结构变化),但 ReactOS 在兼容 NT 5.x 行为时选择了更简单的固定 37-bucket 设计。

Q4. 为什么要有两套引用计数(PointerCount, HandleCount)而不是一个计数器?

核心权衡:释放判断 vs 命名空间管理 vs 诊断

  • PointerCount 控制释放 :当且仅当内核中最后一个指针引用解除时(PointerCount: 1 → 0),对象才被释放。这是必须的------即使没有句柄打开,内核驱动代码持有对象体指针期间对象必须存活。
  • HandleCount 控制命名 :当 HandleCount → 0(即所有用户态句柄都关闭),对象才会从命名空间被移除(ObpDeleteNameCheck)。这样保证"关闭句柄后再次打开同名对象不会失败"。
  • 若只用一个计数器(比如每个句柄 +1,每个内核指针引用也 +1):无法区分"是最后一个句柄关闭但仍有内核引用" vs "所有引用都结束"。前者需要保留名称在目录中,后者则不------但仅看一个计数器无法做这个区分。

4.1.10 增强子节:Info 块的动态布局与对齐

4.1.10.1 设计意图

每个对象是否需要 Name/Handle/Quota/Creator Info 取决于该对象的使用方式,而非其类型------即同一类型的对象在不同场景下可能有不同的 Info 块组合。例如:

  • 匿名事件CreateEvent(NULL, ..., NULL)):没有名称 → 不需要 NameInfo;
  • 命名事件CreateEvent(NULL, ..., L"MyEvent")):有名称 → 需要 NameInfo;
  • 内核创建的文件对象:只在内核中使用,不计入用户配额 → 可能不需要 QuotaInfo;
  • 用户态打开的文件对象:需要对进程配额计费 → 需要 QuotaInfo;
  • 当 NtGlobalFlag 启用了 FLG_MAINTAIN_OBJECT_TYPELIST 时 :所有对象都需要 CreatorInfo,以便在 !drvobj/!object 调试命令中可被遍历。

设计意图是:在创建时静态决定布局,创建后不变,以最小的开销为每个对象选择最精简的头结构。

4.1.10.2 概念解释

Info 块的分配策略(ObpAllocateObject 中的判定逻辑)

根据 ntoskrnl/ob/oblife.c:646-713(file:///d:/reactos/ntoskrnl/ob/oblife.c#L646-L713):

Info 块 何时分配 代码条件
NameInfo 调用者提供了非空 ObjectName(即 ObjectName->Buffer != NULL if (ObjectName->Buffer) { NameSize = sizeof(OBJECT_HEADER_NAME_INFO); ObpObjectsWithName++; }
HandleInfo 对象类型声明了 MaintainHandleCount(如进程/线程/文件,而 Directory 类型通常不需要) if (ObjectType->TypeInfo.MaintainHandleCount) { HandleSize = sizeof(OBJECT_HEADER_HANDLE_INFO); ObpObjectsWithHandleDB++; }
QuotaInfo 以下任一成立: ① PagedPoolCharge/NonPagedPoolCharge/SdCharge 与类型默认值不同; ② 调用方是用户态进程(非 System 进程); ③ ObjectAttributes 带有 OBJ_EXCLUSIVE oblife.c:656-667(file:///d:/reactos/ntoskrnl/ob/oblife.c#L656-L667)
CreatorInfo 对象类型声明了 MaintainTypeList,或系统启用全局类型列表维护 if (ObjectType->TypeInfo.MaintainTypeList) { CreatorSize = sizeof(OBJECT_HEADER_CREATOR_INFO); ObpObjectsWithCreatorInfo++; }
对齐要求
  • OBJECT_HEADER 的对齐 :Header 内的 Body 字段类型为 QUAD(即 LONGLONG),编译器按 8 字节对齐该字段 → Header 起始地址天然 8 字节对齐。
  • Info 块的对齐 :由于 Info 块顺序紧接 Header 之前 分配(在同一次 ExAllocatePoolWithTag 中),只要 Header 对齐 8 字节且每个 Info 块大小是 8 的倍数(OBJECT_HEADER_NAME_INFO = 32/24 字节,OBJECT_HEADER_HANDLE_INFO = 16 字节,OBJECT_HEADER_QUOTA_INFO = 32/16 字节,OBJECT_HEADER_CREATOR_INFO = 16 字节),则所有 Info 块也自然 8 字节对齐。
  • 对象体的对齐BodyQUAD 类型起始 → 对象体从 8 字节对齐地址开始,符合 EPROCESSFILE_OBJECT 等结构的对齐要求(这些结构通常以 LONGLONG/KEVENT 开头,需要 8 字节对齐)。
动态分配时各 Info 块的排列顺序(从低地址到高地址)

ntoskrnl/ob/oblife.c:727-778(file:///d:/reactos/ntoskrnl/ob/oblife.c#L727-L778) 的代码顺序决定:

复制代码
  低地址 (Pool 起始)                                    高地址
  ┌────────────┬────────────┬──────────┬───────────┬────────┬──────────┐
  │ QuotaInfo  │ HandleInfo │ NameInfo │CreatorInfo│ Header │   Body   │
  │ (若有)     │  (若有)    │  (若有)  │  (若有)   │  固定  │  对象体  │
  └────────────┴────────────┴──────────┴───────────┴────────┴──────────┘
       ▲              ▲           ▲           ▲           ▲
       │              │           │           │           │
     QuotaSize    HandleSize   NameSize   CreatorSize   FIELD_OFFSET(, Body)
       │              │           │           │
       └──────────────┴───────────┴───────────┴───────────► FinalSize

  Header->QuotaInfoOffset  = QuotaSize + HandleSize + NameSize + CreatorSize
  Header->HandleInfoOffset = HandleSize + NameSize + CreatorSize
  Header->NameInfoOffset   = NameSize + CreatorSize

为什么这个顺序(而不是反过来)?因为 CreatorInfo 在 Header 紧前面 ,这样 OBJECT_HEADER_TO_CREATOR_INFO 可以用固定的 sizeof(OBJECT_HEADER_CREATOR_INFO) 作为负偏移,无需占用 UCHAR 字段------节省了一个字节(虽然微小,但体现了极致优化)。

4.1.10.3 为什么要这样设计

Q1. 为什么不在对象创建后动态增删 Info 块(例如首次命名时再分配 NameInfo)?

答案:简化并发与内存管理。

  • 池分配代价 :若在命名时动态分配 NameInfo,需要额外的 ExAllocatePoolWithTag 调用;并且在对象生命周期中可能再次分配------这需要在 Header 上增加锁(目前 Header 没有专门的锁,只有类型的 ObjectLocks)。
  • 释放原子性 :对象释放时 ObpDeallocateObject(见 ntoskrnl/ob/oblife.c:37-143(file:///d:/reactos/ntoskrnl/ob/oblife.c#L37-L143))只需一次 ExFreePoolWithTag 即可释放整个块。如果各 Info 块独立分配,要做 N 次释放------增加复杂度与竞态窗口。
  • 一致的大小 :对象创建时大小就固定下来,ObGetObjectInformation 等查询接口可以安全地遍历,无需担心字段在查询过程中从无变有。

唯一例外HandleInfo 在对象首次被多个进程同时打开时,可能从 SingleEntry(存储在 Header 前的结构中)升级为 HandleCountDatabase(额外从池中分配)。这由 Handle Table 管理代码在持锁下进行,是个小而受控的"一次升级"。

Q2. 为什么 CreatorInfo 不用负偏移量字段而用固定 sizeof 判断?

答案 :节省一个 UCHAR,并强化"CreatorInfo 紧接 Header 之前"的不变式。

  • CreatorInfo 的分配条件与其他 Info 块不同 :它由 Type 的 MaintainTypeList 决定,在对象创建时就已知,并且始终存在或始终不存在------不像 NameInfo 取决于调用者是否传入名称。
  • 由于它在 Header 紧前(如果存在),因此 Header - sizeof(OBJECT_HEADER_CREATOR_INFO) 即可定位,无需额外字段。OB_FLAG_CREATOR_INFO 标志位足以判断是否存在。
  • 这反过来又对排列顺序施加了约束:CreatorInfo 必须是离 Header 最近 的 Info 块------这就是 ObpAllocateObject 中最后处理 CreatorInfo 的原因(向前推进 Header 指针前,最后一个被"跳过"的块就是 CreatorInfo)。
Q3. 示例对比
对象 是否有 Name 是否需要 Quota 是否需要 Handle Count CreatorInfo 最终头部大小(示意)
匿名事件(用户态创建) 是(用户态) 是(Event 类型维护句柄计数) 视 NtGlobalFlag QuotaInfo(32) + HandleInfo(16) + Header(48) = 96 bytes
命名文件对象 (路径 C:\x.txt 视 NtGlobalFlag QuotaInfo(32) + HandleInfo(16) + NameInfo(32) + Header(48) = 128 bytes
内核驱动创建的设备对象 是(\Device\MyDevice 否(系统进程,不计配额) 否(Device 类型不维护 HandleCount) NameInfo(32) + Header(48) = 80 bytes
进程对象EPROCESS 否(进程不用名称在命名空间中查找) 通常是 QuotaInfo(32) + HandleInfo(16) + CreatorInfo(16) + Header(48) = 112 bytes

实际大小因 x86/x64 以及字段对齐而变化;上表为概念示意。


4.1.11 增强子节:命名空间遍历与 DosDeviceMap

4.1.11.1 设计意图

Windows 需要在同一台物理机器上隔离多个会话的"可见 DOS 设备名":

  • Session 0(服务、系统进程)拥有完整的 DOS 设备集;
  • Session 1、2...(交互式登录用户)可能看到:
    • 全局设备(C:D: 等磁盘卷);
    • 自己通过 DefineDosDevicesubst 命令创建的本地映射;
    • 不可见其他会话的私有映射。

设计目标:① 使 \??\ 前缀的解析结果取决于当前进程所属会话;② 允许显式访问全局设备(通过 \GLOBAL??\\??\Global\);③ 驱动 substDefineDosDevice、网络重定向盘符等用户态操作。

4.1.11.2 概念解释

DEVICE_MAP 结构

sdk/include/ndk/obtypes.h:523-530(file:///d:/reactos/sdk/include/ndk/obtypes.h#L523-L530):

c 复制代码
typedef struct _DEVICE_MAP
{
    POBJECT_DIRECTORY DosDevicesDirectory;        // 本会话的 \??\ 目录
    POBJECT_DIRECTORY GlobalDosDevicesDirectory;  // 全局 \GLOBAL??\ 目录
    ULONG ReferenceCount;                         // 引用计数(会话死亡时递减)
    ULONG DriveMap;                               // 位图:bit N=1 表示盘符 A+N 存在
    UCHAR DriveType[32];                          // 每个盘符的类型(DRIVE_FIXED/REMOTE/...)
} DEVICE_MAP, *PDEVICE_MAP;

每个进程在 EPROCESS 中有一个 DeviceMap 字段(ReactOS 中对应字段位于 EPROCESS)。当 ObpLookupObjectName 遇到 \??\ 前缀时:

  1. 读取当前进程的 DeviceMap
  2. 切换到 DeviceMap->DosDevicesDirectory 继续解析;
  3. 在该目录查找失败时,回退到 DeviceMap->GlobalDosDevicesDirectory 再查一次(shadow lookup,见 ntoskrnl/ob/obdir.c:288-299(file:///d:/reactos/ntoskrnl/ob/obdir.c#L288-L299))。
会话隔离的 ??\ 目录

系统启动时创建的是全局 \GLOBAL?? 目录。当终端服务会话管理器 (smss.exe) 为每个登录用户创建新会话时:

  1. 创建一个新的 OBJECT_DIRECTORY(类型 Directory);
  2. 为其分配一个 DEVICE_MAP,令其 GlobalDosDevicesDirectory = \GLOBAL??
  3. 将会话内所有进程的 DeviceMap 设置为这张 map;
  4. 当此会话内调用 DefineDosDevice 时,新创建的符号链接被挂到会话私有DosDevicesDirectory 上。
DefineDosDevice / subst 的内核实现

用户态 DefineDosDeviceW(L"Z:", L"\\??\\D:\\MyFolder", ...) 或命令行 subst Z: D:\MyFolder 最终调用:

复制代码
  NtCreateSymbolicLinkObject(
      ObjectAttributes->RootDirectory = 当前会话的 \??\ 目录,
      ObjectAttributes->ObjectName   = L"Z:",
      TargetPath = L"\\??\\D:\\MyFolder"   // 注意这是另一个符号链接
  )

所以 Z: 是一个指向 \??\D:\MyFolder(最终解析为 \Device\HarddiskVolume1\MyFolder)的符号链接,被挂在当前会话的 \??\ 目录下。对其他会话不可见。

全局优先级规则

\??\C: 的解析在会话私有目录找到匹配(已被 subst 覆盖为其他目标)时,优先使用会话私有的 。只有在会话私有目录中完全找不到才回退到 \GLOBAL??。这确保:

  • 用户无法意外"丢失"自己创建的盘符;
  • 管理工具仍可通过 \\?\GLOBALROOT\... 访问真实设备。

4.1.11.3 为什么要这样设计

Q1. 为什么 CMD 的 subst 只对当前用户/当前会话可见?

答案 :因为 subst 最终调用 DefineDosDevice,而它创建的符号链接被放入调用者进程所属会话DosDevicesDirectory。其他会话有各自独立的 DosDevicesDirectory------所以它们看不到这条符号链接。这是设计的初衷:为终端服务(多用户远程登录)提供设备名隔离。

如果希望跨会话可见,需在 DefineDosDevice 中指定 DDD_RAW_TARGET_PATH | DDD_NO_BROADCAST_SYSTEM 并直接在 \GLOBAL?? 下创建,或以系统进程身份调用。

Q2. 为什么不直接用 \GLOBAL??\ 作为 \??\,而要引入每个会话一份?

答案:兼容性 + 安全性。

  • 兼容性 :DOS 时代遗留的 A:B:COM1LPT1 等命名在现代系统上可能不存在------但某些老程序仍会去打开它们。通过会话级的覆盖可以给特定应用"伪造"这些设备。
  • 安全性 :服务(Session 0)拥有的设备(如命名管道 \\.\pipe\...)不应被普通登录会话的用户直接以同名方式访问,否则沙箱失效。
  • 终端服务 :用户 A 映射了 Z: 到他的网络共享 \\serverA\share,用户 B 不应看到;这是基本的会话隔离。
Q3. 为什么 \??\ 前缀的识别用硬编码 WCHAR 比较而不是目录遍历?

答案 :性能。ObpLookupObjectName 是所有文件/设备打开路径上的热点。识别 \??\ 前缀可以立即跳转到 DosDevicesDirectory 而无需从根目录逐级遍历------节省 2 次目录查找。

源码中使用的 64-bit 整数比较技巧(ntoskrnl/ob/obname.c:689-716(file:///d:/reactos/ntoskrnl/ob/obname.c#L689-L716)):

c 复制代码
// "\??\" 恰好是 4 个 WCHAR = 8 字节,可放入一个 ULONGLONG
if ((ObjectName->Length >= 4*sizeof(WCHAR)) &&
    (*(PULONGLONG)ObjectName->Buffer == ObpDosDevicesShortNamePrefix.Alignment.QuadPart)) {
    DeviceMap = ObpReferenceDeviceMap();
    // 跳过前缀,剩余路径从第 4 个字符开始
    LocalName.Length -= 4*sizeof(WCHAR);
    LocalName.Buffer += 4;
    Directory = DeviceMap->DosDevicesDirectory;
}

这是 O(1) 的比较,避免了对路径前缀的 hash 计算和目录遍历。


4.1.12 增强子节:Permanent 对象与延迟删除

4.1.12.1 设计意图

  1. 系统关键对象的存活保证 :根目录 \、类型目录 \ObjectTypes\??\ 等对象在系统整个生命周期中必须存在。如果某个驱动"忘记"引用它们,对象不应消失------OB_FLAG_PERMANENT 保证这一点。
  2. 热路径上的快速释放 :当对象的 PointerCount 在 APC 禁用的上下文(例如 DPC 级别)中降到 0 时,立即调用 ObpDeleteObject 是不安全的(可能在高 IRQL 上调用被动级的 ExFreePoolWithTag)。延迟删除将释放工作移到 PASSIVE_LEVEL 的工作项中异步完成。
  3. 用户态可控的对象生命周期NtMakePermanentObject / NtMakeTemporaryObject 允许驱动或管理员将某个对象"钉"在命名空间中或释放它。

4.1.12.2 概念解释

OB_FLAG_PERMANENT 的语义
  • 当对象被标记为 OB_FLAG_PERMANENT 时,它的 PointerCount 始终保持 ≥ 1(在对象创建时被显式 +1);
  • 即使所有用户态句柄关闭、所有内核指针引用解除,对象仍不被释放
  • ObpDeleteNameCheckntoskrnl/ob/obname.c:299-391(file:///d:/reactos/ntoskrnl/ob/obname.c#L299-L391))检查到该标志时跳过从目录中移除名称的操作;
  • 需要通过 NtMakeTemporaryObject 显式"去永久化"才能让对象走正常的释放流程。
NtMakePermanentObject / NtMakeTemporaryObject

两者的实现(见 ntoskrnl/ob/oblife.c:1508-1535(file:///d:/reactos/ntoskrnl/ob/oblife.c#L1508-L1535))非常对称:

c 复制代码
// 内部通用辅助函数
VOID FASTCALL ObpSetPermanentObject(PVOID ObjectBody, BOOLEAN Permanent)
{
    POBJECT_HEADER Header = OBJECT_TO_OBJECT_HEADER(ObjectBody);
    ObpAcquireObjectLock(Header);

    if (Permanent) {
        Header->Flags |= OB_FLAG_PERMANENT;
    } else {
        Header->Flags &= ~OB_FLAG_PERMANENT;
    }
    ObpReleaseObjectLock(Header);

    // 去永久化 → 检查是否已无句柄/引用,若是则触发删除
    if (!Permanent) ObpDeleteNameCheck(ObjectBody);
}
延迟删除(OB_FLAG_DEFER_DELETE / ObpReaperWorkItem)

ObDereferenceObject 在 APC 禁用的上下文中被调用(例如在 DPC、持有 spin lock 的代码路径)时,不能立即释放对象(因为池分配/释放要求 PASSIVE_LEVEL)。此时调用 ObpDeferObjectDeletion(见 ntoskrnl/ob/obref.c:51-73(file:///d:/reactos/ntoskrnl/ob/obref.c#L51-L73)):

c 复制代码
VOID NTAPI ObpDeferObjectDeletion(POBJECT_HEADER Header)
{
    PVOID Entry;
    do {
        Entry = ObpReaperList;
        Header->NextToFree = Entry;   // 复用 HandleCount 字段的内存作为链表指针
    } while (InterlockedCompareExchangePointer(&ObpReaperList, Header, Entry) != Entry);

    if (!Entry) {
        // 列表从空变非空 → 首次有对象入链 → 排队工作项
        ExQueueWorkItem(&ObpReaperWorkItem, CriticalWorkQueue);
    }
}

ObpReaperWorkItem(工作项回调 ObpReapObject,见 ntoskrnl/ob/oblife.c:218-244(file:///d:/reactos/ntoskrnl/ob/oblife.c#L218-L244))在 PASSIVE_LEVEL 被调用时,原子地取走整个链表 并逐个调用 ObpDeleteObject 释放对象。

注意 HandleCount / NextToFree 的 union 设计:在对象仍然存活时 HandleCount 有效;在对象被标记删除后(此时 HandleCount 必为 0,否则不会进入删除),NextToFree 被用作链表指针。这是一个典型的"节省内存的 union"手法。

4.1.12.3 为什么要这样设计

Q1. 为什么不在系统启动时把永久对象的引用计数设置为一个巨大的数(如 0x80000000),而要引入一个标志位?

答案:两个原因------① 引用计数与删除逻辑需要统一;② 可撤销。

  • 统一引用计数模型 :如果用 "magic number",那么 ObDereferenceObject 在递减时必须检查 if (PointerCount > SOME_THRESHOLD) ...------不优雅,且有 overflow risk。OB_FLAG_PERMANENT 作为一个独立的布尔位,在 ObpDeleteNameCheck 和释放路径上直接判断,简洁。
  • 可撤销 :永久对象需要能变回普通对象(NtMakeTemporaryObject)。用一个标志位切换比"往回增加"引用计数更自然------后者难以区分"是系统自动加的永久引用还是驱动显式引用"。
Q2. 为什么延迟删除使用无锁链表 + 工作项,而不是直接排队 DPC?

答案:避免在 DPC 级别进行复杂释放。

  • ExQueueWorkItem(CriticalWorkQueue) 会将工作项挂到系统工作线程,在 PASSIVE_LEVEL 执行。此时可以安全地调用 ObpDeleteObject(其内部的 DeleteProcedure 可能涉及复杂的 I/O 资源回收)。
  • InterlockedCompareExchangePointer 实现的无锁入队确保了任何 IRQL 上都能安全排队
  • 只在链表从空变非空时才排队一次工作项,避免工作项风暴。
Q3. NextToFree 复用 HandleCount 的字段是否存在竞态?

答案 :否。当 ObpDeferObjectDeletion 被调用时,PointerCount 已经降到 0,而在 PointerCount 降到 0 之前 HandleCount 必须已经是 0(由 ASSERT(Header->HandleCount == 0) 保证)。也就是说,当代码决定"延迟删除"时,对象已经处于"所有句柄已关闭 + 所有指针引用已解除"的状态,HandleCount 不再被读取------复用其内存作为链表指针是安全的。


4.1.13 小结(增强版)

本节覆盖的主要内容

  1. 对象与对象目录的统一抽象 :对象管理器将所有内核资源(进程、线程、文件、事件、驱动、设备、目录、符号链接......)统一为带 OBJECT_HEADER 的对象,并通过对象目录组成 / 根目录下的命名空间。
  2. OBJECT_HEADER 的布局PointerCountHandleCountTypeFlags 与四个可选 Info 块(Name/Handle/Quota/Creator)通过负偏移量 定位到 Header 之前。OBJECT_TO_OBJECT_HEADER 宏用 CONTAINING_RECORD 从对象体回溯到对象头。
  3. 对象创建流程ObCreateObjectObpCaptureObjectCreateInformationObpAllocateObject 一次性分配 Header + Info 块 + Body,返回 Body 指针。后续由 ObInsertObject 完成命名空间插入与句柄分配。
  4. 双计数器的生命周期管理PointerCount 控制对象是否可释放,HandleCount 控制对象是否从目录中移除名称。所有增减都用 InterlockedIncrementSizeT / InterlockedDecrementSizeT 完成,确保 SMP 安全。
  5. 37-bucket Hash 目录 :对象目录以 hash 表(NUMBER_HASH_BUCKETS = 37)实现,对大小写不敏感的名称做 hash → 取模 → 链表线性查找;命中条目被提升到 bucket 头部以加速重复访问。
  6. 名称解析 ObpLookupObjectName :逐段解析 \ 分隔的路径;识别 \??\ 前缀跳转到会话 DOS 设备目录;通过 OBP_LOOKUP_CONTEXT 缓存 hash 并持有锁以完成"查找 → 插入"的原子操作;支持最多 30 层符号链接 reparse。
  7. 符号链接与会话隔离\??\C:\Device\HarddiskVolume1 的透明解析;每个会话有自己的 DosDevicesDirectorysubst 命令仅对当前会话可见;\GLOBAL??\ 允许显式访问全局设备。
  8. Permanent 对象与延迟删除OB_FLAG_PERMANENT 保证根目录等关键对象永不被引用计数释放;OB_FLAG_DEFER_DELETE + ObpReaperWorkItem 让在高 IRQL 上无法立即释放的对象在工作项中异步清理。

关键数据结构 vs 对应函数对照表

数据结构 主要操作函数 所在文件
OBJECT_HEADER ObCreateObject, ObpAllocateObject, ObReferenceObject, ObDereferenceObject, ObpDeleteObject oblife.c, obref.c
OBJECT_HEADER_NAME_INFO ObpAllocateObject, ObpLookupEntryDirectory, ObQueryNameString oblife.c, obdir.c, obname.c
OBJECT_HEADER_HANDLE_INFO ObpIncrementHandleCount, ObpDecrementHandleCount oblife.c, handle 表相关
OBJECT_HEADER_QUOTA_INFO ObpAllocateObject, PsChargePoolQuota oblife.c
OBJECT_HEADER_CREATOR_INFO ObpAllocateObject, ObInitSystem 中遍历 TypeList oblife.c, obinit.c
OBJECT_DIRECTORY NtCreateDirectoryObject, ObpInsertEntryDirectory, ObpLookupEntryDirectory, ObpDeleteEntryDirectory obdir.c
OBJECT_DIRECTORY_ENTRY (Hash 链表节点,内部使用) obdir.c
OBJECT_SYMBOLIC_LINK NtCreateSymbolicLinkObject, ObpParseSymbolicLink obname.c
OBP_LOOKUP_CONTEXT ObpLookupObjectName, ObpInsertEntryDirectory, ObpLookupEntryDirectory obname.c, obdir.c
DEVICE_MAP ObSetDeviceMap, ObpCreateDosDevicesDirectory obname.c
ObpReaperList / ObpReaperWorkItem ObpDeferObjectDeletion, ObpReapObject obref.c, oblife.c

下一步的学习路径

  • 4.2 对象类型 :深入 OBJECT_TYPEOBJECT_TYPE_INITIALIZER------类型如何注册、OpenProcedure/CloseProcedure/DeleteProcedure/ParseProcedure/SecurityProcedure/QueryNameProcedure 如何联动、类型对象的 TypeList 与 !object 调试命令的实现。
  • 4.3 句柄表HANDLE_TABLE 的三层表结构(LOW_LEVEL_ENTRIES / MID_LEVEL_ENTRIES / HIGH_LEVEL_ENTRIES)、句柄值编码(3 位标记位 + 29 位索引)、句柄表扩容与审计、ExMapHandleToPointer 的加锁策略、内核句柄表与进程句柄表的差异。
  • 4.4 对象安全SECURITY_DESCRIPTOR 在对象头中的存储、ObCheckObjectAccess / ObpAuditObjectAccess 的访问检查流程、自主访问控制(DACL)与强制完整性控制(MIC)。
  • 4.5 符号链接 / 设备映射ObpParseSymbolicLink 的完整实现、DefineDosDevice 在内核中的完整语义、会话创建时 ObSetDeviceMap 如何切换 EPROCESS.DeviceMap
    </seed:tool_call>
相关推荐
C137的本贾尼1 小时前
InnoDB 内存架构:Buffer Pool、Change Buffer 与 Log Buffer
数据库·oracle·架构
半兽先生1 小时前
flv.js解决其中一个监控断线导致其他的监控播放阻塞
开发语言·javascript·ecmascript
玖玥拾1 小时前
C/C++ 基础笔记(九)联合、枚举及文件操作
c语言·c++·文件操作·枚举·联合
canonical_entropy1 小时前
吸引子引导与轨迹挖掘:AI Native Engineering 的收敛机制
数学·架构·ai编程
小糯米6011 小时前
C语言 动态内存管理
c语言·开发语言
Dxy12393102161 小时前
BAT 窗口不输出日志:三种静默方案,从半隐藏到完全消失
linux·运维·服务器
invicinble1 小时前
关于postgersql相关技术栈的总结
架构
@insist1231 小时前
系统架构设计师-从 PDR到 WPDRRC 的模型演进与架构实践
架构·系统架构·软考·系统架构设计师·软件水平考试
say_fall2 小时前
可编程中断控制器8259A工作方式超详细解析
android·开发语言·学习·硬件架构·硬件工程