Reactos 第 4 章 对象管理 — 4.4 对象的创建

第 4 章 对象管理 --- 4.4 对象的创建

本节是第 4 章的"实战篇"------把 4.1~4.3 节分别讨论的「对象头」「对象类型」「句柄表」三者串联起来,形成一条完整的"从用户态 API 到内核态对象"的调用链。我们将沿着 NtCreateEvent 这条最经典的系统调用,逐行追踪 ntoskrnl/ob/obhandle.c(file:///d:/reactos/ntoskrnl/ob/obhandle.c) 中 ObInsertObject 的实现,揭示对象创建过程中参数解析、配额扣除、名称插入、句柄分配的完整流程。

4.4.0 框架图

复制代码
┌──────────────────────────────────────────────────────────────────────────────────┐
│                                用户态 API                                          │
│   CreateEvent() / CreateFile() / CreateProcess()  (kernel32.dll / advapi32.dll)   │
└──────────────────────────────────────────────────────────────────────────────────┘
                                       │ 系统调用(int 2E / sysenter)
                                       ▼
┌──────────────────────────────────────────────────────────────────────────────────┐
│                          内核 API 层 (Win32 子系统 API)                              │
│   NtCreateEvent() / NtCreateFile() / NtCreateProcess() / NtCreateKey()           │
│   位置:ntoskrnl/ke/xxx.c  /  ntoskrnl/se/xxx.c  /  ntoskrnl/ob/create.c           │
└──────────────────────────────────────────────────────────────────────────────────┘
                                       │ 调用 ObCreateObjectType 系列
                                       ▼
┌──────────────────────────────────────────────────────────────────────────────────┐
│                          对象管理器层 (Object Manager)                              │
│   ┌──────────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────────┐    │
│   │ ObCreateObject│───▶│ ObpAllocateObject│    │ ObCreateObjectType  │    │
│   │   (oblife.c) │    │  (oblife.c)   │    │  (obinit.c)  │    │              │    │
│   └──────────────┘    └──────────────┘    └──────────────┘    └──────────────┘    │
│          │                                                                       │
│          │   分配 OBJECT_HEADER + Info 块 + 对象体 (Body)                          │
│          ▼                                                                       │
│   ┌──────────────────────────────────────────────────────────────────────┐       │
│   │   OBJECT_HEADER + NameInfo + HandleInfo + QuotaInfo + CreatorInfo    │       │
│   │   ──────────────────────────┬────────────────────────────────────── │       │
│   │                             ▼                                        │       │
│   │                       Object Body (KEVENT / EPROCESS / FILE_OBJECT)  │       │
│   └──────────────────────────────────────────────────────────────────────┘       │
│                                                                                  │
│   ┌──────────────┐    ┌──────────────┐    ┌──────────────┐                       │
│   │ObpCaptureObject│   │ObpCaptureName│   │ObpProbeAndCaptureObjectAttributes │ │
│   │CreateInfo    │   │              │   │   (oblife.c) │                       │
│   └──────────────┘    └──────────────┘    └──────────────┘                       │
│                                                                                  │
│   ┌──────────────┐    ┌──────────────┐    ┌──────────────┐                       │
│   │ ObInsertObject│───▶│ObpLookupObject│    │ObpChargeQuota│    │ObpCreateHandle │ │
│   │ (obhandle.c) │    │   Name        │   │  ForObject   │    │  (obhandle.c)  │ │
│   └──────────────┘    └──────────────┘    └──────────────┘    └──────────────┘    │
│          │                                                                       │
│          │   调用 4.5.4 节详细讨论的 ObpLookupObjectName                          │
│          │   详见 4.4.4 节配额扣除机制                                            │
│          │   调用 ObpCreateHandle(详见 4.3.4 节)                                │
└──────────────────────────────────────────────────────────────────────────────────┘
                                       │
                                       ▼
┌──────────────────────────────────────────────────────────────────────────────────┐
│                          句柄表层 (Handle Table)                                    │
│   详见 4.3.2 节 HANDLE_TABLE 三级页表式结构                                         │
│   ┌──────────────────────────────────────────────────────────────┐               │
│   │  HANDLE_TABLE  ──▶  TableCode[Low2Bit=Level]                │               │
│   │       │              │                                       │               │
│   │       ▼              ▼                                       │               │
│   │  HANDLE_TABLE_ENTRY[]  (16 字节/Entry)                       │               │
│   │       │  Object (低 3 位用作 TagBits)                          │               │
│   │       ▼                                                      │               │
│   │  POBJECT_HEADER  ──▶  POBJECT_BODY                           │               │
│   └──────────────────────────────────────────────────────────────┘               │
└──────────────────────────────────────────────────────────────────────────────────┘
                                       │
                                       ▼
┌──────────────────────────────────────────────────────────────────────────────────┐
│                          后续类型专属处理                                            │
│   KeInitializeEvent() / PspInitializeProcess() / IopInitializeFileObject()         │
│   (由各子系统的创建 API 自行调用,详见 4.4.7 节三种典型 API 对比)                  │
└──────────────────────────────────────────────────────────────────────────────────┘

4.4.0.1 设计意图

核心问题

用户态程序需要"创建一个内核对象"------这是对象管理器最基础、也是最频繁的操作之一。但从"用户态调用一个 API"到"内核真正分配对象、插入命名空间、返回句柄"之间,需要穿过 6~7 个抽象层:API 包装层、系统调用层、参数捕获层、对象分配层、配额扣除层、名称查找层、句柄分配层。每一层都有自己的职责和安全检查。

这个过程的核心难点在于:

  1. 多阶段错误处理:任何一步失败都可能导致内存泄漏、命名空间污染、配额耗尽等问题
  2. 安全边界保护 :用户态传入的 UNICODE_STRINGSECURITY_DESCRIPTOR 必须在受控的"捕获"阶段被复制到内核空间
  3. 资源一致性:配额、对象、句柄三者的生命周期必须精确协调

设计哲学

对象创建采用「三步走」的清晰模式:

  1. 第一步:分配ObCreateObject)------ 只负责"分配壳"(Header + Info 块 + Body),不关心类型语义
  2. 第二步:初始化 (调用方代码)------ 由类型专属的初始化函数(KeInitializeEvent 等)填充 Body
  3. 第三步:插入与句柄ObInsertObject)------ 一次性完成配额扣除、名称查找、句柄分配

这种"三步走"的设计哲学与 Unix 内核的 create + init + publish 模式异曲同工,但通过明确的边界保证了:

  • 可回滚性:每一步都可以独立失败,已分配的资源在错误路径上可被正确清理
  • 类型无关性:对象管理器不关心 Body 的具体内容,类型逻辑下放给子系统
  • 安全性统一性:所有安全检查(参数捕获、配额扣除、权限检查)都在对象管理器层完成

本节定位

4.1~4.3 节是"原子化"的------分别讨论了对象头、对象类型、句柄表三个独立组件。本节是"分子化"的------把这些组件组合起来,讨论一个完整的创建流程。我们将沿着源代码逐层追踪 ObInsertObject 的实现(位于 obhandle.c:2935(file:///d:/reactos/ntoskrnl/ob/obhandle.c#L2935)),揭示:

  • 用户态 OBJECT_ATTRIBUTES 如何被捕获为 OBJECT_CREATE_INFORMATION
  • 配额检查失败时的回滚机制
  • 名称查找的完整流程(详见 4.5.4 节)
  • 句柄分配与权限计算

4.4.1 创建流程总览

4.4.1.1 完整的调用链

CreateEvent 为例,完整的调用链如下:

复制代码
用户态 (kernel32.dll)
└── CreateEventW()                          [wraps parameters to NT API]
    └── ntdll!NtCreateEvent()                [system call entry]
        └── 内核态 (ntoskrnl)
            └── NtCreateEvent()              [ke/xxxx.c --- system call dispatcher]
                ├── ProbeForRead()           [verify user-mode pointer access]
                ├── ObpProbeAndCaptureObjectAttributes()  [capture OBJECT_ATTRIBUTES]
                │   └── ObpCaptureObjectCreateInformation()
                │       ├── ProbeForRead(OBJECT_ATTRIBUTES)
                │       ├── ProbeForRead(UNICODE_STRING)
                │       ├── Capture Unicode string into kernel pool
                │       └── Capture SecurityDescriptor (if provided)
                ├── ObCreateObject()          [allocate Header + Info + Body]
                │   ├── ExAllocatePoolWithTag()  [single allocation]
                │   ├── RtlZeroMemory(Body)    [zero body for safety]
                │   ├── ObpSetObjectType()     [set Type field]
                │   ├── ObpAllocateObjectNameInfo()  [allocate NameInfo if named]
                │   └── Initialize PointerCount = 1
                ├── <调用方执行> KeInitializeEvent()  [initialize Body fields]
                ├── ObInsertObject()          [insert into namespace + create handle]
                │   ├── ObpChargeQuotaForObject()  [deduct pool quota]
                │   ├── ObpLookupObjectName()      [name resolution + creation]
                │   │   ├── ObpLookupEntryDirectory()
                │   │   └── (recursive Parse for symlinks)
                │   ├── ObpCreateHandle()           [allocate handle entry]
                │   └── ExCreateHandle()            [底层 handle table op]
                └── KeReleaseSemaphore() 或类似    [free capture buffer]

4.4.1.2 两个核心 API 的职责划分

函数 文件 核心职责 不负责
ObCreateObject oblife.c:1037-1132(file:///d:/reactos/ntoskrnl/ob/oblife.c#L1037-L1132) 分配 Header + Info 块 + Body;初始化公共字段 初始化 Body 内部;插入命名空间;分配句柄
ObInsertObject obhandle.c:2935-3070(file:///d:/reactos/ntoskrnl/ob/obhandle.c#L2935-L3070) 配额扣除、名称查找、句柄分配 分配内存;初始化 Body

这种职责划分的好处:

  • 模块化:每个函数只做一件事,单元测试和调试都更容易
  • 可组合性 :可以单独调用 ObCreateObject 而不插入命名空间(用于内部对象)
  • 错误隔离ObCreateObject 失败时无需回滚命名空间(还没插入);ObInsertObject 失败时只需回滚配额和句柄

4.4.1.3 关键调用时序图

复制代码
时间轴 ─────────────────────────────────────────────────────────────────▶

  用户态
    │  CreateEventW(...)
    │
    ▼  [sysenter]
  内核态
    │
    │  ProbeForRead(ObjectAttributes)  ← 验证用户态指针
    │       │
    │       ▼
    │  ObpProbeAndCaptureObjectAttributes()
    │       │
    │       ├─ 分配临时缓冲区
    │       ├─ ProbeForRead + copy_from_user
    │       │
    │       ▼
    │  ObCreateObject(KEVENT, sizeof(KEVENT), ...)
    │       │
    │       ├─ ExAllocatePoolWithTag(NonPagedPool, total_size)
    │       ├─ RtlZeroMemory
    │       ├─ OBJECT_HEADER.PointerCount = 1
    │       ├─ OBJECT_HEADER.Type = ExEventObjectType
    │       ├─ OBJECT_HEADER.HandleCount = 0
    │       │
    │       ▼
    │  KeInitializeEvent(Event, NotificationEvent, FALSE)  ← 调用方初始化 Body
    │       │
    │       ▼
    │  ObInsertObject(Event, AccessState, ..., &Handle)
    │       │
    │       ├─ ObpChargeQuotaForObject(Process, KEVENT)
    │       │     └─ PsChargePoolQuota()  ← 失败则 ExFreePool + return
    │       │
    │       ├─ ObpLookupObjectName(...)
    │       │     ├─ 匿名对象:直接跳过
    │       │     └─ 命名对象:ObpLookupEntryDirectory(...)
    │       │
    │       ├─ ObpCreateHandle(Process, Event, ...)
    │       │     └─ ExCreateHandle()  ← 分配句柄 Entry
    │       │
    │       ▼
    │  *ReturnedHandle = Handle
    │       │
    │       ▼
    │  ObpFreeCapturedAttributes(CapturedInfo)  ← 释放临时缓冲区
    │
    ▼  [sysexit / iret]
  用户态
    │
    ▼
  返回 HANDLE 给用户态

4.4.2 OBJECT_ATTRIBUTES 参数解析

4.4.2.1 用户态结构定义

OBJECT_ATTRIBUTES 是用户态 → 内核态传递对象创建参数的标准结构:

c 复制代码
typedef struct _OBJECT_ATTRIBUTES {
    ULONG Length;                      // sizeof(OBJECT_ATTRIBUTES),版本控制
    HANDLE RootDirectory;              // 相对路径的根目录(NULL = 绝对路径)
    PUNICODE_STRING ObjectName;        // 指向用户态 UNICODE_STRING(待捕获)
    ULONG Attributes;                  // OBJ_INHERIT | OBJ_PERMANENT | ...
    PVOID SecurityDescriptor;          // 安全描述符(可选)
    PVOID SecurityQualityOfService;    // QoS(可选)
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;

定义在 sdk/include/ntos/io.h(file:///d:/reactos/sdk/include/ntos/io.h) 和 Windows SDK 的 winnt.h 中。

4.4.2.2 关键标志位

Attributes 字段是位掩码,常用值:

标志 含义
OBJ_INHERIT 0x00000002 句柄可被子进程继承
OBJ_PERMANENT 0x00000010 对象是永久的,不会因引用计数归零而删除
OBJ_EXCLUSIVE 0x00000020 一次只允许一个句柄
OBJ_CASE_INSENSITIVE 0x00000040 名称比较大小写不敏感
OBJ_OPENIF 0x00000080 已存在同名对象时打开而非失败
OBJ_OPENLINK 0x00000100 打开符号链接的目标(而非链接本身)
OBJ_KERNEL_HANDLE 0x00000200 创建内核句柄
OBJ_FORCE_ACCESS_CHECK 0x00000400 强制安全检查(即使来自内核态)
OBJ_IGNORE_IMPERSONATED_DEVICEMAP 0x00000800 忽略被模拟的设备映射

4.4.2.3 内核态的捕获流程

ObpProbeAndCaptureObjectAttributesoblife.c(file:///d:/reactos/ntoskrnl/ob/oblife.c))将用户态结构复制到内核池:

c 复制代码
NTSTATUS
NTAPI
ObpProbeAndCaptureObjectAttributes(IN POBJECT_ATTRIBUTES UserObjectAttributes,
                                   IN KPROCESSOR_MODE AccessMode,
                                   IN ULONG AllocateSize,
                                   IN POBJECT_CREATE_INFORMATION ObjectCreateInfo,
                                   IN PUNICODE_STRING ObjectName /* 已捕获的 Name */)
{
    POBJECT_ATTRIBUTES CapturedAttributes;
    PSECURITY_DESCRIPTOR SecurityDescriptor;
    PSECURITY_QUALITY_OF_SERVICE SecurityQos;
    ULONG CapturedSize = sizeof(OBJECT_ATTRIBUTES_INFORMATION);

    /* 1. 分配内核缓冲区 */
    CapturedAttributes = ExAllocatePoolWithTag(PagedPool, AllocateSize, 'tAbO');
    if (!CapturedAttributes) return STATUS_INSUFFICIENT_RESOURCES;

    /* 2. 探测并复制 OBJECT_ATTRIBUTES 头 */
    Status = ProbeAndReadSafeBuffer(UserObjectAttributes, sizeof(OBJECT_ATTRIBUTES));
    RtlCopyMemory(CapturedAttributes, UserObjectAttributes, sizeof(OBJECT_ATTRIBUTES));

    /* 3. 探测并复制 UNICODE_STRING(ObjectName) */
    if (CapturedAttributes->ObjectName) {
        ProbeForRead(CapturedAttributes->ObjectName, sizeof(UNICODE_STRING), ...);
        /* 复制 UNICODE_STRING 头 */
        CapturedAttributes->ObjectName->Length = ...;
        CapturedAttributes->ObjectName->Buffer = ExAllocatePoolWithTag(...);
        ProbeForRead(UserName->Buffer, UserName->Length, sizeof(WCHAR));
        RtlCopyMemory(CapturedAttributes->ObjectName->Buffer, UserName->Buffer, UserName->Length);
    }

    /* 4. 探测并复制 SecurityDescriptor(可选) */
    if (CapturedAttributes->SecurityDescriptor) {
        SecurityDescriptor = MmAllocatePool(PagedPool, ...);
        /* 复制 SD(详见 4.4.5 节) */
    }

    /* 5. 探测并复制 SecurityQos(可选) */
    /* ... */

    /* 6. 填充 ObjectCreateInfo(捕获后的结构) */
    ObjectCreateInfo->RootDirectory = CapturedAttributes->RootDirectory;
    ObjectCreateInfo->Attributes = CapturedAttributes->Attributes;
    ObjectCreateInfo->SecurityDescriptor = CapturedAttributes->SecurityDescriptor;
    ObjectCreateInfo->SecurityQos = CapturedAttributes->SecurityQos;
    ObjectCreateInfo->ObjectName = CapturedAttributes->ObjectName;

    return STATUS_SUCCESS;
}

4.4.2.4 Probe 的安全意义

ProbeForReadProbeForWrite 是 Windows 内核对用户态指针的标准检查机制:

  • 防止 TOCTOU(Time-of-Check to Time-of-Use)攻击:在用户态指针被复制之前,确保指针指向的内存是可读的
  • 防止任意内核读写:未捕获的用户态指针在内核中不能直接解引用
  • 处理分页问题:用户态页面可能被换出到磁盘,捕获阶段确保页面已被锁定到内存

这与 Unix 的 copy_from_user / copy_to_user 完全同构,但 Windows 的 Probe* API 设计上要求先探测、再复制(两步走),提供更细粒度的控制。


4.4.3 OBJECT_CREATE_INFORMATION 详解

4.4.3.1 内核态"捕获后"结构

OBJECT_CREATE_INFORMATIONOBJECT_ATTRIBUTES 捕获到内核空间后的"内部表示"。它在内核中以 _OBJECT_CREATE_INFORMATION 形式存在(oblife.c(file:///d:/reactos/ntoskrnl/ob/oblife.c)):

c 复制代码
typedef struct _OBJECT_CREATE_INFORMATION {
    ULONG Attributes;                  // 已验证的标志
    HANDLE RootDirectory;              // 已验证的根目录句柄
    PUNICODE_STRING ObjectName;        // 已捕获到内核池的名称(不必再 Probe)
    PVOID SecurityDescriptor;          // 已捕获的 SD(可选)
    PVOID SecurityQualityOfService;    // 已捕获的 QoS(可选)
    PSECURITY_DESCRIPTOR SecurityDescriptorOffset;  // 父目录 SD(若需要继承)
    PVOID EaBuffer;                    // 扩展属性(File 类型专用)
    ULONG EaLength;
    // ... 其他内部字段
} OBJECT_CREATE_INFORMATION, *POBJECT_CREATE_INFORMATION;

4.4.3.2 内部表示与外部表示的差异

方面 OBJECT_ATTRIBUTES(外部) OBJECT_CREATE_INFORMATION(内部)
指针位置 用户态 内核池
可访问性 需要 Probe 已是内核指针,可直接解引用
长度字段 用户态可控制 由内核在捕获时填充
生命周期 系统调用结束即释放 在对象创建期间持续有效
额外信息 仅基础属性 可包含父目录 SD、EA 缓冲等

4.4.3.3 关键转换函数

内核中的两个核心函数负责 OBJECT_ATTRIBUTESOBJECT_CREATE_INFORMATION 之间的转换:

函数 位置 职责
ObpProbeAndCaptureObjectAttributes oblife.c(file:///d:/reactos/ntoskrnl/ob/oblife.c) 探测 + 捕获用户态结构
ObpReleaseObjectCreateInformation oblife.c(file:///d:/reactos/ntoskrnl/ob/oblife.c) 释放捕获时分配的临时缓冲区
ObpAllocateObjectNameInfo oblife.c(file:///d:/reactos/ntoskrnl/ob/oblife.c) 分配 OBJECT_HEADER_NAME_INFO 并填充 Name
ObpFreeObjectNameInfo oblife.c(file:///d:/reactos/ntoskrnl/ob/oblife.c) 释放 OBJECT_HEADER_NAME_INFO

4.4.4 配额扣除与还原机制

4.4.4.1 为什么需要配额?

ObpChargeQuotaForObjectobhandle.c:429-484(file:///d:/reactos/ntoskrnl/ob/obhandle.c#L429-L484))负责在对象创建时从当前进程扣除池配额。这是为了:

  1. 防止资源耗尽:单进程无法通过创建大量小对象耗尽系统池
  2. 公平分配:每个进程按配额获得资源份额
  3. 进程退出时自动清理:进程对象被销毁时自动释放其配额

4.4.4.2 配额的构成

OBJECT_TYPE_INITIALIZER 中有两个关键字段定义配额:

c 复制代码
ULONG DefaultPagedPoolCharge;        // 默认分页池配额
ULONG DefaultNonPagedPoolCharge;     // 默认非分页池配额

配额的具体值在 ObpChargeQuotaForObject 中计算:

c 复制代码
QuotaCharge = ObjectType->TypeInfo.DefaultNonPagedPoolCharge
            + ObjectType->TypeInfo.DefaultPagedPoolCharge
            + ObjectHeaderSize              // OBJECT_HEADER + Info 块
            + ALIGN_UP(ObjectName->Length, sizeof(PVOID))  // 名称缓冲
            + 0; // 各种附加费用

4.4.4.3 配额扣除的实现

c 复制代码
NTSTATUS
NTAPI
ObpChargeQuotaForObject(IN POBJECT_HEADER ObjectHeader,
                         IN PUNICODE_STRING ObjectName,
                         IN POBJECT_TYPE ObjectType)
{
    PEPROCESS Process = PsGetCurrentProcess();
    ULONG PagedPoolCharge, NonPagedPoolCharge;

    /* 计算总配额 */
    NonPagedPoolCharge = ObjectType->TypeInfo.DefaultNonPagedPoolCharge
                       + ObjectHeader->HeaderSize;
    PagedPoolCharge = ObjectType->TypeInfo.DefaultPagedPoolCharge
                    + (ObjectName ? ObjectName->Length : 0);

    /* 从进程配额中扣除 */
    if (ObjectType->TypeInfo.PoolType == NonPagedPool) {
        Status = PsChargePoolQuota(Process, NonPagedPool, NonPagedPoolCharge);
    } else {
        Status = PsChargePoolQuota(Process, PagedPool, PagedPoolCharge);
    }

    if (!NT_SUCCESS(Status)) {
        return Status;
    }

    /* 记录已扣除的配额(用于回滚) */
    ObjectHeader->QuotaBlockCharged = Process->Pcb.QuotaBlock;

    return STATUS_SUCCESS;
}

4.4.4.4 配额回滚

如果在 ObInsertObject 后续步骤中失败,需要回滚已扣除的配额:

c 复制代码
VOID
NTAPI
ObpUndoChargeQuotaForObject(IN POBJECT_HEADER ObjectHeader)
{
    PEPROCESS Process = PsGetCurrentProcess();
    ULONG PagedPoolCharge, NonPagedPoolCharge;
    POBJECT_TYPE ObjectType = ObjectHeader->Type;

    NonPagedPoolCharge = ObjectType->TypeInfo.DefaultNonPagedPoolCharge
                       + ObjectHeader->HeaderSize;
    PagedPoolCharge = ObjectType->TypeInfo.DefaultPagedPoolCharge
                    + ObjectHeader->NameInfoOffset ? /* 计算 NameInfo */ : 0;

    PsReturnPoolQuota(Process->Pcb.QuotaBlock, PagedPool, PagedPoolCharge);
    PsReturnPoolQuota(Process->Pcb.QuotaBlock, NonPagedPool, NonPagedPoolCharge);
}

ObInsertObject 中的错误回滚路径会在 STATUS_* 返回前调用 ObpUndoChargeQuotaForObject,确保配额状态一致。

4.4.4.5 设计原则:两阶段提交

配额扣除采用"两阶段提交"模式:

  1. 预扣除ObpChargeQuotaForObject):在 ObCreateObject 之后立即扣除
  2. 提交/回滚ObInsertObject 成功时提交,失败时回滚)

这种模式的好处:

  • 失败时简单回滚:如果对象创建失败,只需"撤销"已扣除的配额
  • 成功后无需特殊处理:成功路径上配额自动归新对象所有
  • 避免重复计算:配额只需计算一次(在预扣除时)

4.4.5 安全描述符捕获

4.4.5.1 SECURITY_DESCRIPTOR 的双重含义

SECURITY_DESCRIPTOR 在对象创建路径上有两种角色:

  1. 新对象的安全描述符 (用户在 OBJECT_ATTRIBUTES 中显式提供)
  2. 父目录的默认安全描述符(用于继承)

4.4.5.2 捕获流程

ObpCaptureObjectCreateInformation 在捕获 OBJECT_ATTRIBUTES 时,会同时处理 SecurityDescriptor:

c 复制代码
if (CapturedAttributes->SecurityDescriptor) {
    /* 用户提供的 SD:完整复制 */
    ProbeForRead(SecurityDescriptor, sizeof(SECURITY_DESCRIPTOR), ...);
    SecurityDescriptor = ExAllocatePoolWithTag(PagedPool, Length, 'rSeS');
    RtlCopyMemory(SecurityDescriptor, UserSD, Length);
    /* 验证 SD 合法性 */
    RtlValidSecurityDescriptor(SecurityDescriptor);
} else {
    SecurityDescriptor = NULL;
}

如果 OBJ_INHERIT 标志被设置,且 SecurityRequired 为 TRUE(由 OBJECT_TYPE_INITIALIZER 决定),对象管理器会在 ObInsertObject 中从父目录继承 SD:

c 复制代码
if (SecurityRequired && !SecurityDescriptor) {
    /* 从父目录继承 */
    Status = ObpInheritSecurityDescriptor(
        ObjectHeader,
        ParentDirectory,
        AccessState);
}

ObpInheritSecurityDescriptor 调用 SeAssignSecurityse/srm.c(file:///d:/reactos/ntoskrnl/se/srm.c))来生成新的 SD,包含继承的 ACE 和显式 ACE。

4.4.5.3 关键安全检查

ObpCaptureObjectCreateInformation 还会检查:

  • ACL 合法性 :使用 RtlValidAcl 验证
  • SID 合法性 :使用 RtlValidSid 验证
  • SD 自洽性 :使用 RtlValidSecurityDescriptor 验证
  • Owner/Group 存在:自相关 SD 必须有 Owner 和 Group SID

任何一项验证失败都会导致 STATUS_INVALID_SECURITY_DESCR 返回,整个对象创建流程被回滚。


4.4.6 名称插入与句柄分配

4.4.6.1 名称插入的时机

ObInsertObject 中的名称插入是分阶段的:

复制代码
1. 解析 RootDirectory(如果有):
   - 如果 RootDirectory 是句柄,先调用 ObReferenceObjectByHandle
   - 转换为 POBJECT_HEADER 形式

2. 调用 ObpLookupObjectName(详见 4.5.4 节):
   - 匿名对象:直接跳过,FoundObject = Object
   - 命名对象:
     a. 逐段解析路径
     b. 检查同层是否已有同名对象
     c. 如果有 OBJ_OPENIF:返回现有对象
     d. 如果没有:插入新对象

3. 插入到 OBJECT_DIRECTORY:
   - ObpInsertEntryDirectory(Directory, ObjectHeader, Name)
   - 详见 4.1.5 节

4.4.6.2 句柄分配的时机

句柄分配发生在名称插入之后:

c 复制代码
if (InsertObject) {
    /* 命名对象:ObInsertObject 负责分配句柄 */
    Status = ObpCreateHandle(Process,
                             InsertObject,
                             AccessState,
                             ObjectType,
                             PreviousMode,
                             ObjectPointerBias,
                             &Handle);
} else {
    /* 内部对象:只返回对象指针,不分配句柄 */
    Handle = NULL;
}

ObpCreateHandleobhandle.c(file:///d:/reactos/ntoskrnl/ob/obhandle.c))的实现详见 4.3.4 节。

4.4.6.3 关键设计决策

为什么先插入名称再分配句柄?

顺序设计为「先名称、后句柄」的原因:

  1. 避免句柄泄漏:如果名称插入失败(重名、权限不足),不需要回滚句柄
  2. 简化错误处理:句柄分配失败时,可以安全地从命名空间中移除对象
  3. 原子性:要么两个都成功,要么两个都失败

为什么对象创建分三步?

复制代码
ObCreateObject      → 分配
<调用方初始化>       → 初始化
ObInsertObject      → 插入 + 句柄

这种"三步走"模式的精妙之处:

  • 第一步失败:只需释放池内存,没有其他资源可清理
  • 第二步失败:调用方自己负责清理(少见,因为是类型专属代码)
  • 第三步失败:需要回滚配额、清理 NameInfo 等

4.4.7 三种典型创建 API 对比

4.4.7.1 NtCreateEvent:最纯粹的同步对象创建

NtCreateEvent 是最简单的对象创建路径之一,因为它:

  • 同步对象,不需要异步通知
  • 默认状态由事件类型决定
  • 通常为匿名对象(除非使用命名事件)
c 复制代码
NTSTATUS
NTAPI
NtCreateEvent(OUT PHANDLE EventHandle,
              IN ACCESS_MASK DesiredAccess,
              IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
              IN EVENT_TYPE EventType,
              IN BOOLEAN InitialState)
{
    PKEVENT Event;
    NTSTATUS Status;

    DPRINT("NtCreateEvent(EventHandle %p, DesiredAccess %x, "
           "ObjectAttributes %p, EventType %x, InitialState %d)\n",
           EventHandle, DesiredAccess, ObjectAttributes, EventType, InitialState);

    /* 1. 分配对象(Header + KEVENT) */
    Status = ObCreateObject(ExEventObjectType,
                            NULL, /* 不通过 IoAllocate* 等 */
                            sizeof(KEVENT),
                            ObjectAttributes,
                            PsGetCurrentProcess(),
                            /* ... */,
                            (PVOID*)&Event);
    if (!NT_SUCCESS(Status)) return Status;

    /* 2. 初始化对象体 */
    KeInitializeEvent(Event, EventType, InitialState);

    /* 3. 插入命名空间 + 分配句柄 */
    Status = ObInsertObject(Event,
                            /* AccessState */,
                            DesiredAccess,
                            0,
                            NULL,
                            EventHandle);
    if (!NT_SUCCESS(Status)) {
        /* 失败回滚:KEVENT 内存由 ObInsertObject 失败时自动释放 */
        return Status;
    }

    return STATUS_SUCCESS;
}

Event 路径的特点

  • 类型无关的"壳"ObCreateObject 不知道 KEVENT 是什么------它只知道要分配 sizeof(KEVENT) 字节
  • 类型相关的"肉"KeInitializeEvent 初始化 Header.TypeHeader.Size、信号状态等
  • 极简的初始化 :因为 KEVENT 的字段很少(主要是 DISPATCHER_HEADER),初始化非常快

4.4.7.2 NtCreateFile:最复杂的对象创建

NtCreateFile 是最复杂的对象创建路径之一,因为文件对象涉及:

  • 大量与 I/O 管理器、文件系统驱动的交互
  • 完整的 IRP 流程
  • 设备对象的 ParseProcedure
  • 符号链接解析(C:\Device\HarddiskVolume1
c 复制代码
NTSTATUS
NTAPI
NtCreateFile(OUT PHANDLE FileHandle,
             IN ACCESS_MASK DesiredAccess,
             IN POBJECT_ATTRIBUTES ObjectAttributes,
             OUT PIO_STATUS_BLOCK IoStatusBlock,
             IN PLARGE_INTEGER AllocationSize OPTIONAL,
             IN ULONG FileAttributes,
             IN ULONG ShareAccess,
             IN ULONG CreateDisposition,
             IN ULONG CreateOptions,
             IN PVOID EaBuffer OPTIONAL,
             IN ULONG EaLength)
{
    /* 1. 复杂的参数验证 */
    if (CreateDisposition > FILE_MAXIMUM_DISPOSITION) return STATUS_INVALID_PARAMETER;
    /* ... 20+ 个参数检查 ... */

    /* 2. 通过 I/O 管理器创建文件(间接通过 ObCreateObject) */
    Status = IoCreateFile(/* ... 大量参数 ... */,
                          FileHandle);
    return Status;
}

IoCreateFile 内部会:

  • 调用 ObOpenObjectByName(详见 4.5.5 节)解析路径
  • 与文件系统驱动的 ParseProcedure 交互
  • 触发 IRP_MJ_CREATE 完整流程
  • 调用 ObInsertObject 插入文件对象

File 路径的特点

  • 多层间接:通过 I/O 管理器间接调用对象管理器
  • 子系统特定逻辑:文件系统驱动决定如何创建文件
  • 异步操作:文件创建可能是异步的,需要等待 I/O 完成

4.4.7.3 NtCreateProcess:最庞大的对象创建

NtCreateProcess 是最庞大的对象创建路径,因为进程对象涉及:

  • 地址空间创建(ASLR、页表初始化)
  • 安全令牌分配
  • 句柄表创建
  • PEB / TEB 分配
  • 大量子系统通知(PS、MM、SE、IO)
c 复制代码
NTSTATUS
NTAPI
NtCreateProcess(OUT PHANDLE ProcessHandle,
                IN ACCESS_MASK DesiredAccess,
                IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
                IN HANDLE ParentProcess,
                IN BOOLEAN InheritObjectTable,
                IN HANDLE SectionHandle OPTIONAL,
                IN HANDLE DebugPort OPTIONAL,
                IN HANDLE ExceptionPort OPTIONAL)
{
    PEPROCESS Process;
    NTSTATUS Status;

    /* 1. 验证参数 */
    if (ParentProcess) {
        Status = ObReferenceObjectByHandle(ParentProcess, PROCESS_CREATE_PROCESS,
                                            PsProcessType, &Parent);
        if (!NT_SUCCESS(Status)) return Status;
    } else {
        Parent = PsGetCurrentProcess();
    }

    /* 2. 创建进程对象(巨型分配) */
    Status = PspCreateProcess(Parent,
                              InheritObjectTable,
                              /* ... */,
                              &Process);
    if (!NT_SUCCESS(Status)) goto cleanup;

    /* 3. 插入命名空间 + 分配句柄 */
    Status = ObInsertObject(Process,
                            /* AccessState */,
                            DesiredAccess,
                            0,
                            NULL,
                            ProcessHandle);

cleanup:
    if (Parent) ObDereferenceObject(Parent);
    return Status;
}

Process 路径的特点

  • 巨型对象EPROCESS 包含 100+ 字段,sizeof 远大于 KEVENT
  • 多子系统协作:PspCreateProcess 会调用 MM、SE、IO 等子系统的初始化函数
  • 复杂的配额管理:地址空间、句柄表、安全令牌都各自有配额
  • 强安全检查:Token 分配、权限继承、审计日志

4.4.7.4 三种 API 的对比

维度 NtCreateEvent NtCreateFile NtCreateProcess
对象体大小 数十字节 数百字节 数千字节
子系统依赖 KE(仅内核) IO + FS 驱动 PS + MM + SE + IO
初始化复杂度 低(1 个函数) 高(多层 IRP) 极高(多子系统)
错误回滚 简单 复杂 非常复杂
创建耗时 微秒级 毫秒级 毫秒级到秒级
配额影响
用户态权限要求 高(SeCreateTokenPrivilege)

共性

  1. 都遵循三步走模式ObCreateObject → 类型专属初始化 → ObInsertObject
  2. 都进行配额扣除 :通过 ObpChargeQuotaForObject
  3. 都返回 HANDLE :通过 ObInsertObjectHandle 参数
  4. 都进行安全检查:参数验证 + 权限检查 + 配额检查

差异

  1. 对象体大小差异巨大
  2. 子系统依赖复杂度不同
  3. 错误回滚难度不同
  4. 创建耗时差异巨大

4.4.8 概念解释

4.4.8.1 OBJECT_ATTRIBUTES

全称OBJECT_ATTRIBUTES

定义:用户态 → 内核态传递对象创建参数的标准结构。

关键字段

  • Length:结构大小,用于版本控制
  • RootDirectory:相对路径的根目录句柄
  • ObjectName:对象名称(用户态 UNICODE_STRING)
  • Attributes:标志位(OBJ_INHERIT、OBJ_PERMANENT 等)
  • SecurityDescriptor:安全描述符(可选)
  • SecurityQualityOfService:QoS(可选)

生命周期:仅在系统调用期间有效;调用结束后被捕获到内核池

4.4.8.2 OBJECT_CREATE_INFORMATION

全称OBJECT_CREATE_INFORMATION

定义OBJECT_ATTRIBUTES 捕获到内核空间后的内部表示。

关键字段 :与 OBJECT_ATTRIBUTES 类似,但所有指针都已指向内核空间。

生命周期 :在对象创建过程中持续有效,由 ObpReleaseObjectCreateInformation 释放

4.4.8.3 ProbeForRead/ProbeForWrite

全称ProbeForRead / ProbeForWrite

定义:Windows 内核中探测用户态指针可访问性的函数。

作用

  • 验证用户态指针是否合法(地址在用户空间内)
  • 验证用户态指针指向的内存是否可读/可写
  • 防止未捕获的用户态指针在内核中直接解引用

使用模式:先 Probe,再复制(两步走)

4.4.8.4 Pool Quota

全称Pool Quota(池配额)

定义:每个进程在创建对象时可使用的池内存配额。

组成

  • PagedPool quota:分页池配额
  • NonPagedPool quota:非分页池配额

作用

  • 防止单进程耗尽系统池内存
  • 公平分配资源
  • 进程退出时自动清理

4.4.8.5 Security Descriptor

全称SECURITY_DESCRIPTOR(安全描述符)

定义:描述对象安全属性的数据结构。

组成

  • Owner SID:对象所有者
  • Group SID:对象所属组
  • DACL:自主访问控制列表
  • SACL:系统访问控制列表

作用

  • 控制哪些用户/组可以访问对象
  • 控制对象操作的审计

4.4.8.6 Handle Bias

全称Handle Bias(句柄偏移)

定义ObInsertObjectObjectPointerBias 参数。

作用:让新句柄指向已存在的对象指针的指定偏移处。常用于:

  • 让新句柄指向 EPROCESS 中嵌入的子对象
  • 避免重复分配内存

典型用法

  • PspCreateProcess 创建进程时,让 Process->Peb 成为新句柄的目标
  • 这样 NtCurrentProcess() 的伪句柄实现可以使用

4.4.9 为什么要这样设计

4.4.9.1 问题 1:为什么需要"捕获"用户态参数?

内核不能直接使用用户态传入的指针。原因是:

  1. 地址空间隔离:内核态不能直接解引用用户态指针(即使技术上可以映射)
  2. 时间窗口问题:用户态可能在任何时候修改内存(TOCTOU 攻击)
  3. 页面换出问题:用户态页面可能被换出到磁盘,访问会触发页错误

解决方案

  • 第一步ProbeForRead/ProbeForWrite 探测指针合法性
  • 第二步RtlCopyMemory/copy_from_user 复制到内核池
  • 结果:内核有独立的、稳定的、安全的内存来存放这些参数

4.4.9.2 问题 2:为什么配额检查在 ObCreateObject 之后立即进行?

如果配额检查在 ObCreateObject 之前进行,可能出现:

  • 配额足够,但 ObCreateObject 失败(命名冲突等)
  • 已经检查过配额但没有分配任何东西

如果配额检查在 ObInsertObject 之后进行,可能出现:

  • 已经插入了命名空间
  • 失败时需要回滚命名空间

解决方案 :在 ObCreateObject 之后、ObInsertObject 之前进行配额检查:

  • 配额失败时只需释放对象内存
  • 配额成功后才进行命名空间插入
  • 任何后续失败都可干净地回滚

4.4.9.3 问题 3:为什么"插入名称"和"分配句柄"要在同一个函数中?

ObInsertObject 同时处理名称插入和句柄分配,原因:

  1. 原子性:用户态期望"创建对象"是原子的------要么成功并获得句柄,要么失败且无副作用
  2. 回滚简化:如果句柄分配失败,可以干净地从命名空间移除对象
  3. 错误处理统一:单一函数处理所有可能的失败场景

如果不合并:

  • ObInsertObject 只插入名称,返回对象指针
  • ObCreateHandle 单独分配句柄
  • 失败时需要调用方手动回滚
  • 错误处理更复杂

4.4.9.4 问题 4:为什么 ObCreateObject 不初始化 Body?

ObCreateObject 只初始化 Header + Info 块,初始化 Body 内部。原因是:

  1. 类型无关性:对象管理器不关心 Body 是 KEVENT 还是 EPROCESS
  2. 关注点分离:Body 的初始化是"类型专属"逻辑
  3. 灵活性 :调用方可以在 ObCreateObject 之后、插入命名空间之前做额外的初始化

反例

  • 如果 ObCreateObject 初始化 Body,就需要在对象管理器中硬编码每种类型的初始化逻辑
  • 这会导致巨大的 switch-case 语句
  • 每次新增类型都需要修改对象管理器

4.4.9.5 问题 5:为什么 NtCreateEvent 几乎是最短的 API,而 NtCreateProcess 如此复杂?

长度差异反映了:

  • 对象本身的大小:KEVENT 数十字节 vs EPROCESS 数千字节
  • 涉及的子系统数量:KE 仅内核 vs PS+MM+SE+IO
  • 子系统之间的依赖关系:同步对象无依赖 vs 进程对象的强依赖
  • 配置参数的数量:KEVENT 4 个 vs NtCreateProcess 8 个
  • 安全检查的严格程度:低 vs 极高

这种差异体现了"接口的复杂度应反映底层的复杂度"的 API 设计原则。


4.4.10 增强子节:创建失败的回滚机制

4.4.10.1 设计意图

核心问题:对象创建是一个多阶段的过程------分配、初始化、配额、名称、句柄。任何一步失败都需要回滚已完成的操作,否则会留下"半成品"(内存泄漏、命名空间污染、配额透支)。

设计哲学 :「RAII 风格的手动资源管理」------每个阶段在分配资源时同时记录回滚点,失败时按"从后往前"顺序回滚。

4.4.10.2 概念解释

  • 回滚点(Rollback Point):在每个可能失败的步骤前记录状态,用于失败时还原
  • 两阶段提交(Two-Phase Commit):先预分配,失败回滚;成功后正式提交
  • 资源栈(Resource Stack):一个对象创建过程中分配的所有资源形成的栈
  • 部分提交(Partial Commit):在多阶段过程中,每个阶段独立提交

4.4.10.3 失败路径分析

ObInsertObject 的失败回滚路径如下:

c 复制代码
NTSTATUS ObInsertObject(...)
{
    NTSTATUS Status;
    BOOLEAN Inserted = FALSE;
    BOOLEAN Charged = FALSE;
    BOOLEAN Created = FALSE;

    /* 阶段 1: 配额扣除 */
    Status = ObpChargeQuotaForObject(...);
    if (!NT_SUCCESS(Status)) goto cleanup;
    Charged = TRUE;

    /* 阶段 2: 名称查找/插入 */
    Status = ObpLookupObjectName(..., InsertObject, ...);
    if (!NT_SUCCESS(Status)) goto cleanup;
    Inserted = TRUE;

    /* 阶段 3: 句柄分配 */
    Status = ObpCreateHandle(...);
    if (!NT_SUCCESS(Status)) goto cleanup;
    Created = TRUE;

cleanup:
    if (!Created && Inserted) {
        /* 阶段 2 成功但阶段 3 失败:从命名空间移除 */
        ObpRemoveObject(InsertObject);
    }
    if (!Inserted && Charged) {
        /* 阶段 1 成功但阶段 2 失败:回滚配额 */
        ObpUndoChargeQuotaForObject(...);
    }
    if (Status != STATUS_SUCCESS) {
        /* 完全失败:释放对象 */
        ObDereferenceObject(Object);  /* 引用计数归零触发对象释放 */
    }
    return Status;
}

4.4.10.4 关键设计原则

  1. 资源反向释放:从后往前释放资源(栈式)
  2. 条件式回滚:只回滚"已成功"步骤的资源
  3. 引用计数兜底:即使回滚逻辑有 bug,引用计数归零也会清理

4.4.10.5 为什么要这样设计

问题 :为什么不让 ObInsertObject 在失败时自动重试?

答案:失败原因多种多样:

  • 配额不足:重试可能持续失败
  • 名称冲突:重试也解决不了
  • 句柄耗尽:可能需要等待

重试不仅无益,还可能引发:

  • 无限循环
  • 资源持续占用
  • 死锁

结论:失败就是失败,立即回滚,让调用方决定是否重试


4.4.11 增强子节:审计与监控

4.4.11.1 设计意图

核心问题:对象创建可能涉及安全敏感操作(如创建进程、修改注册表)。系统需要:

  1. 审计日志:记录谁创建了什么对象(用于安全审计)
  2. 性能监控:统计对象创建的数量、耗时(用于性能分析)
  3. 资源追踪:追踪对象的所有权和生命周期(用于调试)

设计哲学 :使用「观察者模式」------核心创建流程不感知监控的存在,监控系统通过回调函数挂钩到关键步骤。

4.4.11.2 概念解释

  • OB_CALLBACK(对象管理器回调):在对象创建/删除/命名时触发的回调
  • ObRegisterCallbacks:注册对象管理器回调的 API
  • ETW(Event Tracing for Windows):Windows 事件追踪系统
  • PspNotifyProcessCreate:进程创建通知

4.4.11.3 OB_CALLBACK 机制

Windows Vista 之后引入了 ObRegisterCallbacks,允许驱动注册回调:

c 复制代码
OB_PREOP_CALLBACK_STATUS PreOperationCallback(
    PVOID RegistrationContext,
    POB_PRE_OPERATION_INFORMATION OperationInformation)
{
    /* 在对象创建/打开/复制之前触发 */
    /* 可用于:防病毒扫描、权限检查、行为监控 */
    if (/* 检测到可疑行为 */) {
        return OB_PREOP_DISALLOW;  /* 阻止操作 */
    }
    return OB_PREOP_SUCCESS;  /* 允许操作 */
}

NTSTATUS RegisterCallback()
{
    OB_CALLBACK_REGISTRATION Registration = {0};
    Registration.Altitude = 100;  /* 加载顺序 */
    Registration.Version = OB_FLT_REGISTRATION_VERSION;
    Registration.OperationRegistration[0].ObjectType = PsProcessType;
    Registration.OperationRegistration[0].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
    Registration.OperationRegistration[0].PreOperation = PreOperationCallback;
    Registration.OperationRegistration[0].PostOperation = PostOperationCallback;

    return ObRegisterCallbacks(&Registration, &RegistrationHandle);
}

4.4.11.4 ETW 追踪

ETW 提供 ObCreate* 系列事件用于追踪对象创建:

复制代码
Event ID 1: ObCreateHandle
Event ID 2: ObDuplicateHandle
Event ID 3: ObInsertObject
Event ID 4: ObpLookupObjectName

ETW 的优势:

  • 低开销:在内核中作为 fast path 实现
  • 完整的事件流:从用户态 API 到内核态的完整调用链
  • 可过滤:只记录感兴趣的事件

4.4.11.5 进程创建通知

PspNotifyProcessCreate 通知所有已注册的进程创建回调:

c 复制代码
VOID PspNotifyProcessCreate(PEPROCESS Process, PEPROCESS Parent)
{
    /* 通知所有 PsSetCreateProcessNotifyRoutine 注册的回调 */
    for (Callback in CallbackList) {
        Callback(Process, Parent, CREATE);
    }

    /* 通知对象管理器回调 */
    ObNotifyCreateObject(Process, PsProcessType);
}

典型的进程创建通知回调:

  • 防病毒软件:扫描新进程
  • 性能监控:记录进程创建时间
  • 调试器:附加到新进程

4.4.11.6 为什么要这样设计

问题:为什么把审计和监控做成可插拔的回调而不是硬编码?

答案

  1. 避免膨胀:对象管理器核心代码不包含所有可能的审计逻辑
  2. 灵活性:不同的安全产品可以注册自己的回调
  3. 性能:不需要审计的进程可以禁用回调
  4. 可扩展性:新需求可以通过添加回调实现

问题:ETW 和 OB_CALLBACK 有什么区别?

答案

  • ETW:观察者,记录事件,不影响流程
  • OB_CALLBACK:参与者,可阻止或修改操作

两者互补:ETW 用于事后分析,OB_CALLBACK 用于实时拦截。


4.4.12 小结(增强版)

4.4.12.1 关键知识点回顾

主题 关键点 源码位置
对象分配 ObCreateObject 一次性分配 Header + Info 块 + Body oblife.c:1037-1132(file:///d:/reactos/ntoskrnl/ob/oblife.c#L1037-L1132)
参数捕获 ObpProbeAndCaptureObjectAttributes 探测并复制用户态结构 oblife.c(file:///d:/reactos/ntoskrnl/ob/oblife.c)
配额扣除 ObpChargeQuotaForObject 从进程配额中扣除 obhandle.c:429-484(file:///d:/reactos/ntoskrnl/ob/obhandle.c#L429-L484)
名称插入 ObpLookupObjectName 解析路径并插入 obname.c:446(file:///d:/reactos/ntoskrnl/ob/obname.c#L446)
句柄分配 ObpCreateHandle 在句柄表中分配 Entry obhandle.c(file:///d:/reactos/ntoskrnl/ob/obhandle.c)
失败回滚 阶段式回滚:对象 → 配额 → 句柄 obhandle.c:2935-3070(file:///d:/reactos/ntoskrnl/ob/obhandle.c#L2935-L3070)

4.4.12.2 设计原则总结

  1. 三步走模式:分配 → 初始化 → 插入(清晰、可组合)
  2. 类型无关核心 + 类型专属初始化:对象管理器只管"壳",子系统管"肉"
  3. 两阶段提交:配额预扣除,失败时回滚
  4. 资源反向释放:从后往前释放资源
  5. 安全分层:Probe → 捕获 → 验证 → 使用

4.4.12.3 常见陷阱与注意事项

  1. 忘记调用 ObInsertObject :只调用 ObCreateObject 而不插入,导致对象内存泄漏
  2. 不处理配额失败 :调用 ObInsertObject 后不检查返回值,可能导致后续操作失败
  3. 类型初始化错误 :在 ObCreateObject 之后忘记调用类型专属初始化函数
  4. 安全描述符泄漏OBJECT_ATTRIBUTES 中的 SecurityDescriptor 如果被分配了,需要在失败时释放
  5. NameInfo 泄漏 :命名对象创建失败时,OBJECT_HEADER_NAME_INFO 需要被清理

4.4.12.4 后续学习路径

  • 4.5.4 节:ObpLookupObjectName 详解(名称解析的完整流程)
  • 4.5.5 节:ObOpenObjectByName 详解(打开已存在对象)
  • 第 5 章:进程与线程管理(实际应用对象创建的最复杂场景)
  • 第 6 章:I/O 管理器(实际应用对象创建的次复杂场景)

4.4.12.5 调试技巧

  • !obtrace 调试器命令:追踪对象创建
  • !handle 调试器命令:查看进程的句柄表
  • !object 调试器命令:查看对象类型和统计
  • dbgprintObInsertObject 中加 printk 调试

4.4.12.6 本节回顾

本节是 4.1~4.3 节的"实战篇"------把抽象的对象管理概念转化为具体的代码流程。读者应当能够:

  • 追踪任意 NtCreateXxx 调用的完整代码路径
  • 理解每一步的作用和安全检查
  • 诊断对象创建失败的原因
  • 编写正确的对象创建代码

至此,第 4 章对象管理的核心内容已完成。后续章节将基于本章的概念,深入探讨对象管理器在具体子系统中的应用。

相关推荐
带娃的IT创业者1 小时前
深度解析:从 GitHub 热门项目看 SEO 自动化的技术架构演进
架构·自动化·github·seo·技术架构·反爬虫
星辰_mya1 小时前
CountDownLatch深度解析
java·开发语言·后端·架构
callJJ2 小时前
Volta + Claude Code 在 Windows 上的路径 Bug 复盘
windows·bug
黑暗森林观察者2 小时前
2026数据仓库可观测性实战:用数据血缘+AI智能诊断,把故障定位从2小时压到5分钟
架构
代码小库2 小时前
【2026前端转 AI 全栈指南】第 1 章:前言 · 后端架构 · 章节导览
前端·人工智能·架构
薛定猫AI2 小时前
【深度解析】OpenRouter Fusion API 技术拆解:多模型融合架构的能力边界与工程实践
网络·架构
女神下凡2 小时前
这是 Cursor(Composer) 的五种核心交互模式
服务器·人工智能·windows·vscode·microsoft
techdashen2 小时前
从 Windows 的 ping.exe 入手:动态库、调用约定与 Rust FFI
开发语言·windows·rust
极客老王说Agent2 小时前
自动化架构演进:2026年有比RPA更加稳定的技术吗?
人工智能·ai·chatgpt·架构·自动化·rpa
独隅2 小时前
IntelliJ IDEA 在 Windows 上的完整安装与使用指南
java·windows·intellij-idea