第 4 章 对象管理 --- 4.2 对象类型(Object Type)
4.2.0 框架图
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ 对象管理器 (Object Manager) │
└─────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ ObpTypeObjectType [「类型对象」的类型] │
│ ┌───────────────────────────────────────────────────────────────────────────────────┐ │
│ │ OBJECT_TYPE │ │
│ │ ┌──────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ Name : L"Type" │ │ │
│ │ │ Index : 1 │ │ │
│ │ │ Key : 'epyT' (ASCII 四字节标签) │ │ │
│ │ │ TypeInfo.PoolType : NonPagedPool │ │ │
│ │ │ TypeInfo.MaintainTypeList : TRUE │ │ │
│ │ │ DeleteProcedure : ObpDeleteObjectType │ │ │
│ │ │ Mutex + 4 ObjectLocks │ │ │
│ │ │ TotalNumberOfObjects : N (已注册类型数量) │ │ │
│ │ └──────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌── TypeList (LIST_ENTRY) ───► POBJECT_HEADER_CREATOR_INFO → POBJECT_HEADER │ │
│ └──┼──────────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ ┌─────────────────────────┐ ┌─────────────────────────┐ ┌───────────────┐ │
│ └─►│ PsProcessType │───►│ IoFileObjectType │───►│ ExEventObject │.. │
│ │ OBJECT_TYPE │ │ OBJECT_TYPE │ │ ...... │ │
│ │ Name: L"Process" │ │ Name: L"File" │ │ │ │
│ │ DeleteProc: PspDelete │ │ CloseProc: IopCloseFile│ │ │ │
│ └─────────────────────────┘ └─────────────────────────┘ └───────────────┘ │
└────────────────────────────────────────────────────────────────────────────────────────────┘
│
┌─────────────────┴──────────────────┐
▼ ▼
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ 对象实例 1 (EPROCESS) │ │ 对象实例 2 (FILE_OBJECT) │
│ OBJECT_HEADER │ │ OBJECT_HEADER │
│ ├─ PointerCount │ │ ├─ PointerCount │
│ ├─ HandleCount │ │ ├─ HandleCount │
│ ├─ Type ────────────────────┼──────────►│ ├─ Type ───► IoFileObjectType│
│ ├─ NameInfo / HandleInfo │ │ ├─ NameInfo / HandleInfo │
│ └─ Body (EPROCESS) │ │ └─ Body (FILE_OBJECT) │
└─────────────────────────────┘ └─────────────────────────────┘
│
┌─────────────────────────┼───────────────────────────┐
▼ ▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐ ┌──────────────────────┐
│ NtCreateProcess │ │ NtCreateFile │ │ NtCreateEvent │
│ NtOpenProcess │ │ NtOpenFile │ │ NtOpenEvent │
└──────────────────────────┘ └──────────────────────────┘ └──────────────────────┘
说明 :对象类型本身也是一种对象,所有类型对象的类型为
ObpTypeObjectType,构成一个「类型自举」结构。类型在系统初始化阶段通过
ObCreateObjectType注册,并通过TypeList链表挂在ObpTypeObjectType上进行遍历与查询。用户态通过
NtQueryObject(ObjectTypesInformation)可以枚举全部类型。
框架图深度解读:
这幅图展示了对象类型系统的核心架构,有三个层次的关系需要理解:
第一层:类型的类型(Type of Types)
图中最顶层的 ObpTypeObjectType 是整个系统的基石。它的 Name 字段是 L"Type",意味着"这是一个描述类型的类型"。这种设计被称为自举(Bootstrapping)------系统需要一个起点来定义类型本身。
在 ReactOS 启动时,ObInitSystem 首先创建 ObpTypeObjectType,然后用它来创建其他类型。这类似于面向对象语言中 Object 类是所有类的基类。
第二层:类型对象层(Type Objects)
PsProcessType、IoFileObjectType、ExEventObjectType 等都是 OBJECT_TYPE 实例。它们通过 TypeList 链表挂在 ObpTypeObjectType 的 TypeList 字段上。
每个类型对象包含:
- 该类型的元数据(Name、Index、Key)
- 该类型的行为定义(8 个 Procedure 回调)
- 该类型的统计信息(TotalNumberOfObjects、HighWaterNumberOfObjects)
- 该类型的同步机制(Mutex、ObjectLocks4)
第三层:对象实例层(Object Instances)
最底层的 EPROCESS、FILE_OBJECT 等是具体的对象实例。每个实例的 OBJECT_HEADER.Type 字段指向它所属的类型对象。
这种三层结构带来的好处是:
- 代码复用:所有对象的创建、引用、销毁逻辑都在对象管理器中统一实现
- 行为定制:每种类型通过回调函数定制自己的行为
- 类型安全 :通过
Type->Key字段可以验证句柄类型是否正确 - 统一管理 :系统可以通过
ObpTypeObjectType->TypeList枚举所有类型和对象
命名空间映射:
图中没有直接展示的是,每个类型对象都被插入到 \ObjectTypes 目录下。例如:
\ObjectTypes\Process→PsProcessType\ObjectTypes\File→IoFileObjectType\ObjectTypes\Event→ExEventObjectType
这允许用户态程序通过 NtOpenObjectByName 打开类型对象,查询该类型的统计信息。
4.2.0.1 设计意图
核心问题
Windows 是一个基于对象的操作系统,对象管理器需要统一管理内核中形形色色的对象(进程、线程、事件、文件、注册表键、设备等)。这些对象在创建方式 、生命周期 、权限模型 、打开/关闭语义 、等待能力等方面差异极大。如何用一套统一的机制来描述并驱动这些差异,是对象管理器必须解决的根本问题。
设计哲学
对象管理器采用「元对象(Meta-Object) 」的设计哲学:为每种对象创建一个「对象类型(Object Type) 」来充当模板 和行为表。内核中所有对象实例都指向其类型对象,由类型对象提供:
- 对象体大小 ------ 对象创建时分配多大的内存
- 配额费用 ------ 创建时在进程配额中扣取多少分页/非分页池
- 访问权限映射表 ------
GENERIC_READ/WRITE/EXECUTE/ALL映射到该类型的具体权限 - 有效访问掩码 & 非法属性 ------ 限定该类型允许的属性和访问
- 过程回调表 ------ 定义打开/关闭/删除/解析/查询名称/安全操作/等待时的钩子函数
这种设计使得对象管理器的核心代码保持类型无关(Type-Agnostic) :ObCreateObject、ObReferenceObjectByHandle、ObCloseHandle 等通用代码通过调用类型上的 Procedure,把类型相关的行为下放给各子系统(PS、IO、MM、CM、EX、LPC)去实现。
本节定位
本节将从 ReactOS 源码出发,深入解析 OBJECT_TYPE 结构、对象类型的创建与初始化、各类 Procedure 回调机制、以及同步对象与等待机制的整合。它是理解「对象为什么拥有行为」的关键桥梁。
4.2.1 什么是对象类型
类与实例的类比
把对象类型理解为面向对象语言中的 Class(类) ,把内核对象理解为Instance(实例):
OBJECT_TYPE≈ Class 描述:定义了对象体的大小、分配位置、配额、权限映射、虚方法表(Procedure 表)。- 内核对象(如
EPROCESS、FILE_OBJECT) ≈ Instance :由对象管理器通过ObCreateObject分配,每个对象头中的Type字段指向其类型对象。 - 同一类型的所有实例共享相同行为 :所有进程对象的删除都走
PspDeleteProcess,所有文件对象的关闭都走IopCloseFile。
对象类型定义了对象的「行为契约」
c
/* 每个对象头都保存指向其类型的指针 */
typedef struct _OBJECT_HEADER {
LONG_PTR PointerCount;
union { LONG_PTR HandleCount; volatile PVOID NextToFree; };
POBJECT_TYPE Type; /* ← 类型指针:决定此对象的行为 */
UCHAR NameInfoOffset;
UCHAR HandleInfoOffset;
UCHAR QuotaInfoOffset;
UCHAR Flags;
union {
POBJECT_CREATE_INFORMATION ObjectCreateInfo;
PVOID QuotaBlockCharged;
};
PSECURITY_DESCRIPTOR SecurityDescriptor;
QUAD Body; /* ← 对象体起始 */
} OBJECT_HEADER, *POBJECT_HEADER;
ntoskrnl/include/ndk/obtypes.h:485-505(file:///d:/reactos/sdk/include/ndk/obtypes.h#L485-L505)
系统内置类型数量概览
在 ReactOS 初始化阶段,各子系统通过 ObCreateObjectType 注册了约 20~30 种核心对象类型。部分典型类型如下:
| 所属子系统 | 典型类型 |
|---|---|
ntoskrnl/ob |
Type, Directory, SymbolicLink |
ntoskrnl/ps |
Process, Thread, Job |
ntoskrnl/io/iomgr |
File, Device, Driver, Adapter, Controller, IoCompletion |
ntoskrnl/ex |
Event, Semaphore, Mutant, Timer, EventPair, Profile, Callback |
ntoskrnl/mm |
Section |
ntoskrnl/config |
Key(注册表) |
ntoskrnl/lpc |
Port, WaitablePort |
ntoskrnl/se |
Token(访问令牌) |
ntoskrnl/dbgk |
DebugObject |
ntoskrnl/wmi |
WmiGuid |
其中 Token、Desktop、WindowStation、Key、Section、SymbolicLink 等类型对用户态不可直接创建 (没有对应的 NtCreateXxx),只能通过特定 API 或命名空间解析间接获得句柄。
4.2.2 OBJECT_TYPE 结构详解
完整结构定义
OBJECT_TYPE 定义在 sdk/include/ndk/obtypes.h:379-393(file:///d:/reactos/sdk/include/ndk/obtypes.h#L379-L393):
c
typedef struct _OBJECT_TYPE {
ERESOURCE Mutex; /* 类型对象自身的互斥资源锁 */
LIST_ENTRY TypeList; /* 挂入 ObpTypeObjectType->TypeList 的链表 */
UNICODE_STRING Name; /* 类型名称,如 L"Process"、L"File"、L"Event" */
PVOID DefaultObject; /* 默认的可等待对象 (KEVENT 指针或偏移) */
ULONG Index; /* 类型索引 (1-based, Index-1 作为 ObpObjectTypes[] 下标) */
ULONG TotalNumberOfObjects; /* 当前存活对象数量 */
ULONG TotalNumberOfHandles; /* 当前句柄数量 */
ULONG HighWaterNumberOfObjects; /* 对象数量历史峰值 (高水位) */
ULONG HighWaterNumberOfHandles; /* 句柄数量历史峰值 (高水位) */
OBJECT_TYPE_INITIALIZER TypeInfo; /* 类型初始化器 (过程回调 + 各种属性) */
ULONG Key; /* 四字节标签 (如 'File' 'Proc' 'Even'),用于句柄表校验 */
ERESOURCE ObjectLocks[4]; /* 用于分散锁,避免对象实例争抢 */
} OBJECT_TYPE;
OBJECT_TYPE_INITIALIZER 结构
OBJECT_TYPE_INITIALIZER 是调用 ObCreateObjectType 时传入的核心参数,定义在
sdk/include/ndk/obtypes.h:352-374(file:///d:/reactos/sdk/include/ndk/obtypes.h#L352-L374):
c
typedef struct _OBJECT_TYPE_INITIALIZER {
USHORT Length; /* 本结构大小 (sizeof) */
BOOLEAN UseDefaultObject; /* 是否使用默认 KEVENT 作为可等待对象 */
BOOLEAN CaseInsensitive; /* 名字比较是否大小写不敏感 (如文件) */
ULONG InvalidAttributes; /* 不允许的 OBJECT_ATTRIBUTES 标志位组合 */
GENERIC_MAPPING GenericMapping; /* GENERIC_xxx → 具体权限的映射表 */
ULONG ValidAccessMask; /* 该类型有效的访问掩码 */
BOOLEAN SecurityRequired; /* 该类型对象是否必须有安全描述符 */
BOOLEAN MaintainHandleCount; /* 是否维护 Per-Process 句柄计数 */
BOOLEAN MaintainTypeList; /* 是否维护 CreatorInfo->TypeList */
POOL_TYPE PoolType; /* 对象实例分配于 NonPagedPool 还是 PagedPool */
ULONG DefaultPagedPoolCharge; /* 默认分页池配额 */
ULONG DefaultNonPagedPoolCharge; /* 默认非分页池配额 */
OB_DUMP_METHOD DumpProcedure; /* 调试 Dump 回调 */
OB_OPEN_METHOD OpenProcedure; /* 句柄被打开/复制/继承时回调 */
OB_CLOSE_METHOD CloseProcedure; /* 句柄递减到 0 时回调 */
OB_DELETE_METHOD DeleteProcedure; /* PointerCount 为 0 时释放对象体 */
OB_PARSE_METHOD ParseProcedure; /* 命名空间解析时回调 (如 Device->File, Section 映射) */
OB_SECURITY_METHOD SecurityProcedure; /* 查询/设置安全描述符时回调 */
OB_QUERYNAME_METHOD QueryNameProcedure; /* NtQueryObject(NameInformation) 时回调 */
OB_OKAYTOCLOSE_METHOD OkayToCloseProcedure; /* 允许关闭前的确认 (通常 NULL) */
} OBJECT_TYPE_INITIALIZER, *POBJECT_TYPE_INITIALIZER;
各字段深入说明
| 字段 | 说明 |
|---|---|
Name |
类型的可读名称。通过 NtQueryObject(ObjectTypesInformation) 返回给用户态。系统内部使用 ObpLookupEntryDirectory 在 \ObjectTypes 目录下按名字查找。 |
Index |
类型索引,范围 [1, ObpObjectTypes[] 大小 - 1]。用于快速在全局 ObpObjectTypes 数组中定位类型对象。ObpTypeObjectType 自身的 Index 为 1。 |
Key |
四字节标签,由类型名前 4 字符转换为 ASCII 得到(如 File→'eliF')。用于句柄表中的句柄类型校验,防止把错误类型的句柄传入系统调用。 |
PoolType |
对象实例默认分配的池类型。同步对象(Event/Mutant/Semaphore/Timer)、Process/Thread 必须用 NonPagedPool;Directory/SymbolicLink/File 可用 PagedPool。 |
DefaultPaged/NonPagedPoolCharge |
创建一个对象时从当前进程配额扣除的内存量。在 ObCreateObjectType 内部会把 OBJECT_HEADER 及附加信息(NameInfo/HandleInfo)的尺寸自动追加到此字段。 |
InvalidAttributes |
该类型对象不允许 传入的 OBJ_* 标志位,例如 Process 类型禁止 `OBJ_PERMANENT |
GenericMapping |
权限映射表。RtlMapGenericMask 用它把 GENERIC_READ/WRITE/EXECUTE/ALL 替换为该类型具体的权限位。例如 File 类型的 GENERIC_READ 会展开为 `FILE_READ_DATA |
ValidAccessMask |
该类型定义的全部有效权限位。传入 DesiredAccess 若含有未在该掩码中定义的位会被 ObCheckObjectAccess 拒绝。 |
MaintainHandleCount |
是否维护每个对象的「进程级别句柄计数」(即 OBJECT_HEADER_HANDLE_INFO)。文件类型开启此开关,以便 NtQueryInformationFile(FilePositionInformation) 能判断文件是否被同一进程多次打开。 |
MaintainTypeList |
是否将所有此类型的已创建对象通过 OBJECT_HEADER_CREATOR_INFO::TypeList 挂入类型对象的 TypeList。用于调试与泄漏检测。受全局标志 FLG_MAINTAIN_OBJECT_TYPELIST 覆盖。 |
DefaultObject |
当 UseDefaultObject = TRUE 时,对象被作为同步对象使用,KeWaitForSingleObject 会调用 ObWaitForSingleObject,它通过 DefaultObject 取得 DISPATCHER_HEADER。对 File 类型,这里硬编码为 FILE_OBJECT.Event 的偏移量而不是全局事件指针。 |
Mutex / ObjectLocks[4] |
类型自身的锁与一组分布式锁,用于修改类型内部统计、插入 TypeList 等操作。 |
字段设计的精妙之处
OBJECT_TYPE 结构的设计体现了多种系统设计原则:
1. 锁的分层设计
Mutex 和 ObjectLocks[4] 构成了两级锁体系:
Mutex:保护类型对象本身的元数据(Name、Index、统计信息)ObjectLocks[4]:分散锁,用于保护 TypeList 的插入/删除操作
这种设计减少了锁竞争------多个线程可以同时向不同类型添加对象,而不会阻塞彼此。
2. Key 字段的双重用途
Key 字段有两个作用:
- 句柄类型验证:当通过句柄访问对象时,句柄表条目会记录类型的 Key,用于快速验证句柄类型是否匹配
- 调试辅助:Key 由类型名的前 4 个字符组成(如 'Proc'、'File'),便于调试时快速识别对象类型
3. Index 的快速查找机制
Index 字段是 1-based 的,Index - 1 直接作为全局数组 ObpObjectTypes[] 的下标。这种设计允许 O(1) 时间复杂度查找类型对象,而无需遍历 TypeList。
4. DefaultObject 的灵活性
DefaultObject 可以是:
- 全局事件指针:如 Event、Semaphore 类型
- 偏移量 :如 File 类型,存储
FILE_OBJECT.Event相对于对象头的偏移
这种灵活性使得不同类型可以以不同方式支持等待操作。
TotalNumberOfObjects / HighWater 统计
TotalNumberOfObjects:此类型当前仍存活的对象数。在ObCreateObject递增,在ObpDeleteObject递减。TotalNumberOfHandles:此类型当前仍打开的句柄数。在句柄创建/复制时递增,句柄关闭时递减。HighWaterNumberOfObjects/HighWaterNumberOfHandles:记录历史最高峰值,永不回退,供性能诊断和驱动泄露检测使用。
这与「Pool Quota」是两个正交的统计维度:Pool Charge 反映的是「一个对象占了多少池内存」,而 TotalNumberOfObjects 反映的是「创建/销毁的对象数量曲线」。
4.2.3 对象类型的初始化:ObInitSystem
对象类型的初始化分为两个阶段:
- Phase 0 (
ObInitSystem首次调用):创建最基本的三个类型 ------ObpTypeObjectType、ObpDirectoryObjectType、ObpSymbolicLinkObjectType。此时系统尚不能创建\ObjectTypes目录。 - Phase 1 (后续阶段):创建根目录
\、\KernelObjects、\ObjectTypes,然后把已注册的所有类型通过ObpInsertEntryDirectory插入到\ObjectTypes目录中,以便用户态可按名枚举。
ObInitSystem 主流程
核心代码位于 ntoskrnl/ob/obinit.c:203-303(file:///d:/reactos/ntoskrnl/ob/obinit.c#L203-L303):
c
/* Phase 0 ------ 创建 Type、Directory、SymbolicLink 三种基础类型 */
/* 1) ObpTypeObjectType: 所有对象类型的类型 */
RtlZeroMemory(&ObjectTypeInitializer, sizeof(ObjectTypeInitializer));
RtlInitUnicodeString(&Name, L"Type");
ObjectTypeInitializer.Length = sizeof(ObjectTypeInitializer);
ObjectTypeInitializer.ValidAccessMask = OBJECT_TYPE_ALL_ACCESS;
ObjectTypeInitializer.UseDefaultObject = TRUE;
ObjectTypeInitializer.MaintainTypeList = TRUE;
ObjectTypeInitializer.PoolType = NonPagedPool;
ObjectTypeInitializer.GenericMapping = ObpTypeMapping;
ObjectTypeInitializer.DefaultNonPagedPoolCharge = sizeof(OBJECT_TYPE);
ObjectTypeInitializer.InvalidAttributes = OBJ_OPENLINK;
ObjectTypeInitializer.DeleteProcedure = ObpDeleteObjectType;
ObCreateObjectType(&Name, &ObjectTypeInitializer, NULL, &ObpTypeObjectType);
/* 2) ObpDirectoryObjectType */
RtlInitUnicodeString(&Name, L"Directory");
ObjectTypeInitializer.PoolType = PagedPool;
ObjectTypeInitializer.ValidAccessMask = DIRECTORY_ALL_ACCESS;
ObjectTypeInitializer.CaseInsensitive = TRUE;
ObjectTypeInitializer.MaintainTypeList = FALSE;
ObjectTypeInitializer.GenericMapping = ObpDirectoryMapping;
ObjectTypeInitializer.DeleteProcedure = NULL;
ObjectTypeInitializer.DefaultNonPagedPoolCharge = sizeof(OBJECT_DIRECTORY);
ObCreateObjectType(&Name, &ObjectTypeInitializer, NULL, &ObpDirectoryObjectType);
ObpDirectoryObjectType->TypeInfo.ValidAccessMask &= ~SYNCHRONIZE;
/* 3) ObpSymbolicLinkObjectType */
RtlInitUnicodeString(&Name, L"SymbolicLink");
ObjectTypeInitializer.DefaultNonPagedPoolCharge = sizeof(OBJECT_SYMBOLIC_LINK);
ObjectTypeInitializer.GenericMapping = ObpSymbolicLinkMapping;
ObjectTypeInitializer.ValidAccessMask = SYMBOLIC_LINK_ALL_ACCESS;
ObjectTypeInitializer.ParseProcedure = ObpParseSymbolicLink;
ObjectTypeInitializer.DeleteProcedure = ObpDeleteSymbolicLink;
ObCreateObjectType(&Name, &ObjectTypeInitializer, NULL, &ObpSymbolicLinkObjectType);
ObpSymbolicLinkObjectType->TypeInfo.ValidAccessMask &= ~SYNCHRONIZE;
其他子系统的注册示例
Process/Thread/Job ------ ntoskrnl/ps/psmgr.c:405-438(file:///d:/reactos/ntoskrnl/ps/psmgr.c#L405-L438)
c
RtlZeroMemory(&ObjectTypeInitializer, sizeof(ObjectTypeInitializer));
ObjectTypeInitializer.Length = sizeof(ObjectTypeInitializer);
ObjectTypeInitializer.InvalidAttributes = OBJ_PERMANENT | OBJ_EXCLUSIVE | OBJ_OPENIF;
ObjectTypeInitializer.PoolType = NonPagedPool;
ObjectTypeInitializer.SecurityRequired = TRUE;
RtlInitUnicodeString(&Name, L"Process");
ObjectTypeInitializer.DefaultNonPagedPoolCharge = sizeof(EPROCESS);
ObjectTypeInitializer.GenericMapping = PspProcessMapping;
ObjectTypeInitializer.ValidAccessMask = PROCESS_ALL_ACCESS;
ObjectTypeInitializer.DeleteProcedure = PspDeleteProcess;
ObCreateObjectType(&Name, &ObjectTypeInitializer, NULL, &PsProcessType);
Event ------ ntoskrnl/ex/event.c:39-57(file:///d:/reactos/ntoskrnl/ex/event.c#L39-L57)
c
RtlZeroMemory(&ObjectTypeInitializer, sizeof(ObjectTypeInitializer));
RtlInitUnicodeString(&Name, L"Event");
ObjectTypeInitializer.Length = sizeof(ObjectTypeInitializer);
ObjectTypeInitializer.DefaultNonPagedPoolCharge = sizeof(KEVENT);
ObjectTypeInitializer.GenericMapping = ExpEventMapping;
ObjectTypeInitializer.PoolType = NonPagedPool;
ObjectTypeInitializer.ValidAccessMask = EVENT_ALL_ACCESS;
ObjectTypeInitializer.InvalidAttributes = OBJ_OPENLINK;
ObCreateObjectType(&Name, &ObjectTypeInitializer, NULL, &ExEventObjectType);
File ------ ntoskrnl/io/iomgr/iomgr.c:306-322(file:///d:/reactos/ntoskrnl/io/iomgr/iomgr.c#L306-L322)
c
RtlInitUnicodeString(&Name, L"File");
ObjectTypeInitializer.DefaultNonPagedPoolCharge = sizeof(FILE_OBJECT);
ObjectTypeInitializer.InvalidAttributes |= OBJ_EXCLUSIVE;
ObjectTypeInitializer.MaintainHandleCount = TRUE;
ObjectTypeInitializer.ValidAccessMask = FILE_ALL_ACCESS;
ObjectTypeInitializer.GenericMapping = IopFileMapping;
ObjectTypeInitializer.CloseProcedure = IopCloseFile;
ObjectTypeInitializer.DeleteProcedure = IopDeleteFile;
ObjectTypeInitializer.SecurityProcedure = IopGetSetSecurityObject;
ObjectTypeInitializer.QueryNameProcedure = IopQueryName;
ObjectTypeInitializer.ParseProcedure = IopParseFile;
ObjectTypeInitializer.UseDefaultObject = FALSE;
ObCreateObjectType(&Name, &ObjectTypeInitializer, NULL, &IoFileObjectType);
ObCreateObjectType 做了什么
ObCreateObjectType 的核心实现位于 ntoskrnl/ob/oblife.c:1136-1395(file:///d:/reactos/ntoskrnl/ob/oblife.c#L1136-L1395)。主要步骤:
- 参数校验 ------ 检查
Length、InvalidAttributes、PoolType的合法性。特别地,如果MaintainHandleCount为 TRUE,则必须提供OpenProcedure或CloseProcedure。 - 名称查重 ------ 如果
\ObjectTypes目录已存在,则在此目录中查找重名类型(ObpLookupEntryDirectory)。 - 分配类型对象 ------ 调用
ObpAllocateObject(NULL, &ObjectName, ObpTypeObjectType, sizeof(OBJECT_TYPE), ...),用类型对象自身的类型(ObpTypeObjectType)分配一个OBJECT_TYPE对象体。 - 设置自举 (Bootstrap)------ 如果
ObpTypeObjectType == NULL(即当前正在创建的就是ObpTypeObjectType自身),则把它设为自己的Header->Type,并把Key硬编码为TAG_OBJECT_TYPE('epyT'),TotalNumberOfObjects = 1。 - Key 计算 ------ 其他类型则把名称前 4 字符转 ANSI 并作为 4 字节 ULONG(如
'Proc'、'File'、'Even')。 - TypeInfo 复制 ------ 把传入的
OBJECT_TYPE_INITIALIZER复制到LocalObjectType->TypeInfo,然后调整PoolType、把 Header 大小追加到 PoolCharge、若未指定SecurityProcedure则使用默认的SeDefaultObjectMethod。 - DefaultObject 决定 :
- 若
UseDefaultObject = TRUE:默认可等待对象设为全局ObpDefaultObject(一个永远处于 Signaled 状态的 Notification Event),并把ValidAccessMask |= SYNCHRONIZE。 - 若类型名为
L"File":硬编码DefaultObject = UlongToPtr(FIELD_OFFSET(FILE_OBJECT, Event))。这样KeWaitForSingleObject能直接从FILE_OBJECT内部的事件等待,而UseDefaultObject保持 FALSE。 - 若类型名为
L"WaitablePort":硬编码DefaultObject = UlongToPtr(FIELD_OFFSET(LPCP_PORT_OBJECT, WaitEvent))。 - 否则
DefaultObject = NULL(不可等待)。
- 若
- 锁初始化 ------
ExInitializeResourceLite初始化 Mutex 及 4 个ObjectLocks。 - 挂入全局 TypeList ------ 通过
OBJECT_HEADER_CREATOR_INFO::TypeList把这个类型挂入ObpTypeObjectType->TypeList链表。 - 分配 Index ------
Index = ObpTypeObjectType->TotalNumberOfObjects,并在ObpObjectTypes[Index-1]中注册,以便快速定位。 - 插入
\ObjectTypes目录 ------ 若目录存在则按名字插入,使对象类型对用户态可见。
这个过程有一个非常优美的自举 性质:ObpTypeObjectType 是「类型对象的类型」,它自己的类型字段也指向自己 (Header->Type = ObpTypeObjectType),构成了一个自引用的不动点(Fixed-Point)。
4.2.4 类型回调机制(Procedure Callbacks)
回调签名定义
以下回调类型均定义在 sdk/include/ndk/obtypes.h:185-256(file:///d:/reactos/sdk/include/ndk/obtypes.h#L185-L256):
| 回调 | 签名 |
|---|---|
OB_OPEN_METHOD |
NTSTATUS (NTAPI *)(OB_OPEN_REASON Reason, PEPROCESS Process, PVOID ObjectBody, ACCESS_MASK GrantedAccess, ULONG HandleCount) |
OB_CLOSE_METHOD |
VOID (NTAPI *)(PEPROCESS Process, PVOID Object, ACCESS_MASK GrantedAccess, ULONG ProcessHandleCount, ULONG SystemHandleCount) |
OB_DELETE_METHOD |
VOID (NTAPI *)(PVOID Object) |
OB_PARSE_METHOD |
NTSTATUS (NTAPI *)(PVOID ParseObject, PVOID ObjectType, PACCESS_STATE AccessState, KPROCESSOR_MODE AccessMode, ULONG Attributes, PUNICODE_STRING CompleteName, PUNICODE_STRING RemainingName, PVOID Context, PSECURITY_QUALITY_OF_SERVICE SecurityQos, PVOID *Object) |
OB_SECURITY_METHOD |
NTSTATUS (NTAPI *)(PVOID Object, SECURITY_OPERATION_CODE OperationType, PSECURITY_INFORMATION SecurityInformation, PSECURITY_DESCRIPTOR SecurityDescriptor, PULONG CapturedLength, PSECURITY_DESCRIPTOR *ObjectSecurityDescriptor, POOL_TYPE PoolType, PGENERIC_MAPPING GenericMapping) |
OB_QUERYNAME_METHOD |
NTSTATUS (NTAPI *)(PVOID Object, BOOLEAN HasObjectName, POBJECT_NAME_INFORMATION ObjectNameInfo, ULONG Length, PULONG ReturnLength, KPROCESSOR_MODE AccessMode) |
OB_OKAYTOCLOSE_METHOD |
BOOLEAN (NTAPI *)(PEPROCESS Process, PVOID Object, HANDLE Handle, KPROCESSOR_MODE AccessMode) |
OB_DUMP_METHOD |
VOID (NTAPI *)(PVOID Object, POB_DUMP_CONTROL Control) |
回调触发时机总览
下表总结了每个回调的触发时机、调用者与用途:
| 回调 | 何时调用 | 调用方 (典型位置) | 用途 |
|---|---|---|---|
| OpenProcedure | 每次句柄被创建/复制/继承(包括 NtCreateXxx、NtOpenXxx、NtDuplicateObject、句柄继承) |
ObpIncrementHandleCount / ObCreateObject 之后 |
让对象类型在「一个句柄被交出」前做一次审计或调整;Process 类型用它做引用计数或配额检查。 |
| CloseProcedure | 句柄递减到 0 时(NtClose 使某对象在该进程中的句柄数归零) |
ObpDecrementHandleCount |
File 类型用于触发 IRP_MJ_CLEANUP,释放文件锁、取消重叠 I/O。 |
| DeleteProcedure | 对象头 PointerCount 从 1 → 0,真正要释放对象体时 |
ObpDeleteObject |
释放对象体内部引用(如 EPROCESS 的 Token、ImageSectionObject)。 |
| ParseProcedure | 名字解析路径上遇到该类型对象作为目录项时(\??\C:\foo.txt 解析 Device 后剩余 C:\foo.txt 交给 File 解析;Section 对象用于内存映射文件解析) |
ObpLookupObjectName |
最复杂回调:由「父对象」把剩余路径解析委托给子系统。Device/IopParseDevice、File/IopParseFile、Section/MiSectionParseProcedure、SymbolicLink/ObpParseSymbolicLink 是典型示例。 |
| SecurityProcedure | NtQuerySecurityObject / NtSetSecurityObject、创建时分配 SD |
ObGetObjectSecurity、ObSetSecurityObject |
若为默认 SeDefaultObjectMethod,使用保存在 OBJECT_HEADER::SecurityDescriptor 的 SD。Key 类型(注册表)重写此回调以读写 hive 内保存的 SD。 |
| QueryNameProcedure | NtQueryObject(ObjectNameInformation) |
ObQueryNameInfo |
由文件对象实现,返回完整的 \Device\HarddiskVolume1\foo.txt 路径(因为 FILE_OBJECT 没有 NameInfo 结构)。其他类型通常使用默认实现(返回 NameInfo 中的名称)。 |
| OkayToCloseProcedure | NtClose 之前 |
ObpCloseHandle |
极少使用。若返回 FALSE,则拒绝本次 NtClose(常用于调试或特殊保护对象)。 |
| DumpProcedure | 调试器 !object 扩展命令 |
调试支持 | 输出此对象的调试信息。 |
典型类型的回调表
| 类型 | OpenProcedure |
CloseProcedure |
DeleteProcedure |
ParseProcedure |
SecurityProcedure |
QueryNameProcedure |
|---|---|---|---|---|---|---|
| Process | --- | --- | PspDeleteProcess |
--- | 默认 | --- |
| Thread | --- | --- | PspDeleteThread |
--- | 默认 | --- |
| File | --- | IopCloseFile |
IopDeleteFile |
IopParseFile |
IopGetSetSecurityObject |
IopQueryName |
| Device | --- | --- | --- | IopParseDevice |
IopGetSetSecurityObject |
--- |
| Section | --- | --- | --- | MiSectionParseProcedure |
默认 | --- |
| Key(注册表) | --- | --- | --- | --- | CmpKeySecurityProcedure |
--- |
| SymbolicLink | --- | --- | ObpDeleteSymbolicLink |
ObpParseSymbolicLink |
默认 | --- |
| Event / Semaphore / Mutant / Timer | --- | --- | --- | --- | 默认 | --- |
| Token | --- | --- | SepDeleteToken |
--- | 默认 | --- |
| IoCompletion (IOCP) | --- | --- | IopDeleteIoCompletion |
--- | 默认 | --- |
上表说明:同步对象类型是「最纯」的对象类型 ------ 它们只是把
KEVENT/KSEMAPHORE/KMUTANT/KTIMER包一层OBJECT_HEADER,既不需要 open/close 钩子,也不需要命名解析,只依赖默认的SeDefaultObjectMethod做权限检查。
回调机制的设计原理
回调机制是对象管理器实现「类型无关核心 + 类型特定行为」的关键技术。它体现了以下设计原则:
1. 开闭原则(Open/Closed Principle)
对象管理器的核心代码(ObCreateObject、ObCloseHandle 等)对扩展开放,对修改关闭。当需要添加新的对象类型时,只需注册新的回调函数,而无需修改对象管理器的核心逻辑。
2. 策略模式(Strategy Pattern)
每种回调函数代表一种策略:
- 删除策略 :
DeleteProcedure定义了如何清理对象体 - 解析策略 :
ParseProcedure定义了如何解析路径 - 安全策略 :
SecurityProcedure定义了如何管理安全描述符
3. 职责分离
回调机制实现了对象管理器与子系统的职责分离:
- 对象管理器:负责对象的通用生命周期管理(分配、引用计数、句柄管理)
- 子系统:负责对象的类型特定行为(文件关闭时触发 IRP、进程删除时清理地址空间)
回调链的执行顺序
在对象的生命周期中,回调函数按特定顺序被调用:
创建阶段:
1. ObCreateObject → 分配内存(无回调)
2. ObInsertObject → 可能触发 ParseProcedure(路径解析)
使用阶段:
3. ObpCreateHandle → 触发 OpenProcedure(句柄创建)
4. NtDuplicateObject → 触发 OpenProcedure(句柄复制)
销毁阶段:
5. ObCloseHandle → 触发 OkayToCloseProcedure(可选)
6. ObpDecrementHandleCount → 触发 CloseProcedure(句柄数归零)
7. ObDereferenceObject → 触发 DeleteProcedure(引用计数归零)
典型回调实现分析
File 类型的 CloseProcedure
IopCloseFile 是一个典型的回调实现:
c
VOID NTAPI IopCloseFile(
PEPROCESS Process,
PVOID Object,
ACCESS_MASK GrantedAccess,
ULONG ProcessHandleCount,
ULONG SystemHandleCount
)
{
PFILE_OBJECT FileObject = (PFILE_OBJECT)Object;
// 触发 IRP_MJ_CLEANUP
IopCleanupFile(FileObject, TRUE);
// 释放文件对象的私有数据
if (FileObject->FsContext) {
ExFreePool(FileObject->FsContext);
}
// 取消所有挂起的 I/O 操作
IopCancelAllOperations(FileObject);
}
这个回调展示了 CloseProcedure 的典型职责:
- 清理资源(释放内存、关闭句柄)
- 取消挂起操作
- 通知子系统(如文件系统驱动)
4.2.5 Wait / Notification 机制与同步对象
什么是「可等待对象」
并非所有内核对象都能作为 KeWaitForSingleObject / KeWaitForMultipleObjects 的目标。只有以下对象是可等待的:
| 类型 | 内部同步原语 | Signaled 状态 |
|---|---|---|
Process (PsProcessType) |
KPROCESS.Header |
进程退出时 |
Thread (PsThreadType) |
KTHREAD.Header |
线程退出时 |
Event (ExEventObjectType) |
KEVENT |
KeSetEvent |
Semaphore (ExSemaphoreObjectType) |
KSEMAPHORE |
计数 > 0 |
Mutant (ExMutantObjectType) |
KMUTANT |
释放 / 未被占用 |
Timer (ExTimerType) |
KTIMER |
到时 |
File (IoFileObjectType) |
FILE_OBJECT.Event |
I/O 完成时设置事件 |
Key (CmpKeyObjectType) |
CM_KEY_BODY.KeyControlBlock->Lock |
极少直接等待 |
IoCompletion (IoCompletionType) |
KQUEUE |
队列非空 |
可等待对象的共同特点:对象体内部嵌入了 DISPATCHER_HEADER,它是内核调度器(Dispatcher)识别的统一头部。
DISPATCHER_HEADER 与 KWAIT_BLOCK
DISPATCHER_HEADER 定义在内核内部头文件(ntoskrnl/include/internal/ke.h)。简要结构:
+0x00 UCHAR Type; // 区分 Event/Mutant/Semaphore/Timer/Thread/Queue
+0x01 UCHAR Signalling; // 是否处于 Signaled 状态
+0x02 UCHAR Size; // 该对象体的 4 字节单位数
+0x03 UCHAR Reserved1;
+0x04 LONG SignalState; // 当前信号状态 (Event: 0/1; Mutant: -1; Semaphore: N)
+0x08 LIST_ENTRY WaitListHead; // 等待此对象的 KWAIT_BLOCK 链表
当线程调用 KeWaitForSingleObject(Object) 时:
- 对象管理器通过
ObpReferenceObjectByHandleWithTag得到对象指针。 - 对象管理器负责从对象体获取
DISPATCHER_HEADER指针 。对 File 类型,使用DefaultObject中保存的偏移量;对 Event/Mutant/Semaphore/Timer 类型,它们的对象体本身就是KEVENT,所以 Body 指针即 Dispatcher Header 指针。 KeWaitForSingleObject分配一个KWAIT_BLOCK,记录线程、对象、等待原因等,把KWAIT_BLOCK插入到DISPATCHER_HEADER.WaitListHead链表。- 把线程状态置为
Waiting,并把KTHREAD.WaitBlock[0..N]指向这些KWAIT_BLOCK。 - 下次调度器触发时,该线程不会被调度。直到对象被设置为 Signaled,Dispatcher 遍历
WaitListHead唤醒等待线程。
ObWaitForSingleObject 的角色
ObWaitForSingleObject(位于 ntoskrnl/ob/obwait.c)是 KeWaitForXxxObject 的上层封装。它负责:
- 验证句柄 → 转换为对象体
- 通过
OBJECT_TYPE::DefaultObject决定内部的DISPATCHER_HEADER - 在线程退出等待时引用对象并释放锁
- 对于 FILE 对象,它通过
DefaultObject中的偏移量 找到FILE_OBJECT.Event字段(一个KEVENT),直接传递给KeWaitForSingleObject
这种设计使得:对象管理器不需要知道任何同步原语的内部结构 ,只依赖类型对象中的 DefaultObject 偏移量或指针,实现了解耦。
等待机制的分层架构
等待机制涉及三个层次的协作:
第一层:用户态 API 层
WaitForSingleObject(hEvent, INFINITE)
↓
NtWaitForSingleObject(hEvent, FALSE, NULL)
↓
KeWaitForSingleObject(Object, Executive, KernelMode, FALSE, NULL)
第二层:对象管理器层(ObWaitForSingleObject)
负责将句柄转换为对象指针,并定位到正确的 DISPATCHER_HEADER:
c
NTSTATUS ObWaitForSingleObject(
HANDLE Handle,
BOOLEAN Alertable,
PLARGE_INTEGER Timeout
)
{
// 1. 通过句柄获取对象
ObReferenceObjectByHandle(Handle, SYNCHRONIZE, NULL, &Object);
// 2. 获取 DISPATCHER_HEADER
if (Object->Type->UseDefaultObject) {
if (Object->Type->DefaultObject & 1) {
// DefaultObject 是偏移量
DispatcherHeader = (PDISPATCHER_HEADER)((PCHAR)Object +
(ULONG_PTR)Object->Type->DefaultObject & ~1);
} else {
// DefaultObject 是指针
DispatcherHeader = (PDISPATCHER_HEADER)Object->Type->DefaultObject;
}
}
// 3. 调用调度器等待
Status = KeWaitForSingleObject(DispatcherHeader, ...);
ObDereferenceObject(Object);
return Status;
}
第三层:调度器层(KeWaitForSingleObject)
负责管理线程状态和等待队列:
线程状态转换:Running → Waiting
插入 WaitListHead 链表
调度器跳过此线程直到被唤醒
信号状态的传播机制
当对象被设置为 Signaled 状态时:
c
KeSetEvent(Event, IO_NO_INCREMENT, FALSE)
↓
唤醒 WaitListHead 上的所有等待线程
↓
线程状态:Waiting → Ready
↓
调度器在下一个时间片调度这些线程
特殊情况:FILE 对象的等待
FILE 对象的等待机制比较特殊:
FILE_OBJECT.Event是一个内嵌的KEVENTDefaultObject存储的是偏移量(0x70 左右,具体取决于版本)- 当 I/O 完成时,I/O 管理器设置此事件
- 用户态可以通过
WaitForSingleObject等待文件 I/O 完成
4.2.6 系统对象类型一览表
下表列出 ReactOS 中常见的对象类型,给出其类型名称 、用户态创建 API 、用途 、关键点/回调 以及注册位置:
| 类型名称 | 创建 API | 用途 | 关键点 / 回调 | 注册位置 |
|---|---|---|---|---|
| Type | (内核专用) | 对象类型的类型 | MaintainTypeList=TRUE, DeleteProc=ObpDeleteObjectType, 自举 |
obinit.c:278(file:///d:/reactos/ntoskrnl/ob/obinit.c#L278) |
| Directory | NtCreateDirectoryObject |
实现对象命名空间 | CaseInsensitive=TRUE, 不可等待 |
obinit.c:289(file:///d:/reactos/ntoskrnl/ob/obinit.c#L289) |
| SymbolicLink | NtCreateSymbolicLinkObject |
DOS 设备名 / 目录重定向 | ParseProc=ObpParseSymbolicLink |
obinit.c:299(file:///d:/reactos/ntoskrnl/ob/obinit.c#L299) |
| Process | NtCreateProcess, NtCreateProcessEx, NtOpenProcess |
进程抽象 | DeleteProc=PspDeleteProcess, SecurityRequired=TRUE, 不可 OBJ_PERMANENT |
psmgr.c:419(file:///d:/reactos/ntoskrnl/ps/psmgr.c#L419) |
| Thread | NtCreateThread, NtCreateThreadEx, NtOpenThread |
线程抽象 | DeleteProc=PspDeleteThread |
psmgr.c:428(file:///d:/reactos/ntoskrnl/ps/psmgr.c#L428) |
| Job | NtCreateJobObject, NtOpenJobObject |
作业对象 | DeleteProc=PspDeleteJob |
psmgr.c:438(file:///d:/reactos/ntoskrnl/ps/psmgr.c#L438) |
| File | NtCreateFile, NtOpenFile |
文件 I/O | MaintainHandleCount=TRUE, CloseProc=IopCloseFile, ParseProc=IopParseFile, SecurityProc=IopGetSetSecurityObject, QueryNameProc=IopQueryName, UseDefaultObject=FALSE 但硬编码 DefaultObject=FILE_OBJECT.Event 偏移 |
iomgr.c:319(file:///d:/reactos/ntoskrnl/io/iomgr/iomgr.c#L319) |
| Device | IoCreateDevice(驱动调用) |
设备对象 | ParseProc=IopParseDevice, DeleteProc=IopDeleteDevice, SecurityProc=IopGetSetSecurityObject |
iomgr.c:278(file:///d:/reactos/ntoskrnl/io/iomgr/iomgr.c#L278) |
| Driver | IoCreateDriver |
驱动对象 | DeleteProc=IopDeleteDriver |
iomgr.c:289(file:///d:/reactos/ntoskrnl/io/iomgr/iomgr.c#L289) |
| Controller | IoCreateController(内核专用) |
控制器对象 | --- | iomgr.c:266(file:///d:/reactos/ntoskrnl/io/iomgr/iomgr.c#L266) |
| Adapter | IoCreateAdapter(内核专用) |
适配器对象 | --- | iomgr.c:258(file:///d:/reactos/ntoskrnl/io/iomgr/iomgr.c#L258) |
| IoCompletion | NtCreateIoCompletion, NtOpenIoCompletion |
I/O 完成端口 | DeleteProc=IopDeleteIoCompletion, UseDefaultObject=TRUE |
iomgr.c:301(file:///d:/reactos/ntoskrnl/io/iomgr/iomgr.c#L301) |
| Event | NtCreateEvent, NtOpenEvent |
事件同步 | UseDefaultObject=TRUE, 无回调 |
event.c:55(file:///d:/reactos/ntoskrnl/ex/event.c#L55) |
| Semaphore | NtCreateSemaphore, NtOpenSemaphore |
信号量同步 | UseDefaultObject=TRUE, 无回调 |
sem.c:59(file:///d:/reactos/ntoskrnl/ex/sem.c#L59) |
| Mutant | NtCreateMutant, NtOpenMutant |
互斥体 | UseDefaultObject=TRUE |
mutant.c(file:///d:/reactos/ntoskrnl/ex/mutant.c) |
| Timer | NtCreateTimer, NtOpenTimer |
计时器 | UseDefaultObject=TRUE |
timer.c:239(file:///d:/reactos/ntoskrnl/ex/timer.c#L239) |
| EventPair | NtCreateEventPair |
事件对 (快速 LPC 同步) | --- | evtpair.c:50(file:///d:/reactos/ntoskrnl/ex/evtpair.c#L50) |
| Key | NtCreateKey, NtOpenKey, NtOpenKeyEx |
注册表键 | SecurityProc=CmpKeySecurityProcedure, 不可 OBJ_OPENLINK |
cmsysini.c:1029(file:///d:/reactos/ntoskrnl/config/cmsysini.c#L1029) |
| Section | NtCreateSection, NtOpenSection |
内存映射文件 | ParseProc=MiSectionParseProcedure, 用于 \\KnownDlls\\xxx.dll 解析 |
section.c:2353(file:///d:/reactos/ntoskrnl/mm/section.c#L2353) |
| Port | NtCreatePort, NtCreateWaitablePort |
LPC 端口 | ParseProc=LpcpParsePortMessage |
port.c:58-69(file:///d:/reactos/ntoskrnl/lpc/port.c#L58) |
| Token | NtCreateToken, NtDuplicateToken, NtOpenProcessToken |
访问令牌 | SecurityRequired=TRUE, DeleteProc=SepDeleteToken |
token.c:1669(file:///d:/reactos/ntoskrnl/se/token.c#L1669) |
| DebugObject | DbgkCreateDebugObject |
调试对象 | --- | dbgkobj.c:1518(file:///d:/reactos/ntoskrnl/dbgk/dbgkobj.c#L1518) |
| Profile | (内核专用) | 性能分析对象 | --- | profile.c:82(file:///d:/reactos/ntoskrnl/ex/profile.c#L82) |
4.2.7 概念解释
| 术语 | 定义 |
|---|---|
| TypeIndex / Index | 每个类型对象在系统中的唯一序号(1-based)。0 号槽位留给内部使用。类型被 ObCreateObjectType 创建时分配,保存在 OBJECT_TYPE.Index 中。NtQueryObject(ObjectTypesInformation) 返回给用户态。 |
| TypeList | 存在于两个层面:(1) ObpTypeObjectType->TypeList ------ 系统所有类型对象挂在这个链表上;(2) 每个类型自己的 TypeList ------ 当 MaintainTypeList=TRUE 时,该类型的所有对象实例通过 OBJECT_HEADER_CREATOR_INFO.TypeList 挂在这个链表上,便于遍历与泄漏检测。 |
| GENERIC_MAPPING | 权限映射表。包含 4 个字段:GenericRead / GenericWrite / GenericExecute / GenericAll,分别定义当用户态请求 GENERIC_READ 等通用权限时应展开为哪些具体的类型权限位。RtlMapGenericMask(DesiredAccess, &GenericMapping) 实现此展开。 |
| ValidAccessMask | 该类型合法的访问权限位全集(含 STANDARD_RIGHTS_* 和 SYNCHRONIZE)。创建句柄时若 DesiredAccess & ~ValidAccessMask 非零则失败。 |
| InvalidAttributes | 该类型对象创建时不允许 使用的 OBJ_* 标志位(如 OBJ_OPENLINK、OBJ_PERMANENT 等)。 |
| PoolType | 对象实例分配的池类型 ------ NonPagedPool(常驻内存,可在任意 IRQL 访问)或 PagedPool(可换出,只能在 APC_LEVEL 及以下访问)。同步对象必须是 NonPagedPool。 |
| Procedure Callbacks | 类型对象内嵌的函数指针表。对象管理器在对象生命周期的关键节点调用对应回调,使子系统获得类型相关行为的控制权。本质上是「基于表的虚函数」。 |
| Type Object(类型对象) | OBJECT_TYPE 结构实例。它本身也是一种由对象管理器管理的对象 ------ 通过 ObpAllocateObject(ObpTypeObjectType) 分配,具有标准的 OBJECT_HEADER。 |
| Type Bootstrap(类型自举) | 创建 ObpTypeObjectType 时的特殊逻辑:此时系统中尚不存在「类型对象」,所以 ObCreateObjectType 检测到 ObpTypeObjectType == NULL 就把它自己的 Header->Type 指向自身,完成自举。 |
| HighWaterMark(高水位) | OBJECT_TYPE 中记录的历史最大 TotalNumberOfObjects 和 TotalNumberOfHandles。当对象/句柄数创历史新高时更新,永不下降。 |
4.2.8 为什么要这样设计
1. 为什么对象类型本身也是一个对象?
- 自描述性 :对象管理器内部的所有管理数据(类型、目录、符号链接)都用对象统一管理 ------ 分配、引用计数、安全描述符、命名空间、句柄、配额等机制对类型自身也适用。这意味着
NtOpenDirectoryObject、NtQueryObject、NtDuplicateObject对类型对象同样有效,用户态可以以统一方式枚举/查询类型信息。 - 生命周期统一 :
OBJECT_TYPE也受引用计数保护,卸载驱动时对一个未被释放的驱动对象进行ObDereferenceObject,最终触发DeleteProcedure(例如IopDeleteDriver)。类型与对象共享同一套生命周期模型。 - 命名空间统一 :所有类型都挂在
\ObjectTypes目录下,ObReferenceObjectByName可以按名查找某个类型对象。用户态的NtQueryObject以ObjectTypesInformation查询全部类型。 - 递归之美 :类型对象的类型就是自己(
ObpTypeObjectType->Header->Type == ObpTypeObjectType),用很少的代码完成了整个类型系统的自举。
2. 为什么每个类型需要这么多回调函数(Open/Close/Delete/Parse/Security/QueryName/OkayToClose/Duplicate)?
这是对象管理器「类型无关的核心 + 类型相关的扩展」设计的直接产物。统一与差异的边界在哪里?
- Open/Close :对象管理器统一管理句柄的创建与销毁,但每个类型对「句柄变化」的反应不同 ------ File 要在句柄关闭时发送
IRP_MJ_CLEANUP释放 byte-range lock,Process 类型需要进行配额调整,Token 类型要更新审计信息。若没有这些回调,对象管理器就得硬编码对每种类型的判断,耦合度爆炸。 - Delete :对象管理器统一释放
OBJECT_HEADER、NameInfo、QuotaInfo 等,但FILE_OBJECT内部还有SectionObjectPointer、FsContext、驱动分配的IRP、缓存管理器缓存BCB等资源 ------ 这些只能由IopDeleteFile负责释放。 - Parse :命名空间路径解析是对象管理器的核心算法,但「目录节点是否是真正的目录」只能由该类型决定。
\\??\\C:\\foo.txt解析到\Device\HarddiskVolume1之后,剩余的C:\foo.txt必须交给文件系统解析。同理 Section 对象、SymbolicLink 都有自己的内部解析语义。若没有 ParseProcedure,对象管理器就不可能「跨界」解析路径。 - Security :注册表键的 SD 并不保存在
OBJECT_HEADER::SecurityDescriptor,而是保存在 hive 中。File 对象的 SD 在磁盘上的 NTFS 属性里。SecurityProcedure允许类型重写安全存取,使得NtQuerySecurityObject对所有类型表现一致。 - QueryName :File 对象的完整路径需要通过
FsContext信息反向构造(因为 File 没有 NameInfo 字段)。其他类型直接返回名称。QueryNameProcedure 提供了这一层抽象。
结论:回调是保持对象管理器通用(Type-Agnostic)同时又能处理所有类型差异的唯一可行方式。
3. 为什么用 TypeIndex + Procedure 表而不是 C++ 虚函数表(VTable)?
虽然在概念上非常相似(每个类型一张函数表),但内核选择了 TypeIndex + 显式表,而不是编译器生成的 VTable:
| 设计维度 | TypeIndex + Procedure 表 | C++ VTable |
|---|---|---|
| 链接约定 | 显式定义,每个函数指针单独可选(可以为 NULL,表示使用默认行为) | 类定义时 VTable 大小固定,必须为每个虚函数提供实现,难以表达「可选回调」 |
| 运行时动态性 | ObCreateObjectType 在运行时组装 OBJECT_TYPE_INITIALIZER,同一类型的初始化器可以按配置变更(例如 File 的 MaintainHandleCount 是运行时决定的) |
VTable 在编译期生成,类型层级一旦确定即不可更改 |
| 字段组合 | 除回调外还需要池类型、配额费用、权限掩码、名称等多种配置。Procedure 表只是 Type 对象的一部分 | VTable 只负责函数表,不负责静态字段(需要额外的 class 静态变量机制) |
| 类型校验 | 通过 Key 四字节标签和 Index 可以快速校验句柄指向的对象是否为期望类型,不必依赖 RTTI |
需要 RTTI 才能运行时判断类型,但内核不使用 C++ RTTI |
| ABI 稳定性 | 结构布局清晰,跨版本/编译器保持稳定 | VTable 布局依赖编译器的 vtable 生成规则 |
| 代码量 | OBJECT_TYPE_INITIALIZER 是纯数据结构,ObCreateObjectType 只需做一次 RtlCopyMemory |
需要实现继承体系并引入构造/析构语义 |
本质区别:C++ 的 VTable 是「按类继承」的函数表,而内核的 Procedure 表是「按类型能力」的函数表 ------ 每个回调都是可选的独立能力位,按需实现。这种设计更贴近组合模式(Composition),更符合内核扩展模型。
4. 为什么要维护 TotalNumberOfObjects/HighWaterMark 统计?
- 性能诊断 :通过
NtQueryObject(ObjectTypesInformation),用户态工具可以查询每个类型当前的对象数和句柄数,结合历史峰值快速定位异常增长。 - 驱动泄漏检测 :如果
TotalNumberOfObjects持续上升而HighWaterMark与之同步,通常暗示存在句柄泄漏或引用计数泄漏。例如 Device 对象数持续上升往往说明某驱动没有正确调用IoDeleteDevice。 - 配额与安全:对象数量上限与进程配额共同构成了资源隔离。恶意进程若能无限创建事件对象就能耗尽系统非分页池。统计字段为配额系统提供精确计数。
4.2.9 增强子节:类型安全与 GENERIC_MAPPING
设计意图
不同类型的对象需要完全不同的权限模型。例如:
- 对 File 类型:
GENERIC_READ应展开为「读数据、读属性、读扩展属性、同步」等。 - 对 Process 类型:
GENERIC_READ应展开为「查询进程信息、查询虚拟内存、查询配额」等。 - 对 Registry Key 类型:
GENERIC_READ应展开为「查询值、枚举子键、通知」等。
用户态调用者并不关心这些差异------他们只想说「我要读权限」。GENERIC_MAPPING 的作用就是在通用 API 与类型特定权限之间搭起桥梁。
概念解释
GENERIC_MAPPING 定义在 sdk/include/xdk/setypes.h(file:///d:/reactos/sdk/include/xdk/setypes.h):
c
typedef struct _GENERIC_MAPPING {
ACCESS_MASK GenericRead;
ACCESS_MASK GenericWrite;
ACCESS_MASK GenericExecute;
ACCESS_MASK GenericAll;
} GENERIC_MAPPING, *PGENERIC_MAPPING;
每个类型在 OBJECT_TYPE_INITIALIZER.GenericMapping 中填写自己的映射表。例如:
Event 类型的映射 ------ ntoskrnl/ex/event.c:20-26(file:///d:/reactos/ntoskrnl/ex/event.c#L20-L26):
c
GENERIC_MAPPING ExpEventMapping = {
STANDARD_RIGHTS_READ | EVENT_QUERY_STATE,
STANDARD_RIGHTS_WRITE | EVENT_MODIFY_STATE,
STANDARD_RIGHTS_EXECUTE | SYNCHRONIZE,
EVENT_ALL_ACCESS
};
Directory 类型的映射 ------ ntoskrnl/ob/obinit.c:27-36(file:///d:/reactos/ntoskrnl/ob/obinit.c#L27-L36):
c
GENERIC_MAPPING ObpDirectoryMapping = {
STANDARD_RIGHTS_READ | DIRECTORY_QUERY | DIRECTORY_TRAVERSE,
STANDARD_RIGHTS_WRITE | DIRECTORY_CREATE_SUBDIRECTORY | DIRECTORY_CREATE_OBJECT,
STANDARD_RIGHTS_EXECUTE | DIRECTORY_QUERY | DIRECTORY_TRAVERSE,
DIRECTORY_ALL_ACCESS
};
RtlMapGenericMask 的作用
当用户态以 GENERIC_READ | GENERIC_WRITE 作为 DesiredAccess 打开对象时,安全系统调用:
c
VOID NTAPI RtlMapGenericMask(PACCESS_MASK DesiredAccess,
PGENERIC_MAPPING GenericMapping)
{
if (*DesiredAccess & GENERIC_READ) *DesiredAccess |= GenericMapping->GenericRead;
if (*DesiredAccess & GENERIC_WRITE) *DesiredAccess |= GenericMapping->GenericWrite;
if (*DesiredAccess & GENERIC_EXECUTE) *DesiredAccess |= GenericMapping->GenericExecute;
if (*DesiredAccess & GENERIC_ALL) *DesiredAccess |= GenericMapping->GenericAll;
*DesiredAccess &= ~GENERIC_ACCESS; /* 清除 GENERIC_* 位 */
}
然后把映射后的具体权限位与该类型的 ValidAccessMask 比较,如果存在未定义位则拒绝访问。这一步在 ObCheckObjectAccess 中完成。
为什么这样设计
- API 通用性 :
NtOpenProcess(..., GENERIC_READ, ...)、NtCreateFile(..., GENERIC_READ, ...)语法完全一致,调用者不必区分 File 和 Process。 - 权限粒度精确:每个类型可以独立定义其权限粒度(File 有 FILE_READ_DATA / FILE_READ_ATTRIBUTES 等 20 多种权限,Process 有 PROCESS_QUERY_INFORMATION / PROCESS_VM_READ 等 10 多种权限)。
- 安全可控:展开后的权限位会被安全描述符中的 ACE 逐一匹配,确保只有被显式允许的权限才能生效。
- 继承性 :SD 中的 ACE 本身也可使用
GENERIC_*权限(由RtlCreateSecurityDescriptor时的RtlMapGenericMask预先展开),使得 ACL 在不同类型上具有语义一致性。
4.2.10 增强子节:命名对象 vs 匿名对象
设计意图
对象在命名空间中的可见性决定了其跨进程可见性 与安全模型 。可命名的对象(Event、Mutex、Semaphore、Section、File、Directory、Key 等)允许通过名称在全局或 session 级共享,匿名对象只能通过句柄复制(NtDuplicateObject)或继承传递给其他进程。
概念解释
命名对象
- 创建方式 :调用
NtCreateEvent/NtCreateMutex等 API 时在OBJECT_ATTRIBUTES.ObjectName中提供完整路径(例如\BaseNamedObjects\MySharedEvent或\Sessions\1\BaseNamedObjects\SessionLocalEvent)。 - 内部机制 :
ObCreateObject会为对象分配OBJECT_HEADER_NAME_INFO结构,记录名称与所属目录(Directory字段)。然后通过ObpInsertEntryDirectory把对象插入到父目录的哈希桶中。 - 打开方式 :
NtOpenEvent(L"\\BaseNamedObjects\\MySharedEvent")触发ObReferenceObjectByName→ 目录解析 → 哈希查找 → 引用对象并返回句柄。
匿名对象
- 创建方式 :
ObjectName = NULL,对象没有OBJECT_HEADER_NAME_INFO。 - 典型场景:进程内部使用的事件/信号量(用于线程间同步)、临时 IOCP、管道服务端句柄等。
- 句柄传递 :匿名对象只能通过以下方式在进程间共享:
- 句柄继承 :子进程通过
STARTUPINFO.hStdInput/Output/Error继承父进程的句柄(需要OBJ_INHERIT)。 NtDuplicateObject:在进程间复制句柄(RPC/LPC 场景常用)。- 驱动内部共享 :驱动在
DeviceExtension中保存对象指针,跨请求共享。
- 句柄继承 :子进程通过
命名空间中的对象数量与安全
- 目录哈希表 :
OBJECT_DIRECTORY使用NUMBER_HASH_BUCKETS = 37个哈希桶,链式解决冲突。 - 命名对象的 ACL :命名对象必须具有安全描述符,否则谁都能打开。默认由创建者进程的 token 和父目录的 inherited SD 决定。这正是「命名对象存在安全风险」的根本原因------攻击者若能预测命名(如
Global\\MyAppEvent),就可以尝试以弱 ACL 打开。 - Session 隔离 :从 Vista 开始,
\BaseNamedObjects\在每个 Session 中是独立的(实际上重定向到\Sessions\<id>\BaseNamedObjects),防止 Session 0 与交互式 Session 互相干扰。
为什么 NtCreateEvent 的 Name 参数是可选的?
- 对线程间同步 ,命名既无意义也无必要。匿名 Event 只需分配
sizeof(KEVENT) + OBJECT_HEADER,无需目录查找、哈希插入、名称空间分配,速度更快。 - 对跨进程同步 (如互斥体、共享内存 Section),命名是唯一可行的发现机制------两个互不相关的进程必须通过名称「约定地点」。
- 这种「可选名称」设计体现了「按需分配」的理念------不强制对象进入命名空间,避免不必要的开销与暴露。
4.2.11 增强子节:对象类型的统计信息与性能监控
设计意图
内核对象数量与句柄数量是衡量系统健康度的核心指标。对象管理器在每个类型对象上维护一套实时统计信息,供驱动作者、系统管理员与性能诊断工具使用。
概念解释
1. 对象计数与句柄计数
| 字段 | 含义 | 维护位置 |
|---|---|---|
TotalNumberOfObjects |
当前存活的该类型对象实例数 | ObCreateObject 中递增,ObpDeleteObject 中递减 |
TotalNumberOfHandles |
当前打开的该类型句柄总数 | ObpIncrementHandleCount 中递增,ObpDecrementHandleCount 中递减 |
HighWaterNumberOfObjects |
历史最大 TotalNumberOfObjects |
仅在对象数超过当前峰值时更新,永不回退 |
HighWaterNumberOfHandles |
历史最大 TotalNumberOfHandles |
同上 |
2. Pool 配额与对象计数的区别
- Pool Quota (
OBJECT_HEADER_QUOTA_INFO::PagedPoolCharge / NonPagedPoolCharge)是进程视角的统计:该进程一共「占用了」多少池内存。当进程创建对象时,对象管理器把配额记入该进程。 - TotalNumberOfObjects 是类型视角的统计:系统中该类型一共有多少对象存活。不受进程边界限制。
两者的区别可以类比为:每个家庭的电费账单 vs 全市电表总数。
3. 高水位 (HighWaterMark) 的意义
HighWaterMark 有两个重要用途:
- 泄漏检测 :如果 File 类型的
TotalNumberOfObjects与HighWaterNumberOfObjects持续同步增长,且在空闲期不回落,暗示某个驱动在IRP_MJ_CLOSE中没有正确解引用文件对象。 - 容量规划:通过历史峰值,驱动开发者可以推断「在最高峰时系统需要分配多少非分页池给 Event 对象」,从而在驱动初始化时预留足够的 lookaside list 深度。
4. OBJECT_TYPE_INFORMATION ------ 用户态查询结构
用户态通过 NtQueryObject(NULL, ObjectTypesInformation, ...) 可以查询全部类型的统计信息,结构定义在
sdk/include/ndk/obtypes.h:285-308(file:///d:/reactos/sdk/include/ndk/obtypes.h#L285-L308):
c
typedef struct _OBJECT_TYPE_INFORMATION {
UNICODE_STRING TypeName;
ULONG TotalNumberOfObjects;
ULONG TotalNumberOfHandles;
ULONG TotalPagedPoolUsage;
ULONG TotalNonPagedPoolUsage;
ULONG TotalNamePoolUsage;
ULONG TotalHandleTableUsage;
ULONG HighWaterNumberOfObjects;
ULONG HighWaterNumberOfHandles;
ULONG HighWaterPagedPoolUsage;
ULONG HighWaterNonPagedPoolUsage;
ULONG HighWaterNamePoolUsage;
ULONG HighWaterHandleTableUsage;
ULONG InvalidAttributes;
GENERIC_MAPPING GenericMapping;
ULONG ValidAccessMask;
BOOLEAN SecurityRequired;
BOOLEAN MaintainHandleCount;
ULONG PoolType;
ULONG DefaultPagedPoolCharge;
ULONG DefaultNonPagedPoolCharge;
} OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
TotalPagedPoolUsage 等字段即 TotalNumberOfObjects × DefaultPagedPoolCharge 的聚合值,用来快速估算该类型在池中所占内存。
4.2.12 小结(增强版)
本节覆盖的核心知识点
- 对象类型是对象的「模板 + 行为表」:决定对象体大小、池位置、配额费用、权限模型、回调行为。
OBJECT_TYPE本身也是一个对象 :由ObpTypeObjectType(自举类型)统一管理。ObInitSystem分阶段初始化 :先创建 Type/Directory/SymbolicLink 三种基础类型,再在 Phase 1 中创建\ObjectTypes目录并把所有类型挂入。ObCreateObjectType负责类型注册 :校验参数 → 分配OBJECT_TYPE→ 复制OBJECT_TYPE_INITIALIZER→ 计算 Key → 分配 Index → 挂入全局 TypeList。- Procedure 回调机制:Open/Close/Delete/Parse/Security/QueryName/OkayToClose 让对象管理器保持通用,由各子系统实现类型特定行为。
- 可等待对象与 DefaultObject :通过
OBJECT_TYPE.DefaultObject字段提供DISPATCHER_HEADER的获取方式,实现统一的KeWaitForSingleObject语义。 - GENERIC_MAPPING:搭起通用 API 与类型特定权限之间的桥梁。
- 命名对象 vs 匿名对象:命名对象可跨进程发现,匿名对象只能通过句柄复制/继承。
- 统计与监控:TotalNumberOfObjects / HighWaterMark / Pool Quota 三套独立的计数体系。
类型 × 回调 × API 对照表
| 类型 | 关键回调 | 典型用户态 API |
|---|---|---|
| Process | DeleteProc=PspDeleteProcess |
NtCreateProcessEx / NtOpenProcess / NtTerminateProcess |
| Thread | DeleteProc=PspDeleteThread |
NtCreateThreadEx / NtOpenThread / NtTerminateThread |
| File | CloseProc=IopCloseFile ParseProc=IopParseFile SecurityProc=IopGetSetSecurityObject QueryNameProc=IopQueryName |
NtCreateFile / NtOpenFile / NtReadFile / NtWriteFile |
| Device | ParseProc=IopParseDevice DeleteProc=IopDeleteDevice |
驱动调用 IoCreateDevice;用户态通过 File 创建间接使用 |
| Event | (无回调,纯同步对象) | NtCreateEvent / NtOpenEvent / NtSetEvent |
| Semaphore | (无回调) | NtCreateSemaphore / NtReleaseSemaphore |
| Mutant | (无回调) | NtCreateMutant / NtReleaseMutant |
| Timer | (无回调) | NtCreateTimer / NtSetTimer |
| Section | ParseProc=MiSectionParseProcedure |
NtCreateSection / NtMapViewOfSection |
| Key | SecurityProc=CmpKeySecurityProcedure |
NtCreateKey / NtOpenKey / NtQueryValueKey |
| SymbolicLink | ParseProc=ObpParseSymbolicLink DeleteProc=ObpDeleteSymbolicLink |
NtCreateSymbolicLinkObject |
| Directory | (无回调) | NtCreateDirectoryObject |
| Token | DeleteProc=SepDeleteToken |
NtOpenProcessToken / NtAdjustPrivilegesToken |
| IoCompletion | DeleteProc=IopDeleteIoCompletion |
NtCreateIoCompletion / NtSetIoCompletion |
下一步学习路径
- 4.3 句柄表 :对象创建后如何通过句柄在用户态被引用?
HANDLE如何被翻译为内核对象指针?进程句柄表的多层扩展结构、句柄继承、ObReferenceObjectByHandle的具体实现。 - 4.4 对象命名空间 :
NtCreateFile(L"\\??\\C:\\foo.txt")这样一个路径如何被ObpLookupObjectName解析?目录哈希、符号链接重定向、DosDevices 映射、Device 栈解析。 - 4.5 引用计数与生命周期 :
ObReferenceObject/ObDereferenceObject/ObpDeleteObject形成的状态机;OBJECT_HEADER_CREATOR_INFO、NameInfo、HandleInfo、QuotaInfo等可选附加结构如何按需分配。 - 5.x 各子系统对象模型:深入 Process/Thread、File/Device、Section、Registry Key 等具体类型对象的创建/打开/关闭流程。