第 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 包装层、系统调用层、参数捕获层、对象分配层、配额扣除层、名称查找层、句柄分配层。每一层都有自己的职责和安全检查。
这个过程的核心难点在于:
- 多阶段错误处理:任何一步失败都可能导致内存泄漏、命名空间污染、配额耗尽等问题
- 安全边界保护 :用户态传入的
UNICODE_STRING、SECURITY_DESCRIPTOR必须在受控的"捕获"阶段被复制到内核空间 - 资源一致性:配额、对象、句柄三者的生命周期必须精确协调
设计哲学
对象创建采用「三步走」的清晰模式:
- 第一步:分配 (
ObCreateObject)------ 只负责"分配壳"(Header + Info 块 + Body),不关心类型语义 - 第二步:初始化 (调用方代码)------ 由类型专属的初始化函数(
KeInitializeEvent等)填充 Body - 第三步:插入与句柄 (
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 内核态的捕获流程
ObpProbeAndCaptureObjectAttributes(oblife.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 的安全意义
ProbeForRead 和 ProbeForWrite 是 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_INFORMATION 是 OBJECT_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_ATTRIBUTES 和 OBJECT_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 为什么需要配额?
ObpChargeQuotaForObject(obhandle.c:429-484(file:///d:/reactos/ntoskrnl/ob/obhandle.c#L429-L484))负责在对象创建时从当前进程扣除池配额。这是为了:
- 防止资源耗尽:单进程无法通过创建大量小对象耗尽系统池
- 公平分配:每个进程按配额获得资源份额
- 进程退出时自动清理:进程对象被销毁时自动释放其配额
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 设计原则:两阶段提交
配额扣除采用"两阶段提交"模式:
- 预扣除 (
ObpChargeQuotaForObject):在ObCreateObject之后立即扣除 - 提交/回滚 (
ObInsertObject成功时提交,失败时回滚)
这种模式的好处:
- 失败时简单回滚:如果对象创建失败,只需"撤销"已扣除的配额
- 成功后无需特殊处理:成功路径上配额自动归新对象所有
- 避免重复计算:配额只需计算一次(在预扣除时)
4.4.5 安全描述符捕获
4.4.5.1 SECURITY_DESCRIPTOR 的双重含义
SECURITY_DESCRIPTOR 在对象创建路径上有两种角色:
- 新对象的安全描述符 (用户在
OBJECT_ATTRIBUTES中显式提供) - 父目录的默认安全描述符(用于继承)
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 调用 SeAssignSecurity(se/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;
}
ObpCreateHandle(obhandle.c(file:///d:/reactos/ntoskrnl/ob/obhandle.c))的实现详见 4.3.4 节。
4.4.6.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.Type、Header.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) |
共性:
- 都遵循三步走模式 :
ObCreateObject→ 类型专属初始化 →ObInsertObject - 都进行配额扣除 :通过
ObpChargeQuotaForObject - 都返回 HANDLE :通过
ObInsertObject的Handle参数 - 都进行安全检查:参数验证 + 权限检查 + 配额检查
差异:
- 对象体大小差异巨大
- 子系统依赖复杂度不同
- 错误回滚难度不同
- 创建耗时差异巨大
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(句柄偏移)
定义 :ObInsertObject 的 ObjectPointerBias 参数。
作用:让新句柄指向已存在的对象指针的指定偏移处。常用于:
- 让新句柄指向
EPROCESS中嵌入的子对象 - 避免重复分配内存
典型用法:
PspCreateProcess创建进程时,让Process->Peb成为新句柄的目标- 这样
NtCurrentProcess()的伪句柄实现可以使用
4.4.9 为什么要这样设计
4.4.9.1 问题 1:为什么需要"捕获"用户态参数?
内核不能直接使用用户态传入的指针。原因是:
- 地址空间隔离:内核态不能直接解引用用户态指针(即使技术上可以映射)
- 时间窗口问题:用户态可能在任何时候修改内存(TOCTOU 攻击)
- 页面换出问题:用户态页面可能被换出到磁盘,访问会触发页错误
解决方案:
- 第一步 :
ProbeForRead/ProbeForWrite探测指针合法性 - 第二步 :
RtlCopyMemory/copy_from_user复制到内核池 - 结果:内核有独立的、稳定的、安全的内存来存放这些参数
4.4.9.2 问题 2:为什么配额检查在 ObCreateObject 之后立即进行?
如果配额检查在 ObCreateObject 之前进行,可能出现:
- 配额足够,但
ObCreateObject失败(命名冲突等) - 已经检查过配额但没有分配任何东西
如果配额检查在 ObInsertObject 之后进行,可能出现:
- 已经插入了命名空间
- 失败时需要回滚命名空间
解决方案 :在 ObCreateObject 之后、ObInsertObject 之前进行配额检查:
- 配额失败时只需释放对象内存
- 配额成功后才进行命名空间插入
- 任何后续失败都可干净地回滚
4.4.9.3 问题 3:为什么"插入名称"和"分配句柄"要在同一个函数中?
ObInsertObject 同时处理名称插入和句柄分配,原因:
- 原子性:用户态期望"创建对象"是原子的------要么成功并获得句柄,要么失败且无副作用
- 回滚简化:如果句柄分配失败,可以干净地从命名空间移除对象
- 错误处理统一:单一函数处理所有可能的失败场景
如果不合并:
ObInsertObject只插入名称,返回对象指针ObCreateHandle单独分配句柄- 失败时需要调用方手动回滚
- 错误处理更复杂
4.4.9.4 问题 4:为什么 ObCreateObject 不初始化 Body?
ObCreateObject 只初始化 Header + Info 块,不初始化 Body 内部。原因是:
- 类型无关性:对象管理器不关心 Body 是 KEVENT 还是 EPROCESS
- 关注点分离:Body 的初始化是"类型专属"逻辑
- 灵活性 :调用方可以在
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 关键设计原则
- 资源反向释放:从后往前释放资源(栈式)
- 条件式回滚:只回滚"已成功"步骤的资源
- 引用计数兜底:即使回滚逻辑有 bug,引用计数归零也会清理
4.4.10.5 为什么要这样设计
问题 :为什么不让 ObInsertObject 在失败时自动重试?
答案:失败原因多种多样:
- 配额不足:重试可能持续失败
- 名称冲突:重试也解决不了
- 句柄耗尽:可能需要等待
重试不仅无益,还可能引发:
- 无限循环
- 资源持续占用
- 死锁
结论:失败就是失败,立即回滚,让调用方决定是否重试
4.4.11 增强子节:审计与监控
4.4.11.1 设计意图
核心问题:对象创建可能涉及安全敏感操作(如创建进程、修改注册表)。系统需要:
- 审计日志:记录谁创建了什么对象(用于安全审计)
- 性能监控:统计对象创建的数量、耗时(用于性能分析)
- 资源追踪:追踪对象的所有权和生命周期(用于调试)
设计哲学 :使用「观察者模式」------核心创建流程不感知监控的存在,监控系统通过回调函数挂钩到关键步骤。
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 为什么要这样设计
问题:为什么把审计和监控做成可插拔的回调而不是硬编码?
答案:
- 避免膨胀:对象管理器核心代码不包含所有可能的审计逻辑
- 灵活性:不同的安全产品可以注册自己的回调
- 性能:不需要审计的进程可以禁用回调
- 可扩展性:新需求可以通过添加回调实现
问题: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 设计原则总结
- 三步走模式:分配 → 初始化 → 插入(清晰、可组合)
- 类型无关核心 + 类型专属初始化:对象管理器只管"壳",子系统管"肉"
- 两阶段提交:配额预扣除,失败时回滚
- 资源反向释放:从后往前释放资源
- 安全分层:Probe → 捕获 → 验证 → 使用
4.4.12.3 常见陷阱与注意事项
- 忘记调用
ObInsertObject:只调用ObCreateObject而不插入,导致对象内存泄漏 - 不处理配额失败 :调用
ObInsertObject后不检查返回值,可能导致后续操作失败 - 类型初始化错误 :在
ObCreateObject之后忘记调用类型专属初始化函数 - 安全描述符泄漏 :
OBJECT_ATTRIBUTES中的SecurityDescriptor如果被分配了,需要在失败时释放 - 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调试器命令:查看对象类型和统计dbgprint在ObInsertObject中加 printk 调试
4.4.12.6 本节回顾
本节是 4.1~4.3 节的"实战篇"------把抽象的对象管理概念转化为具体的代码流程。读者应当能够:
- 追踪任意
NtCreateXxx调用的完整代码路径 - 理解每一步的作用和安全检查
- 诊断对象创建失败的原因
- 编写正确的对象创建代码
至此,第 4 章对象管理的核心内容已完成。后续章节将基于本章的概念,深入探讨对象管理器在具体子系统中的应用。