第 4 章 对象管理 --- 4.5 几个常用的内核函数
本节是第 4 章的"工具箱"------把对象管理器中最常用的 7 个函数逐一拆解。每个函数都是阅读 ReactOS 源码时频繁遇到的关键 API:句柄转换(ObReferenceObjectByHandle / ObReferenceObjectByPointer)、名称查找(ObpLookupEntryDirectory / ObpLookupObjectName)、对象打开(ObOpenObjectByName / ObReferenceObjectByName)、引用释放(ObDereferenceObject)。掌握这些函数是理解整个对象管理器的"钥匙"。
4.5.0 框架图
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 7 个常用内核函数总览 │
└──────────────────────────────────────────────────────────────────────────────────┘
│
┌──────────────────────────────┼──────────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 句柄转换类 │ │ 名称查找类 │ │ 引用管理类 │
│ │ │ │ │ │
│ • ObReference │ │ • ObpLookup │ │ • ObDereference │
│ ObjectByHandle │ │ EntryDirectory │ │ Object │
│ (4.5.1) │ │ (4.5.3) │ │ (4.5.7) │
│ │ │ │ │ │
│ • ObReference │ │ • ObpLookup │ │ │
│ ObjectByPointer│ │ ObjectName │ │ │
│ (4.5.2) │ │ (4.5.4) │ │ │
└──────────────────┘ │ │ └──────────────────┘
│ • ObOpen │
│ ObjectByName │
│ (4.5.5) │
│ │
│ • ObReference │
│ ObjectByName │
│ (4.5.6) │
└──────────────────┘
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 函数关系图 │
│ │
│ ObReferenceObjectByHandle ───▶ 用于:句柄 → 对象指针 + 权限验证 │
│ │ │
│ │ 内部调用 ExMapHandleToPointer │
│ │ ├─ ExpLookupHandleTableEntry │
│ │ └─ ExpLockHandleTableEntry │
│ │ │
│ ▼ │
│ ObpLookupEntryDirectory ───▶ 用于:目录 → 名称查找(Hash 表) │
│ │ │
│ │ 内部:ObpInsertEntryDirectory / ObpDeleteEntryDirectory │
│ │ │
│ ▼ │
│ ObpLookupObjectName ───▶ 用于:完整路径解析(符号链接 + ParseProcedure) │
│ │ │
│ │ 内部调用 ObpLookupEntryDirectory │
│ │ │
│ ▼ │
│ ObOpenObjectByName ───▶ 用于:打开已存在对象 │
│ │ │
│ │ 内部调用 ObpLookupObjectName + ObpCreateHandle │
│ │ │
│ ▼ │
│ ObReferenceObjectByName ───▶ 用于:内核态按名称引用对象(不创建句柄) │
│ │ │
│ │ 内部调用 ObpLookupObjectName + ObReferenceObjectByPointer │
│ │ │
│ ▼ │
│ ObDereferenceObject ───▶ 用于:递减引用计数 │
│ │ │
│ │ 当计数为 0 时调用 ObpDeleteObject + Type->DeleteProcedure │
└──────────────────────────────────────────────────────────────────────────────────┘
4.5.0.1 设计意图
核心问题
对象管理器是 Windows 内核的"基础设施层"------它为所有子系统(PS、IO、MM、SE、CM、EX)提供统一的对象管理服务。在所有这些子系统的代码中,有 7 个函数出现的频率远高于其他函数:
- 句柄转换类 :
ObReferenceObjectByHandle、ObReferenceObjectByPointer------ 用于从"用户态可见的句柄"或"已知对象指针"获取"带引用的对象指针" - 名称查找类 :
ObpLookupEntryDirectory、ObpLookupObjectName------ 用于在命名空间中查找对象 - 对象打开类 :
ObOpenObjectByName、ObReferenceObjectByName------ 用于按名称打开/引用对象 - 引用管理类 :
ObDereferenceObject------ 用于释放引用
设计哲学
这 7 个函数的设计体现了对象管理器的几个核心原则:
- 「Lookup 模式」 :
Lookup*系列函数负责名称解析;Reference*系列函数负责引用计数;Open*系列函数负责创建句柄。三者分工明确。 - 「By-Name vs By-Handle vs By-Pointer」:内核提供三种获取对象的方式,分别对应"按名称查找"、"从用户态句柄获取"、"从内核态指针获取"。每种方式有不同的安全语义。
- 「P-前缀的内部函数」 :
Obp*函数是"对象管理器内部使用"的函数,通常不导出给驱动程序。Ob*函数(无 P)是"导出函数",可以被驱动调用。
本节定位
本节将逐个拆解这 7 个函数,每个函数都从「函数签名 → 核心流程 → 关键设计决策 → 常见使用场景」四个角度展开。读完本节后,读者应当能够:
- 在 ReactOS 源码中快速识别这 7 个函数
- 理解每个函数的安全语义
- 编写正确的对象引用/释放代码
- 诊断常见的对象管理错误(如引用计数泄漏)
4.5.1 ObReferenceObjectByHandle()
4.5.1.1 函数签名与参数解析
c
NTSTATUS
NTAPI
ObReferenceObjectByHandle(IN HANDLE Handle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType OPTIONAL,
IN KPROCESSOR_MODE AccessMode,
OUT PVOID *Object,
OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL);
参数详解:
| 参数 | 含义 |
|---|---|
Handle |
用户态/内核态传入的句柄值 |
DesiredAccess |
调用方请求的访问权限(用于权限验证) |
ObjectType |
期望的对象类型(可选,若指定则进行类型检查) |
AccessMode |
调用方的处理器模式(UserMode / KernelMode) |
Object |
输出:对象体指针 |
HandleInformation |
输出:句柄信息(属性 + 实际权限,可选) |
返回值:
STATUS_SUCCESS:成功STATUS_INVALID_HANDLE:句柄值非法STATUS_ACCESS_DENIED:权限不足STATUS_OBJECT_TYPE_MISMATCH:类型不匹配
源码位置:ntoskrnl/ob/obref.c(file:///d:/reactos/ntoskrnl/ob/obref.c)(约 450 行)
4.5.1.2 句柄定位与锁定
ObReferenceObjectByHandle 的第一步是定位并锁定句柄表项:
c
PHANDLE_TABLE_ENTRY
NTAPI
ExpLookupHandleTableEntry(IN PHANDLE_TABLE HandleTable,
IN HANDLE Handle)
{
ULONG_PTR TableCode = HandleTable->TableCode;
ULONG_PTR LowIndex = (ULONG_PTR)Handle & 0xFFF; // 提取低 12 位
ULONG_PTR MidIndex = ((ULONG_PTR)Handle >> 12) & 0x1FF; // 中 9 位
ULONG_PTR HighIndex = ((ULONG_PTR)Handle >> 21) & 0x1FF; // 高 9 位
// ... 详细查找逻辑
}
句柄值分解(详见 4.3.0 节框架图):
- 低 2 位(TagBits):标志位,0 表示普通句柄
- 低 12 位:在叶子页表中的索引(512 个 Entry)
- 中 9 位:在中间页表中的索引
- 高 9 位:在根页表中的索引
- 最高位(仅 64 位):内核句柄标志
锁定 通过 ExpLockHandleTableEntry(无锁原子操作)实现:
c
VOID ExpLockHandleTableEntry(PHANDLE_TABLE_ENTRY Entry)
{
PVOID OldValue, NewValue;
do {
OldValue = Entry->Object;
NewValue = OldValue | EXHANDLE_TABLE_ENTRY_LOCK_BIT;
} while (InterlockedCompareExchangePointer(&Entry->Object, NewValue, OldValue) != OldValue);
}
4.5.1.3 权限验证流程
权限验证包含三层检查:
c
/* 1. 句柄值合法性 */
if (HandleToLong(Handle) < 0) {
/* 处理伪句柄(-1, -2)*/
if (Handle == NtCurrentProcess()) { /* ... */ }
else if (Handle == NtCurrentThread()) { /* ... */ }
else if (KernelHandle) { /* 内核句柄 */ }
else return STATUS_INVALID_HANDLE;
}
/* 2. Entry 状态验证 */
if (Entry->Object == NULL) {
ExUnlockHandleTableEntry(...);
return STATUS_INVALID_HANDLE;
}
/* 3. 权限验证 */
if (AccessMode != KernelMode) {
if ((Entry->GrantedAccess & DesiredAccess) != DesiredAccess) {
ExUnlockHandleTableEntry(...);
return STATUS_ACCESS_DENIED;
}
}
/* 4. 类型验证(如果指定了 ObjectType)*/
if (ObjectType && ObjectHeader->Type != ObjectType) {
ExUnlockHandleTableEntry(...);
return STATUS_OBJECT_TYPE_MISMATCH;
}
关键设计决策:
- 内核态权限绕过 :
AccessMode == KernelMode时跳过权限检查 - 类型检查可选:调用方可以指定期望类型,让函数自动验证
- 失败时不递增引用计数:所有检查通过后才递增
4.5.1.4 引用计数递增
权限验证通过后,递增对象的引用计数:
c
InterlockedIncrementSizeT(&ObjectHeader->PointerCount);
*Object = &ObjectHeader->Body;
if (HandleInformation) {
HandleInformation->GrantedAccess = Entry->GrantedAccess;
HandleInformation->HandleAttributes = Entry->ObAttributes & OBJ_HANDLE_ATTRIBUTES;
}
ExUnlockHandleTableEntry(...);
return STATUS_SUCCESS;
为什么使用 InterlockedIncrement?
- 多核并发:可能多个 CPU 同时引用同一对象
- 避免竞态 :
InterlockedIncrement是原子操作 - 内存屏障:保证指针和引用计数的可见性
4.5.1.5 错误路径分析
| 错误码 | 触发条件 | 实际影响 |
|---|---|---|
STATUS_INVALID_HANDLE |
句柄值未在 NextHandleNeedingPool 范围内 |
句柄表无此 Entry |
STATUS_ACCESS_DENIED |
GrantedAccess 缺少 DesiredAccess 所需位 |
权限不足 |
STATUS_OBJECT_TYPE_MISMATCH |
ObjectHeader->Type != ObjectType |
类型错误 |
STATUS_KERNEL_HANDLE_NOT_EXPOSED |
用户态试图访问未导出的内核句柄 | 内核句柄隔离 |
STATUS_HANDLE_REVOKED |
句柄已被撤销(极少使用) | 句柄失效 |
关键点:所有错误路径都正确地:
- 释放锁定的 Entry
- 离开临界区
- 不递增引用计数
4.5.1.6 关键设计决策
- 伪句柄的处理 :
NtCurrentProcess()(-1)和NtCurrentThread()(-2)是特殊值,直接返回当前对象指针 - 内核句柄标志 :64 位系统上,最高位置 1 表示内核句柄,仅允许
KernelMode访问 - 类型检查的可选性:让调用方决定是否验证类型------某些场景下需要接受多种类型
- 权限检查的严格性:用户态严格检查,内核态宽松检查
- 引用计数的原子性 :使用
InterlockedIncrement保证并发安全
4.5.1.7 常见使用场景
c
/* 场景 1:从用户态句柄获取进程对象 */
NTSTATUS Status = ObReferenceObjectByHandle(
ProcessHandle, // HANDLE
PROCESS_VM_READ, // 请求的权限
PsProcessType, // 期望类型
UserMode, // 调用方模式
&Process, // 输出对象指针
NULL // 不需要句柄信息
);
if (NT_SUCCESS(Status)) {
/* 使用 Process 访问进程内存 */
KeStackAttachProcess(&Process->Pcb, &ApcState);
/* ... */
KeUnstackDetachProcess(&ApcState);
ObDereferenceObject(Process); // 释放引用
}
c
/* 场景 2:打开事件对象 */
NTSTATUS Status = ObReferenceObjectByHandle(
EventHandle,
EVENT_MODIFY_STATE, // 请求修改权限
ExEventObjectType,
UserMode,
&Event,
NULL
);
if (NT_SUCCESS(Status)) {
KeSetEvent((PKEVENT)Event, 0, FALSE);
ObDereferenceObject(Event);
}
4.5.1.8 注意事项
- 必须配对
ObDereferenceObject:每次成功引用都对应一次释放 - 不要在 DPC/ISR 中调用:会触发页面错误
- 不要在内核 APC 中调用:可能死锁
- 不要递归调用:可能导致引用计数泄漏
4.5.2 ObReferenceObjectByPointer()
4.5.2.1 函数签名与参数解析
c
NTSTATUS
NTAPI
ObReferenceObjectByPointer(IN PVOID Object,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType,
IN KPROCESSOR_MODE AccessMode);
与 ObReferenceObjectByHandle 的区别:
| 维度 | ObReferenceObjectByHandle |
ObReferenceObjectByPointer |
|---|---|---|
| 输入 | HANDLE(用户态可见) | 指针(内核态已知) |
| 查找 | 需查句柄表 | 不需查找 |
| 权限检查 | 检查句柄的 GrantedAccess | 不检查(内核态已知) |
| 类型检查 | 强制 | 强制 |
| 使用场景 | 跨边界(用户态→内核态) | 内核内部 |
源码位置:ntoskrnl/ob/obref.c:100(file:///d:/reactos/ntoskrnl/ob/obref.c#L100)
4.5.2.2 实现细节
c
NTSTATUS
NTAPI
ObReferenceObjectByPointer(IN PVOID Object,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType,
IN KPROCESSOR_MODE AccessMode)
{
POBJECT_HEADER ObjectHeader = OBJECT_TO_OBJECT_HEADER(Object);
/* 类型检查(强类型) */
if (ObjectHeader->Type != ObjectType) {
return STATUS_OBJECT_TYPE_MISMATCH;
}
/* 递增引用计数 */
InterlockedIncrementSizeT(&ObjectHeader->PointerCount);
return STATUS_SUCCESS;
}
注意 :与 ObReferenceObjectByHandle 不同,ObReferenceObjectByPointer:
- 不返回对象指针(因为输入就是对象指针)
- 不进行权限检查(因为内核态已经知道对象)
- 仍然进行类型检查(防止类型错误)
4.5.2.3 使用场景
c
/* 场景 1:在驱动中引用已知对象 */
PEPROCESS Process = IoGetCurrentProcess(); // 已知是当前进程
ObReferenceObjectByPointer(Process, 0, PsProcessType, KernelMode);
/* 使用 Process */
ObDereferenceObject(Process);
c
/* 场景 2:在对象创建路径中引用父对象 */
NTSTATUS PspCreateProcess(PEPROCESS ParentProcess, /* ... */)
{
ObReferenceObjectByPointer(ParentProcess, 0, PsProcessType, KernelMode);
/* ... */
}
4.5.2.4 为什么不需要权限检查?
ObReferenceObjectByPointer 不进行权限检查的原因是:
- 调用方是内核代码:已经被信任
- 指针是已知的:不存在"伪造"的可能
- 简化代码:避免每次都进行无意义的权限检查
但这并不意味着任何对象都可以引用:
- 类型检查仍然强制:防止意外的类型错误
- KPROCESSOR_MODE 参数:在某些特殊情况下可能影响行为
4.5.2.5 注意事项
- 必须保证对象有效 :调用方必须确保
Object指向真实的对象(未释放) - 类型必须精确匹配 :
PsProcessType不能用于PsThreadType - 必须配对
ObDereferenceObject:与ObReferenceObjectByHandle相同 - 不要在持有自旋锁时调用:会触发死锁(引用计数是 PagedPool 中的指针)
4.5.3 ObpLookupEntryDirectory()
4.5.3.1 函数签名与参数解析
ObpLookupEntryDirectory 是 4.5.4 节 ObpLookupObjectName 的核心内部函数,负责在一个具体的 OBJECT_DIRECTORY 中按名称查找或创建条目。
c
NTSTATUS
NTAPI
ObpLookupEntryDirectory(IN POBJECT_DIRECTORY Directory,
IN PUNICODE_STRING Name,
IN ULONG Attributes,
IN BOOLEAN Insert,
IN POBP_LOOKUP_CONTEXT Context,
OUT POBJECT_HEADER *FoundObject);
参数详解:
| 参数 | 含义 |
|---|---|
Directory |
目标对象目录 |
Name |
要查找的名称(UNICODE_STRING) |
Attributes |
标志位(决定大小写敏感性) |
Insert |
是否在未找到时插入新条目 |
Context |
查找上下文(用于跨目录跟踪) |
FoundObject |
输出:找到的对象头 |
源码位置:ntoskrnl/ob/obdir.c:235(file:///d:/reactos/ntoskrnl/ob/obdir.c#L235)
4.5.3.2 Hash 表查找机制
OBJECT_DIRECTORY 内部使用 37 桶 Hash 表(4.1.5 节(file:///d:/reactos/doc/第4章_对象管理_4.1.md#415)):
c
typedef struct _OBJECT_DIRECTORY {
struct _OBJECT_DIRECTORY_ENTRY *HashBuckets[NUMBER_HASH_BUCKETS]; // 37 桶
// ... 其他字段
} OBJECT_DIRECTORY, *POBJECT_DIRECTORY;
Hash 函数(简化版):
c
ULONG ObpHashName(PUNICODE_STRING Name)
{
WCHAR Char;
ULONG Hash = 0;
PWCHAR Buffer = Name->Buffer;
ULONG Length = Name->Length / sizeof(WCHAR);
while (Length--) {
Char = RtlUpcaseUnicodeChar(Buffer[Length]);
Hash = (Hash << 1) | (Hash >> 31); // 循环左移
Hash += Char;
}
return Hash % NUMBER_HASH_BUCKETS;
}
关键设计决策:
- 37 桶是质数:减少 Hash 碰撞
- 循环左移 + 累加:分布更均匀
- 大写归一化:大小写不敏感
4.5.3.3 大小写不敏感比较
Attributes & OBJ_CASE_INSENSITIVE 决定比较方式:
c
BOOLEAN ObpCompareNames(PUNICODE_STRING Name1, PUNICODE_STRING Name2, BOOLEAN CaseInsensitive)
{
if (Name1->Length != Name2->Length) return FALSE;
if (CaseInsensitive) {
return RtlCompareUnicodeString(Name1, Name2, TRUE) == 0;
} else {
return RtlCompareUnicodeString(Name1, Name2, FALSE) == 0;
}
}
大小写不敏感的典型场景:
- 文件对象(
C:\Windows和c:\windows相同) - 设备对象
- 注册表键
大小写敏感的典型场景:
- 进程对象(区分大小写)
- 互斥体
- 事件
4.5.3.4 查找上下文缓存
OBP_LOOKUP_CONTEXT 缓存查找结果以加速「先查后创建」的操作:
c
typedef struct _OBP_LOOKUP_CONTEXT {
POBJECT_DIRECTORY Directory; // 当前目录
POBJECT_DIRECTORY_ENTRY Entry; // 当前条目
ULONG HashIndex; // 当前位置的 Hash 索引
ULONG LockState; // 锁状态
} OBP_LOOKUP_CONTEXT, *POBP_LOOKUP_CONTEXT;
为什么需要缓存?
考虑以下场景:
- 调用
ObOpenObjectByName查找\Device\HarddiskVolume1\foo ObpLookupEntryDirectory在\Device目录中找到HarddiskVolume1- 但
HarddiskVolume1是一个符号链接 - 解析符号链接后,路径变成
\Device\HarddiskVolume1\foo - 现在需要重新从
\开始查找
如果不缓存,每次都会重新查找 \ → Device → HarddiskVolume1。缓存可以让第二次查找"接续"第一次的结果。
4.5.3.5 创建新条目流程
当 Insert = TRUE 且未找到时:
c
if (Insert && *FoundObject == NULL) {
/* 分配 OBJECT_DIRECTORY_ENTRY */
NewEntry = ExAllocatePoolWithTag(PagedPool, sizeof(OBJECT_DIRECTORY_ENTRY), 'EteN');
/* 复制名称(UNICODE_STRING) */
NewEntry->Name.Length = Name->Length;
NewEntry->Name.MaximumLength = Name->Length + sizeof(WCHAR);
NewEntry->Name.Buffer = ExAllocatePoolWithTag(PagedPool, NewEntry->Name.MaximumLength, 'NmeN');
RtlCopyMemory(NewEntry->Name.Buffer, Name->Buffer, Name->Length);
/* 设置对象头(稍后由 ObInsertObject 填充) */
NewEntry->ObjectHeader = NULL; // 占位
/* 插入到 Hash 桶 */
HashIndex = ObpHashName(Name);
NewEntry->ChainLink = Directory->HashBuckets[HashIndex];
Directory->HashBuckets[HashIndex] = NewEntry;
/* 保存到 Context(供后续填充) */
Context->Entry = NewEntry;
Context->HashIndex = HashIndex;
}
关键设计决策:
- 先占位后填充 :Entry 的
ObjectHeader字段在创建时为 NULL,ObInsertObject后续填充 - 名称独立分配:Entry 内部只存指向名称的指针,名称单独分配
- 链头插入:新 Entry 插入到 Hash 桶的头部(O(1) 操作)
4.5.3.6 关键设计决策
- 37 桶 Hash 表:质数减少碰撞
- 链头插入:LIFO 行为,最近创建的 Entry 在最前面
- 查找上下文缓存:避免重复查找
- 插入和查找分离:先查找,必要时再插入
4.5.3.7 注意事项
- 目录锁 :调用方必须持有
Directory的锁 - 名称生命周期:插入时分配的名称缓冲必须长期有效(直到对象删除)
- Entry 分配失败 :
ExAllocatePoolWithTag失败时返回STATUS_INSUFFICIENT_RESOURCES
4.5.4 ObpLookupObjectName()
4.5.4.1 函数签名与参数解析
c
NTSTATUS
NTAPI
ObpLookupObjectName(IN HANDLE RootHandle OPTIONAL,
IN OUT PUNICODE_STRING ObjectName,
IN ULONG Attributes,
IN POBJECT_TYPE ObjectType,
IN KPROCESSOR_MODE AccessMode,
IN OUT PVOID ParseContext,
IN PSECURITY_QUALITY_OF_SERVICE SecurityQos OPTIONAL,
IN PVOID InsertObject OPTIONAL,
IN OUT PACCESS_STATE AccessState,
OUT POBP_LOOKUP_CONTEXT LookupContext,
OUT PVOID *FoundObject);
参数详解:
| 参数 | 含义 |
|---|---|
RootHandle |
根目录句柄(NULL 表示从 \ 根目录开始) |
ObjectName |
完整路径(如 \Device\HarddiskVolume1\foo) |
Attributes |
标志位 |
ObjectType |
期望的对象类型(可选) |
AccessMode |
处理器模式 |
ParseContext |
解析上下文(由 ParseProcedure 设置) |
SecurityQos |
安全 QoS(可选) |
InsertObject |
要插入的新对象(创建时使用) |
AccessState |
访问状态(用于权限累积) |
LookupContext |
查找上下文(用于跨目录跟踪) |
FoundObject |
输出:找到的对象 |
源码位置:ntoskrnl/ob/obname.c:446(file:///d:/reactos/ntoskrnl/ob/obname.c#L446)
4.5.4.2 路径解析流程
完整的路径解析流程如下:
c
/* 初始化 */
ObpInitializeLookupContext(LookupContext);
*FoundObject = NULL;
/* 1. 解析根目录 */
if (RootHandle) {
/* 从句柄获取根目录 */
ObReferenceObjectByHandle(RootHandle, 0, ObpDirectoryObjectType, AccessMode, &Directory, NULL);
} else {
/* 使用全局根目录 */
Directory = ObpRootDirectoryObject;
}
/* 2. 提取第一段路径 */
ComponentName = /* ObjectName 的第一段 */;
RemainingName = /* ObjectName 的剩余部分 */;
/* 3. 循环解析 */
while (TRUE) {
/* 查找当前目录中的 ComponentName */
Status = ObpLookupEntryDirectory(Directory, &ComponentName, Attributes, Insert, Context, &Object);
if (Object) {
if (RemainingName.Length == 0) {
/* 找到目标 */
*FoundObject = Object;
return STATUS_SUCCESS;
}
/* 还有剩余路径,根据对象类型处理 */
if (Object->Type == ObpDirectoryObjectType) {
/* 目录对象:继续向下 */
Directory = (POBJECT_DIRECTORY)Object;
} else if (Object->Type->TypeInfo.ParseProcedure) {
/* 有 ParseProcedure:调用它解析剩余路径 */
Status = Object->Type->TypeInfo.ParseProcedure(
Object,
Object->Type,
AccessState,
AccessMode,
Attributes,
&ObjectName,
&RemainingName,
ParseContext,
SecurityQos,
FoundObject);
if (Status == STATUS_REPARSE) {
/* 符号链接:使用新的路径重试 */
ObjectName = ReparseTarget;
goto loop_start;
}
return Status;
} else {
/* 无 ParseProcedure:路径冲突 */
return STATUS_OBJECT_PATH_INVALID;
}
} else {
/* 未找到:如果是 Insert 模式且 InsertObject 有效,则插入 */
if (Insert && InsertObject) {
Status = ObpInsertEntryDirectory(Directory, InsertObject, &ComponentName, Context);
*FoundObject = InsertObject;
return STATUS_SUCCESS;
}
return STATUS_OBJECT_NAME_NOT_FOUND;
}
/* 提取下一段 */
ComponentName = RemainingName 的下一段;
RemainingName = RemainingName 的剩余部分;
}
4.5.4.3 符号链接重解析
符号链接对象会触发 STATUS_REPARSE:
c
/* 符号链接对象的 ParseProcedure 是 ObpParseSymbolicLink */
NTSTATUS
NTAPI
ObpParseSymbolicLink(IN PVOID SymbolicLinkObject,
/* ... */,
PUNICODE_STRING RemainingName,
/* ... */,
PVOID *FoundObject)
{
PSYMBOLIC_LINK_OBJECT SymLink = (PSYMBOLIC_LINK_OBJECT)SymbolicLinkObject;
UNICODE_STRING NewName;
/* 构建新路径:SymLink->LinkTarget + RemainingName */
NewName.Length = SymLink->LinkTarget.Length + RemainingName->Length;
NewName.Buffer = ExAllocatePoolWithTag(PagedPool, NewName.Length, 'eSmL');
RtlCopyUnicodeString(&NewName, &SymLink->LinkTarget);
RtlAppendUnicodeStringToString(&NewName, RemainingName);
/* 设置重解析标志 */
*FoundObject = NULL;
return STATUS_REPARSE; // 表示路径需要重新解析
}
重解析机制:
MaxReparse = 30:限制重解析次数,防止循环- 每次重解析从
\根目录重新开始 - 维护一个
ReparseBuffer缓存
4.5.4.4 ParseProcedure 调用
每种类型可以定义自己的 ParseProcedure:
| 类型 | ParseProcedure | 作用 |
|---|---|---|
| Directory | 默认 | 继续向下解析 |
| SymbolicLink | ObpParseSymbolicLink |
重定向到链接目标 |
| Device | IopParseDevice |
解析为设备对象 |
| File | IopParseFile |
打开文件对象 |
| Section | MiSectionParseProcedure |
映射内存段 |
典型 ParseProcedure 流程:
c
NTSTATUS IopParseDevice(IN PVOID DeviceObject,
/* ... */,
PUNICODE_STRING RemainingName,
/* ... */,
PVOID *FoundObject)
{
PDEVICE_OBJECT Device = (PDEVICE_OBJECT)DeviceObject;
/* 1. 查找附加到设备的文件系统 */
if (Device->DeviceType == FILE_DEVICE_DISK) {
Vpb = Device->Vpb;
if (Vpb && Vpb->DeviceObject) {
/* 通过 VPB 找到文件系统 */
*FoundObject = Vpb->DeviceObject;
return STATUS_REPARSE; // 让上层的 IopParseFile 继续
}
}
/* 2. 直接打开设备 */
Status = IopOpenDevice(Device, AccessMode, DesiredAccess, ...);
/* 3. 返回文件对象 */
*FoundObject = NewFileObject;
return STATUS_SUCCESS;
}
4.5.4.5 循环解析与重解析限制
c
ULONG MaxReparse = 30; // 最大重解析次数
/* 主循环 */
while (MaxReparse--) {
/* ... 解析路径 ... */
if (Status == STATUS_REPARSE) {
/* 重置路径并重试 */
continue;
}
break;
}
if (MaxReparse == 0) {
return STATUS_OBJECT_NAME_INVALID; // 可能是循环引用
}
循环解析的常见原因:
- 符号链接指向自身(
A → A) - 符号链接形成环(
A → B → A) - 配置错误
为什么限制为 30?
- 性能:如果无限重试,会消耗大量 CPU
- 安全:防止恶意构造的循环引用
- 可观察性:返回错误码让管理员发现配置问题
4.5.4.6 权限检查时机
ObpLookupObjectName 在以下时机进行权限检查:
| 时刻 | 检查内容 | 函数 |
|---|---|---|
| 根目录解析 | 是否有 Traverse 权限 |
ObpCheckObjectAccess |
| 每段路径 | 是否有 Traverse 权限 |
ObpCheckObjectAccess |
| 目标对象 | 是否有完整访问权限 | ObCheckObjectAccess |
为什么需要 Traverse 权限?
考虑路径 \A\B\C:
- 要访问
C,必须先经过A和B - 如果用户对
A或B没有Traverse权限,应该被拒绝 - 这种检查防止用户通过"跳板"访问未授权的对象
4.5.4.7 关键设计决策
- 逐段解析 :与 Unix 的
path_lookup思路一致 - 符号链接重解析:在主循环中处理,而非递归
- ParseProcedure 多态:每种类型自定义解析逻辑
- 重解析限制:30 次上限防止循环
- Traverse 权限:按段检查,防止未授权访问
4.5.4.8 注意事项
- 目录锁顺序:必须按"从根到叶"获取目录锁
- APC 级别:不能在 APC_LEVEL 调用
- 分页:调用期间不能持有自旋锁
4.5.5 ObOpenObjectByName()
4.5.5.1 函数签名与参数解析
c
NTSTATUS
NTAPI
ObOpenObjectByName(IN POBJECT_ATTRIBUTES ObjectAttributes,
IN POBJECT_TYPE ObjectType,
IN KPROCESSOR_MODE AccessMode,
IN ACCESS_MASK DesiredAccess,
IN PACCESS_STATE PassedAccessState OPTIONAL,
OUT PHANDLE Handle);
参数详解:
| 参数 | 含义 |
|---|---|
ObjectAttributes |
对象属性(含路径) |
ObjectType |
期望的对象类型 |
AccessMode |
处理器模式 |
DesiredAccess |
请求的访问权限 |
PassedAccessState |
访问状态(可选) |
Handle |
输出:句柄 |
源码位置:ntoskrnl/ob/obhandle.c:2532(file:///d:/reactos/ntoskrnl/ob/obhandle.c#L2532)
4.5.5.2 与 ObReferenceObjectByName 的区别
| 维度 | ObOpenObjectByName |
ObReferenceObjectByName |
|---|---|---|
| 输出 | 句柄(HANDLE) | 指针(PVOID) |
| 句柄表 | 需要 | 不需要 |
| 配额 | 扣除句柄配额 | 扣除对象配额 |
| 使用场景 | 用户态可见的句柄 | 仅内核可见的引用 |
4.5.5.3 名称查找与对象打开
ObOpenObjectByName 的实现:
c
NTSTATUS
NTAPI
ObOpenObjectByName(IN POBJECT_ATTRIBUTES ObjectAttributes,
/* ... */)
{
PVOID Object;
NTSTATUS Status;
PACCESS_STATE AccessState = NULL;
/* 1. 准备访问状态(如果未提供) */
if (!PassedAccessState) {
AccessState = ExAllocatePoolWithTag(NonPagedPool, sizeof(ACCESS_STATE), 'sAcA');
Status = SeCreateAccessState(AccessState, /* ... */, DesiredAccess, &ObjectType->TypeInfo.GenericMapping);
} else {
AccessState = PassedAccessState;
}
/* 2. 查找对象(详见 4.5.4 节) */
Status = ObpLookupObjectName(
ObjectAttributes->RootDirectory,
ObjectAttributes->ObjectName,
ObjectAttributes->Attributes,
ObjectType,
AccessMode,
NULL, // ParseContext
NULL, // SecurityQos
NULL, // InsertObject (NULL = 只打开)
AccessState,
&LookupContext,
&Object);
if (!NT_SUCCESS(Status)) {
goto cleanup;
}
/* 3. 创建句柄 */
Status = ObpCreateHandle(
PsGetCurrentProcess(),
Object,
AccessState,
ObjectType,
AccessMode,
0, // ObjectPointerBias
Handle);
cleanup:
if (Object) ObDereferenceObject(Object); // 释放 Lookup 增加的引用
if (AccessStateCreatedHere) SeDeleteAccessState(AccessState);
return Status;
}
4.5.5.4 安全描述符处理
ObOpenObjectByName 在打开时进行完整的权限检查:
- 对象类型检查:必须是请求的类型
- 安全描述符检查:调用方是否有 DesiredAccess 权限
- 审计日志:对敏感对象记录打开事件
4.5.5.5 句柄分配
ObOpenObjectByName 调用 ObpCreateHandle(详见 4.3.4 节):
c
Status = ObpCreateHandle(
Process,
Object,
AccessState,
ObjectType,
AccessMode,
0,
Handle);
句柄的特殊属性:
OBJ_INHERIT:可被子进程继承OBJ_KERNEL_HANDLE:内核句柄(仅 64 位)
4.5.5.6 关键设计决策
- 查找 → 引用 → 创建句柄:三阶段流程
- 释放查找的引用:Lookup 增加的引用在句柄创建后释放
- 统一的错误处理:任何一步失败都正确清理
4.5.5.7 使用场景
c
/* 场景 1:打开命名互斥体 */
UNICODE_STRING Name = RTL_CONSTANT_STRING(L"\\BaseNamedObjects\\MyMutex");
OBJECT_ATTRIBUTES ObjectAttributes;
HANDLE MutexHandle;
InitializeObjectAttributes(&ObjectAttributes, &Name, OBJ_CASE_INSENSITIVE, NULL, NULL);
Status = ObOpenObjectByName(
&ObjectAttributes,
ExMutantObjectType,
KernelMode,
MUTANT_ALL_ACCESS,
NULL,
&MutexHandle);
4.5.6 ObReferenceObjectByName()
4.5.6.1 函数签名与参数解析
c
NTSTATUS
NTAPI
ObReferenceObjectByName(IN PUNICODE_STRING ObjectName,
IN ULONG Attributes,
IN PACCESS_STATE AccessState OPTIONAL,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType,
IN KPROCESSOR_MODE AccessMode,
IN PVOID ParseContext OPTIONAL,
OUT PVOID *Object);
与 ObOpenObjectByName 的区别:
- 输出是对象指针 而非句柄
- 不调用
ObpCreateHandle - 不扣除句柄配额
源码位置:ntoskrnl/ob/obhandle.c(file:///d:/reactos/ntoskrnl/ob/obhandle.c)
4.5.6.2 使用场景
c
/* 场景 1:内核代码引用系统对象 */
UNICODE_STRING Name = RTL_CONSTANT_STRING(L"\\Device\\HarddiskVolume1");
PDEVICE_OBJECT Device;
Status = ObReferenceObjectByName(
&Name,
OBJ_CASE_INSENSITIVE,
NULL,
FILE_READ_ATTRIBUTES,
IoDeviceObjectType,
KernelMode,
NULL,
&Device);
if (NT_SUCCESS(Status)) {
/* 使用 Device */
ObDereferenceObject(Device);
}
4.5.6.3 关键设计决策
- 仅内核使用 :
ObReferenceObjectByName是内部 API,通常不导出 - 不创建句柄:避免句柄表的负担
- 安全语义与
ObOpenObjectByName相同:完整的安全检查
4.5.6.4 注意事项
- 必须确保
AccessMode是KernelMode:用户态不应调用此函数 - 必须配对
ObDereferenceObject - 不能用于打开新对象:仅用于引用已存在的对象
4.5.7 ObDereferenceObject()
4.5.7.1 函数签名与参数解析
c
VOID
NTAPI
ObDereferenceObject(IN PVOID Object);
参数:
Object:对象体指针(注意:是指向OBJECT_BODY的指针,不是OBJECT_HEADER)
返回值:无(Void)
源码位置:ntoskrnl/ob/obref.c:250(file:///d:/reactos/ntoskrnl/ob/obref.c#L250)
4.5.7.2 引用计数递减
ObDereferenceObject 的核心逻辑:
c
VOID
NTAPI
ObDereferenceObject(IN PVOID Object)
{
POBJECT_HEADER ObjectHeader = OBJECT_TO_OBJECT_HEADER(Object);
LONG_PTR NewCount;
/* 原子递减引用计数 */
NewCount = InterlockedDecrementSizeT(&ObjectHeader->PointerCount);
/* 如果引用计数归零,触发对象销毁 */
if (NewCount == 0) {
ObpDeleteObject(Object);
}
}
4.5.7.3 对象销毁触发
当 PointerCount 归零时,ObpDeleteObject 被调用:
c
VOID
NTAPI
ObpDeleteObject(IN PVOID Object)
{
POBJECT_HEADER ObjectHeader = OBJECT_TO_OBJECT_HEADER(Object);
POBJECT_TYPE ObjectType = ObjectHeader->Type;
/* 1. 从命名空间中移除 */
if (ObjectHeader->NameInfoOffset) {
ObpRemoveObject(ObjectHeader);
}
/* 2. 调用类型专属的 DeleteProcedure */
if (ObjectType->TypeInfo.DeleteProcedure) {
ObjectType->TypeInfo.DeleteProcedure(Object);
}
/* 3. 释放安全描述符 */
if (ObjectHeader->SecurityDescriptor) {
ObpDeassignSecurity(ObjectHeader);
}
/* 4. 释放 QuotaInfo */
if (ObjectHeader->QuotaInfoOffset) {
/* ... */
}
/* 5. 释放对象池内存 */
ObpDeallocateObject(ObjectHeader);
}
4.5.7.4 DeleteProcedure 调用
DeleteProcedure 是类型专属的清理函数:
| 类型 | DeleteProcedure | 清理内容 |
|---|---|---|
| Process | PspDeleteProcess |
释放 EPROCESS、PID、句柄表 |
| Thread | PspDeleteThread |
释放 ETHREAD、堆栈、APC |
| File | IopDeleteFile |
关闭 IRP、释放 FCB |
| Event | NULL | (无 Body 资源) |
| Section | MiSectionDelete |
取消映射、写回数据 |
4.5.7.5 关键设计决策
- 原子递减 :
InterlockedDecrement保证并发安全 - 延迟删除 :
PointerCount归零后才真正删除 - 类型专属清理 :
DeleteProcedure抽象出类型差异 - 命名空间解链:在删除前从命名空间移除,防止悬挂引用
4.5.7.6 引用计数泄漏的检测
常见的引用计数错误:
| 错误 | 症状 | 检测方法 |
|---|---|---|
| 忘记 Dereference | 对象永远不释放 | !handle + !object 查看对象数 |
| 过度 Dereference | 引用计数变负,触发 BSOD | BugCheck 0x18D |
| 不匹配的 Dereference | 对象被意外释放 | !analyze -v |
4.5.7.7 注意事项
- 必须配对引用:每次 Reference 必须有一次 Dereference
- 不能在持有自旋锁时调用:触发分页
- 不能在 DPC 级别调用
- 不能 Dereference NULL(虽然代码会处理)
4.5.7.8 与 ObDereferenceObjectDefer 的关系
Windows 有 ObDereferenceObjectDefer 函数:
- 在引用计数归零时不立即删除
- 而是放入延迟删除队列
- 由
ObReapObject异步清理
ReactOS 中没有 ObDereferenceObjectDefer(或实现不同),但 NT 5.2+ 之后引入这个 API 处理"自旋锁→删除"死锁问题。
4.5.8 概念解释
4.5.8.1 HANDLE_VALUE 编码
Windows 句柄的位级结构:
- 32 位系统:TagBits(2) + LowIndex(10) + MidIndex(10) + HighIndex(10)
- 64 位系统:TagBits(2) + LowIndex(10) + MidIndex(10) + HighIndex(10) + Reserved(31) + KernelFlag(1)
关键点:
- 句柄值不是连续的(大量空洞)
- 句柄不是 PID(不同维度)
4.5.8.2 OBJECT_DIRECTORY
OBJECT_DIRECTORY 是命名空间的核心数据结构:
- 37 桶 Hash 表
- 链头插入(LIFO)
- 锁保护
4.5.8.3 OBP_LOOKUP_CONTEXT
OBP_LOOKUP_CONTEXT 缓存查找结果:
- 当前目录
- 当前 Entry
- Hash 索引
- 锁状态
作用:加速"先查后创建"的操作,避免重复查找。
4.5.8.4 ParseProcedure
OB_PARSE_METHOD 是类型的 ParseProcedure 函数指针:
- 多态:每种类型自定义解析逻辑
- 链式:一个对象的 Parse 可以返回另一个对象
- 符号链接:重定向路径
4.5.8.5 STATUS_REPARSE
STATUS_REPARSE 是 NTSTATUS 的特殊值:
- 不表示错误
- 表示"路径需要重新解析"
- 由符号链接的 ParseProcedure 返回
4.5.8.6 PointerCount vs HandleCount
| 维度 | PointerCount | HandleCount |
|---|---|---|
| 含义 | 内核态引用次数 | 用户态句柄数 |
| 操作 | ObReferenceObjectByHandle、ObDereferenceObject |
ObpCreateHandle、ObpCloseHandle |
| 归零影响 | 触发对象销毁 | 触发 CloseProcedure |
4.5.9 为什么要这样设计
4.5.9.1 问题 1:为什么需要 ObpLookupEntryDirectory 作为独立函数?
将"目录内的名称查找"从 ObpLookupObjectName 中分离出来,原因:
- 性能优化:一个目录内的查找是简单操作,不需要路径解析的复杂逻辑
- 代码复用 :
ObpInsertEntryDirectory和ObpDeleteEntryDirectory都可以复用此函数 - 清晰的语义边界:「在目录中查找」和「解析完整路径」是两个不同层次的操作
4.5.9.2 问题 2:为什么 ObpLookupObjectName 不处理符号链接重解析?
ObpLookupObjectName 确实 处理符号链接,但通过 STATUS_REPARSE 机制:
- 避免递归调用:递归会导致栈溢出风险
- 循环检测:循环重试比递归更容易检测
- 可重入性:循环重试可以正确释放已获取的资源
4.5.9.3 问题 3:为什么 ObReferenceObjectByHandle 必须验证权限?
权限验证是安全的关键防线:
- 防止越权访问:用户态不能访问它没有权限的对象
- 防止权限提升:即使用户态拿到高权限句柄,也只能用它被授予的权限
- 审计需求:验证失败时记录审计日志
但内核态可以绕过------这是设计权衡:内核是可信的,不应该被自己的权限检查阻止。
4.5.9.4 问题 4:为什么 ObDereferenceObject 简单到只需要递减计数?
ObDereferenceObject 故意保持简单:
- 复杂的逻辑(命名空间移除、安全描述符释放)放到
ObpDeleteObject中 - 当
PointerCount归零时,复杂逻辑只执行一次 - 引用计数频繁变动时,复杂逻辑不执行
性能优化 :在对象引用频繁的场景下(如文件系统),只递增/递减计数非常快;只有最后一次 Dereference 才触发清理。
4.5.9.5 问题 5:为什么需要 Obp 内部的私有函数?
Obp* 函数是「Private」的,不导出给驱动:
- API 稳定性:导出函数的接口更稳定,内部函数可以自由修改
- 安全:避免驱动直接访问内部结构
- 关注点分离:内部函数由对象管理器自己使用,不暴露给外部
但内部函数经常被驱动依赖 ------这导致 ReactOS 实际上导出 了部分 Obp* 函数。
4.5.10 增强子节:引用计数的并发安全
4.5.10.1 设计意图
核心问题 :对象引用计数在多核多线程环境下需要严格的并发安全。如果两个 CPU 同时 Reference 和 Dereference 同一对象,没有原子操作可能导致:
- 计数丢失:两个操作都读到旧值,后写入的丢失
- 对象过早释放:计数在不应该归零时归零
- BSOD :
PointerCount变负触发BugCheck 0x18D
设计哲学 :「单变量原子操作 」------只用 InterlockedIncrement/InterlockedDecrement 这两个原子原语,配合「释放即不再访问」的纪律。
4.5.10.2 概念解释
- 原子操作(Atomic Operation):CPU 提供的不可中断的操作
- 内存屏障(Memory Barrier):保证指令执行顺序的 CPU 指令
- 引用计数(Reference Count):记录对象被引用次数的计数器
- ABA 问题:并发环境下可能出现的「值未变但语义已变」问题
4.5.10.3 原子操作原语
x86 提供以下原子原语:
| 原语 | 作用 |
|---|---|
LOCK XADD |
原子加法(InterlockedIncrement 内部使用) |
LOCK CMPXCHG |
原子比较并交换(InterlockedCompareExchange 内部使用) |
LOCK XCHG |
原子交换(InterlockedExchange 内部使用) |
Windows 内核包装了这些原语:
InterlockedIncrementInterlockedDecrementInterlockedCompareExchangePointerInterlockedExchangePointer
4.5.10.4 引用计数的「纪律」
引用计数的正确使用需要严格的纪律:
c
/* 1. Reference */
ObReferenceObject(Object); // PointerCount 从 1 → 2
/* 在此期间,对象保证有效 */
/* 2. 使用对象 */
Object->Field1 = ...; // 安全
/* 3. Dereference */
ObDereferenceObject(Object); // PointerCount 从 2 → 1
/* 此后,不能再访问 Object */
核心纪律:
- 不能在没有引用的情况下访问对象
- 不能多次释放同一个引用
- 不能在持有引用时假设对象不变(其它代码可能修改)
4.5.10.5 引用计数与锁的交互
引用计数与锁的交互是一个微妙的问题:
c
/* 错误:持有锁时 Dereference */
KeAcquireSpinLock(&Lock, &OldIrql);
Object = ...;
KeReleaseSpinLock(&Lock, OldIrql);
ObDereferenceObject(Object); // 可能触发分页,等待,BSOD!
/* 正确:先 Dereference 再释放锁 */
KeAcquireSpinLock(&Lock, &OldIrql);
Object = ...;
ObReferenceObject(Object); // 递增引用计数
KeReleaseSpinLock(&Lock, OldIrql);
ObDereferenceObject(Object); // 现在安全
规则:
- 不能在持有自旋锁时 Dereference(可能触发分页)
- 不能在 DPC/ISR 级别 Dereference
- 不能跨 APC 边界传递对象指针
4.5.10.6 为什么要这样设计
问题 :为什么不让 ObDereferenceObject 自动决定何时延迟删除?
答案 :这正是 ObDereferenceObjectDefer 的作用------在引用计数归零时放入延迟删除队列,由专门线程在合适时机删除。
但在 ReactOS/NT 5.0 时代没有这个 API。内核开发者必须自觉地遵守引用计数纪律。
反例 :如果让 ObDereferenceObject 自动延迟删除:
- 删除时机不可预测
- 调试困难
- 性能不可控
结论:引用计数的纪律是内核编程的基本功,没有捷径。
4.5.11 增强子节:对象生命周期管理
4.5.11.1 设计意图
核心问题:对象的生命周期涉及多个组件(Header、Info 块、Body、命名空间、句柄表、配额)。任何一处遗漏都可能导致资源泄漏或悬挂引用。
设计哲学 :「一进一退 」原则------每次 Reference 必须配对 Dereference,每次 Open 必须配对 Close。
4.5.11.2 对象生命周期的五个阶段
[创建]
ObCreateObject → 分配内存
<初始化>
ObInsertObject → 插入命名空间 + 创建句柄
PointerCount = 1 (Body 引用) + 1 (句柄引用)
↓
[使用]
ObReferenceObjectByHandle → PointerCount += 1
使用对象
ObDereferenceObject → PointerCount -= 1
↓
[关闭]
NtClose → 句柄引用 -= 1
当 HandleCount = 0 → 调用 CloseProcedure
当 PointerCount = 0 → 调用 DeleteProcedure → 释放内存
↓
[已删除]
内存返回池
4.5.11.3 概念解释
- 生命周期(Lifecycle):对象从创建到销毁的完整过程
- 悬挂引用(Dangling Reference):对象已被释放但仍有引用
- 泄漏(Leak):对象被引用但不再使用,无法释放
- TypeList 监控:通过类型对象跟踪所有该类型的对象
4.5.11.4 对象生命周期的安全保证
内核提供以下机制保证对象生命周期的安全:
- 引用计数:保证对象在使用期间不被释放
- 类型化对象 :通过
Type字段进行类型检查 - 命名空间原子性:插入/移除命名空间是原子的
- 延迟删除:避免在持锁时删除对象
4.5.11.5 引用计数与命名空间的协同
c
/* 场景:进程 A 打开进程 B 的句柄 */
1. 进程 B 被创建 → PointerCount = 1
2. 进程 A 调用 OpenProcess → 查找 \ObjectTypes\Process 中 B 的 Entry
3. ObReferenceObjectByHandle(进程B, ...) → PointerCount = 2
4. 创建句柄 → HandleCount = 1
5. 进程 A 使用句柄
/* 进程 A 关闭句柄 */
6. NtClose → 句柄 Entry 移除
7. HandleCount 递减 → CloseProcedure 可能被调用
8. PointerCount 递减(之前增加的引用) → PointerCount = 1
9. 句柄已关闭
/* 进程 B 退出 */
10. 进程 B 的 PspExitProcess → PointerCount 递减 → PointerCount = 0
11. DeleteProcedure 调用 → 释放进程 B 的资源
关键点:
- 句柄的关闭不立即删除对象(如果还有内核态引用)
- 引用计数归零才真正删除
- 关闭句柄触发
CloseProcedure(如IopCloseFile)
4.5.11.6 为什么要这样设计
问题 :为什么不让 NtClose 立即删除对象?
答案:
- 内核态代码可能仍持有对象指针
- 立即删除会导致悬挂引用
- 引用计数保证对象在使用期间有效
问题 :为什么 ObDereferenceObject 不做更多安全检查?
答案:
- 性能考虑:每次 Reference/Dereference 都执行
- 复杂度分散:安全检查由调用方负责
- 调试友好:错误更容易定位
4.5.12 小结(增强版)
4.5.12.1 7 个函数的核心要点
| 函数 | 核心职责 | 关键参数 | 典型调用方 |
|---|---|---|---|
ObReferenceObjectByHandle |
句柄 → 对象指针 + 引用 | Handle, DesiredAccess, ObjectType |
几乎所有接受 HANDLE 的系统调用 |
ObReferenceObjectByPointer |
已知指针 → 引用 | Object, ObjectType |
驱动内部、对象创建路径 |
ObpLookupEntryDirectory |
目录内名称查找/创建 | Directory, Name, Insert |
ObpLookupObjectName |
ObpLookupObjectName |
完整路径解析 | ObjectName, Attributes |
ObOpenObjectByName |
ObOpenObjectByName |
按名称打开对象 | ObjectAttributes, ObjectType |
各种 NtOpen* 系统调用 |
ObReferenceObjectByName |
按名称引用对象 | ObjectName, ObjectType |
内核内部 |
ObDereferenceObject |
释放引用 | Object |
所有 ObReference* 的配对调用 |
4.5.12.2 设计原则总结
- 三类对象访问方式:By-Handle / By-Pointer / By-Name
- 三类 API 风格:Lookup / Reference / Open
- 引用计数纪律:一进一退,不多不少
- 失败回滚:每一步都可以干净地回滚
- 并发安全:原子操作 + 内存屏障
4.5.12.3 常见陷阱
- 引用计数泄漏 :忘记调用
ObDereferenceObject - 悬挂引用:对象被释放后仍访问
- 不匹配的类型 :用
PsProcessType检查ETHREAD - 在自旋锁中 Dereference:触发分页,BSOD
- 跨线程传递对象指针:可能在线程结束时对象已被释放
4.5.12.4 后续学习路径
- 第 5 章:进程与线程管理(最复杂的使用场景)
- 第 6 章:I/O 管理器(次复杂的使用场景)
- 第 7 章:配置管理器(注册表访问)
- WRK:Windows Research Kernel(完整 NT 5.2 源码)
4.5.12.5 调试技巧
!obtrace:追踪对象创建!handle:查看进程的句柄表!object:查看对象类型!referencetable:查看对象的引用计数- Verifier:启用对象引用计数检查
4.5.12.6 本节回顾
本节是 4.1~4.4 节的"工具箱"------把对象管理的核心 API 详细拆解。读者应当能够:
- 识别这 7 个函数在源码中的使用
- 编写正确的对象引用/释放代码
- 诊断引用计数错误
- 理解对象管理器的整体 API 设计
至此,第 4 章对象管理的全部核心内容已完成。读者已经具备阅读 ReactOS 中任何涉及对象管理的代码的能力。
附录:4.5 节函数交叉引用
| 引用方 | 被引用方 | 关系 |
|---|---|---|
NtCreateEvent |
ObCreateObject |
4.5.1 节中讨论的句柄转换常用于分配 |
ObInsertObject |
ObpChargeQuotaForObject |
4.4.4 节详细讨论 |
ObInsertObject |
ObpLookupObjectName |
4.5.4 节详细讨论 |
ObInsertObject |
ObpCreateHandle |
4.3.4 节详细讨论 |
ObpLookupObjectName |
ObpLookupEntryDirectory |
4.5.3 节详细讨论 |
ObOpenObjectByName |
ObpLookupObjectName |
4.5.4 节 |
ObReferenceObjectByName |
ObpLookupObjectName |
4.5.4 节 |
ObReferenceObjectByHandle |
ObpCreateHandle |
4.3.4 节(反向) |
ObDereferenceObject |
ObpDeleteObject |
4.5.7 节详细讨论 |
ObpDeleteObject |
Type->DeleteProcedure |
4.2.4 节详细讨论 |