[4]Windows内核情景分析---内核对象
写过Windows应用程序的朋友都常常听说“内核对象”、“句柄”等术语却无从得知他们的内核实现到底是怎样的, 本篇文章就揭开这些技术的神秘面纱。
常见的内核对象有:
Job、Directory(对象目录中的目录)、SymbolLink(符号链接),Section(内存映射文件)、Port(LPC端口)、IoCompletion(Io完成端口)、File(并非专指磁盘文件)、同步对象(Mutex、Event、Semaphore、Timer)、Key(注册表中的键)、Token(用户/组令牌)、Process、Thread、Pipe、Mailslot、Debug(调试端口)等
内核对象就是一个数据结构,就是一个struct结构体,各种不同类型的对象有不同的定义,本片文章不专门介绍各个具体对象类型的结构体定义,只讲述一些公共的对象管理机制。
至于各个具体对象类型的结构体定义,后文逐步会有详细介绍。
所有内核对象都遵循统一的使用模式:
第一步:先创建对象;
第二步:打开对象,得到句柄(可与第一步合并在一起,表示创建时就打开)
第三步:通过API访问对象;
第四步,关闭句柄,递减引用计数;
第五步:句柄全部关完并且引用计数降到0后,销毁对象。
句柄就是用来维系对象的把柄,就好比N名纤夫各拿一条绳,同拉一艘船。每打开一次对象就可拿到一个句柄,表示拿到该对象的一次访问权。
内核对象是全局的,各个进程都可以访问,比如两个进程想要共享某块内存来进行通信,就可以约定一个对象名,然后一个进程可以用CreatFileMapping(”SectionName”)创建一个section,而另一个进程可以用OpenFileMapping(”SectionName”)打开这个section,这样这个section就被两个进程共享了。
(注意:本篇说的都是内核对象的句柄。像什么hWnd、hDC、hFont、hModule、hHeap、hHook等等其他句柄,并不是指内核对象,因为这些句柄值不是指向进程句柄表中的索引,而是另外一种机制)
各个对象的结构体虽然不同,但有一些通用信息记录在对象头中,看下面的结构体定义
typedef struct _OBJECT_HEADER
{
LONG PointerCount;//引用计数
union
{
LONG HandleCount;//本对象的打开句柄计数(每个句柄本身也占用一个对象引用计数)
volatile VOID* NextToFree;//下一个要延迟删除的对象
};
OBJECT_TYPE* Type;//本对象的类型,类型本身也是一种内核对象,因此我习惯叫‘类型对象’
UCHAR NameInfoOffset;//对象名的偏移(无名对象没有Name)
UCHAR HandleInfoOffset;//各进程的打开句柄统计信息数组
UCHAR QuotaInfoOffset;//对象本身实际占用内存配额(当不等于该类对象的默认大小时要用到这个)
UCHAR Flags;//对象的一些属性标志
union
{
OBJECT_CREATE_INFORMATION* ObjectCreateInfo;//来源于创建对象时的OBJECT_ATTRIBUTES
PVOID QuotaBlockCharged;
};
PSECURITY_DESCRIPTOR SecurityDescriptor;//安全描述符(对象的拥有者、ACL等信息)
QUAD Body;//通用对象头后面紧跟着真正的结构体(这个字段是后面真正结构体中的第一个成员)
} OBJECT_HEADER, *POBJECT_HEADER;
如上,Body就是对象体中的第一个字段,头部后面紧跟具体对象类型的结构体定义
typedef struct _OBJECT_HEADER_NAME_INFO
{
POBJECT_DIRECTORY Directory;//对象目录中的父目录(不一定是文件系统中的目录)
UNICODE_STRING Name;//相对于Directory的路径或者全路径
ULONG QueryReferences;//对象名查询操作计数
…
} OBJECT_HEADER_NAME_INFO, *POBJECT_HEADER_NAME_INFO;
typedef struct _OBJECT_HEADER_CREATOR_INFO
{
LIST_ENTRY TypeList;//用来挂入所属‘对象类型’中的链表(也即类型对象内部的对象链表)
PVOID CreatorUniqueProcess;//表示本对象是由哪个进程创建的
…
} OBJECT_HEADER_CREATOR_INFO, *POBJECT_HEADER_CREATOR_INFO;
对象头中记录了NameInfo、HandleInfo、QuotaInfo、CreatorInfo这4种可选信息。如果这4种可选信息全部都有的话,整个对象的布局从低地址到高地址的内存布局为:
QuotaInfo-> HandleInfo->NameInfo->CreatorInfo->对象头->对象体;这4种可选信息的相对位置倒不重要,但是必须记住,他们都是在对象头中的上方(也即对象头上面的低地址端)。以下为了方便,不妨叫做“对象头中的可选信息”、“头部中的可选信息”。
于是有宏定义:
//由对象体的地址得到对象头的地址
#define OBJECT_TO_OBJECT_HEADER(pBody) CONTAINING(pBody,OBJECT_HEADER,Body)
//得到对象的名字
#define OBJECT_HEADER_TO_NAME_INFO(h)
h->NameInfoOffset?(h - h->NameInfoOffset):NULL
//得到对象的创建者信息
#define OBJECT_HEADER_TO_CREATOR_INFO(h)
h->Flags & OB_FLAG_CREATOR_INFO?h-sizeof(OBJECT_HEADER_CREATOR_INFO):NULL
所有有名字的对象都会进入内核中的‘对象目录’中,对象目录就是一棵树。内核中有一个全局指针变量ObpRootDirectoryObject,就指向对象目录树的根节点,根节点是一个根目录。
对象目录的作用就是用来将对象路径解析为对象地址。给定一个对象路径,就可以直接在对象目录中找到对应的对象。就好比给定一个文件的全路径,一定能从磁盘的根目录中向下一直搜索找到对应的文件。
如某个设备对象的对象名(全路径)是”\Device\MyCdo”,那么从根目录到这个对象的路径中:
Device是根目录中的子目录,MyDevice则是Device目录中的子节点。
对象有了名字,应用程序就可以直接调用CreateFile打开这个对象,获得句柄,没有名字的对象无法记录到对象目录中,应用层看不到,只能由内核自己使用。
内核中各种类型的对象在对象目录中的位置:
目录对象:最常见,就是对象目录中的目录节点(可以作为叶节点)
普通对象:只能作为叶节点
符号链接对象:只能作为叶节点
注意文件对象和注册表中的键对象看似有文件名、键名,但此名非对象名。因此,文件对象与键对象是无名的,无法进入对象目录中
根目录也是一种目录对象,符号链接对象可以链接到对象目录中的任何节点,包括又链向另一个符号链接对象。
对象目录中,每个目录节点下面的子节点可以是
1、 普通对象节点
2、 子目录
3、 符号链接
该目录中的所有子节点对象都保存在该目录内部的目录项列表中。不过,这个列表不是一个简单的数组,而是一个开式hash表,用来方便查找。根据该目录中各个子对象名的hash值,将对应的子对象挂入对应的hash链表中,用hash方式存储这些子对象以提高查找效率
目录本身也是一种内核对象,其类型就叫“目录类型”,现在就可以看一下这种对象的结构体定义:
typedef struct _OBJECT_DIRECTORY
{
struct _OBJECT_DIRECTORY_ENTRY* HashBuckets[37];//37条hash链
EX_PUSH_LOCK Lock;
struct _DEVICE_MAP *DeviceMap;
…
} OBJECT_DIRECTORY, *POBJECT_DIRECTORY;
如上,目录对象中的所有子对象按hash值分门别类的安放在该目录内部不同的hash链中
其中每个目录项的结构体定义为:
typedef struct _OBJECT_DIRECTORY_ENTRY
{
struct _OBJECT_DIRECTORY_ENTRY * ChainLink;//下一个目录项(即下一个子节点)
PVOID Object;//对象体的地址
ULONG HashValue;//所在hash链
} OBJECT_DIRECTORY_ENTRY, *POBJECT_DIRECTORY_ENTRY;
看到没,每个目录项记录了指向的对象的地址,同时间接记录了对象名信息
下面这个函数用来在指定的目录中查找指定名称的子对象
VOID*
ObpLookupEntryDirectory(IN POBJECT_DIRECTORY Directory,
IN PUNICODE_STRING Name,
IN ULONG Attributes,
IN POBP_LOOKUP_CONTEXT Context)
{
BOOLEAN CaseInsensitive = FALSE;
PVOID FoundObject = NULL;
//表示对象名是否严格大小写匹配查找
if (Attributes & OBJ_CASE_INSENSITIVE) CaseInsensitive = TRUE;
HashValue=CalcHash(Name->Buffer);//计算对象名的hash值
HashIndex = HashValue % 37;//获得对应的hash链索引
//记录本次是在那条hash中查找
Context->HashValue = HashValue;
Context->HashIndex = (USHORT)HashIndex;
if (!Context->DirectoryLocked)
ObpAcquireDirectoryLockShared(Directory, Context);//锁定目录,以便在其中进行查找操作
//遍历对应hash链中的所有对象
AllocatedEntry = &Directory->HashBuckets[HashIndex];
LookupBucket = AllocatedEntry;
while ((CurrentEntry = *AllocatedEntry))
{
if (CurrentEntry->HashValue == HashValue)
{
ObjectHeader = OBJECT_TO_OBJECT_HEADER(CurrentEntry->Object);
HeaderNameInfo = OBJECT_HEADER_TO_NAME_INFO(ObjectHeader);
if ((Name->Length == HeaderNameInfo->Name.Length) &&
(RtlEqualUnicodeString(Name, &HeaderNameInfo->Name, CaseInsensitive)))
{
break;//找到对应的子对象
}
}
AllocatedEntry = &CurrentEntry->ChainLink;
}
if (CurrentEntry)//如果找到了子对象
{
if (AllocatedEntry != LookupBucket)
将找到的子对象挂入链表的开头,方便下次再次查找同一对象时直接找到;
FoundObject = CurrentEntry->Object;
}
if (FoundObject) //如果找到了子对象
{
ObjectHeader = OBJECT_TO_OBJECT_HEADER(FoundObject);
ObpReferenceNameInfo(ObjectHeader);//递增对象名字的引用计数
ObReferenceObject(FoundObject);//注意递增了对象本身的引用计数
if (!Context->DirectoryLocked)
ObpReleaseDirectoryLock(Directory, Context);
}
//检查本次函数调用前,查找上下文中是否已有一个先前的中间节点对象,若有就释放
if (Context->Object)
{
ObjectHeader = OBJECT_TO_OBJECT_HEADER(Context->Object);
HeaderNameInfo = OBJECT_HEADER_TO_NAME_INFO(ObjectHeader);
ObpDereferenceNameInfo(HeaderNameInfo);
ObDereferenceObject(Context->Object);
}
Context->Object = FoundObject;
return FoundObject;//返回找到的子对象
}
如上,hash查找子对象,找不到就返回NULL。
注意由于这个函数是在遍历路径的过程中逐节逐节的调用的,所以会临时查找中间的目录节点,记录到Context中。
对象类型:
typedef struct _OBJECT_TYPE
{
ERESOURCE Mutex;
LIST_ENTRY TypeList;//本类对象的链表,记录所有同类对象
UNICODE_STRING Name;//类型名
PVOID DefaultObject;//指本类对象默认使用的同步事件对象
ULONG Index;//本类型的索引,也即表示这是系统中第几个注册的对象类型
ULONG TotalNumberOfObjects;//对象链表中总的对象个数
ULONG TotalNumberOfHandles;//所有同类对象的打开句柄总数
ULONG HighWaterNumberOfObjects;//历史本类对象个数峰值
ULONG HighWaterNumberOfHandles; //历史本类对象的句柄个数峰值
//关键字段。创建类型对象时,会将类型信息拷贝到下面这个字段中
OBJECT_TYPE_INITIALIZER TypeInfo;
ULONG Key;//事实上用作内存分配的tag,同类对象占用的内存块都标记为同一个tag
ERESOURCE ObjectLocks[4];
} OBJECT_TYPE;
//下面这个结构体描述了类型的关键信息
typedef struct _OBJECT_TYPE_INITIALIZER
{
USHORT Length;//本结构体本身的长度
BOOLEAN UseDefaultObject;//是否使用全局默认的同步事件对象
BOOLEAN CaseInsensitive;//指本类对象的对象名是否大小写不敏感
ULONG InvalidAttributes;//本类对象不支持的属性集合
GENERIC_MAPPING GenericMapping;//一直懒得去分析这个字段
ULONG ValidAccessMask;// 本类对象支持的属性集合
BOOLEAN SecurityRequired;//本类对象是否需要安全控制(另外:凡是有名字的对象都需要安全控制)
BOOLEAN MaintainHandleCount;//对象头中是否维护句柄统计信息
BOOLEAN MaintainTypeList;//是否维护创建者信息(也即是否需要挂入到所属对象类型的链表中)
POOL_TYPE PoolType;//本类对象位于分页池还是非分页池(一般内核对象都分配在非分页池中)
ULONG DefaultPagedPoolCharge; //对象占用的分页池总体大小
ULONG DefaultNonPagedPoolCharge;//对象占用的非分页池总体大小
OB_DUMP_METHOD DumpProcedure;//?
OB_OPEN_METHOD OpenProcedure;//打开对象时调用,非常重要
OB_CLOSE_METHOD CloseProcedure;//关闭句柄时调用,非常重要
OB_DELETE_METHOD DeleteProcedure;//销毁对象时调用,非常重要
OB_PARSE _METHOD ParseProcedure;//自定义的路径解析函数(设备、文件、键都提供了此函数)
OB_SECURITY_METHOD SecurityProcedure;//查询、设置对象安全描述符的函数
OB_QUERYNAME_METHOD QueryNameProcedure;//文件对象提供了自定义的QueryNameString函数
OB_OKAYTOCLOSE_METHOD OkayToCloseProcedure;//每次关闭句柄前都会调用这个函数检查可否关闭
} OBJECT_TYPE_INITIALIZER, *POBJECT_TYPE_INITIALIZER;
Windows内核中有许多预定义的对象类型,程序员也可以自己注册一些自定义的对象类型,就像自注册“窗口类”一样。下面这个函数用来注册一种对象类型(注意对象类型本身也是一种内核对象,因此,‘对象类型’即是‘类型对象’,‘类型对象’即是‘对象类型’)
NTSTATUS
ObCreateObjectType(IN PUNICODE_STRING TypeName,
IN POBJECT_TYPE_INITIALIZER ObjectTypeInitializer,
OUT POBJECT_TYPE *ObjectType)
{
ObpInitializeLookupContext(&Context);
//若 \ObjectTypes 目录下已经创建过了这种对象类型。返回失败
ObpAcquireDirectoryLockExclusive(ObpTypeDirectoryObject, &Context);
if (ObpLookupEntryDirectory(ObpTypeDirectoryObject,
TypeName,
OBJ_CASE_INSENSITIVE,
FALSE,
&Context))
{
ObpReleaseLookupContext(&Context);
return STATUS_OBJECT_NAME_COLLISION;//不能重复创建同一种对象类型
}
ObjectName.Buffer = ExAllocatePoolWithTag(PagedPool,TypeName->MaximumLength,tag);
ObjectName.MaximumLength = TypeName->MaximumLength;
RtlCopyUnicodeString(&ObjectName, TypeName);
//分配一块内存,创建类型对象
Status = ObpAllocateObject(NULL, //CreateInfo=NULL
&ObjectName,//对象的名字
ObpTypeObjectType,//类型对象本身的类型
sizeof(OBJECT_TYPE),//对象的大小
KernelMode,
(POBJECT_HEADER*)&Header);
LocalObjectType = (POBJECT_TYPE)&Header->Body;
LocalObjectType->Name = ObjectName;//类型对象的自身的名称
Header->Flags |= OB_FLAG_KERNEL_MODE | OB_FLAG_PERMANENT;//类型对象全由内核创建并有永久性
LocalObjectType->TotalNumberOfObjects =0;
LocalObjectType->TotalNumberOfHandles =0; //本类对象的个数与句柄个数=0
//拷贝类型信息(这个TypeInfo就是类型描述符)
LocalObjectType->TypeInfo = *ObjectTypeInitializer;
LocalObjectType->TypeInfo.PoolType = ObjectTypeInitializer->PoolType;
//类型对象的对象体上面的所有头部大小
HeaderSize = sizeof(OBJECT_HEADER) +
sizeof(OBJECT_HEADER_NAME_INFO)+(ObjectTypeInitializer->MaintainHandleCount ?sizeof(OBJECT_HEADER_HANDLE_INFO) : 0);
if (ObjectTypeInitializer->PoolType == NonPagedPool)
LocalObjectType->TypeInfo.DefaultNonPagedPoolCharge += HeaderSize;
else
LocalObjectType->TypeInfo.DefaultPagedPoolCharge += HeaderSize;
//查询、设置对象安全描述符的函数
if (!ObjectTypeInitializer->SecurityProcedure)
LocalObjectType->TypeInfo.SecurityProcedure = SeDefaultObjectMethod;
if (LocalObjectType->TypeInfo.UseDefaultObject)
{
LocalObjectType->TypeInfo.ValidAccessMask |= SYNCHRONIZE;//本对象可用于同步操作
LocalObjectType->DefaultObject = &ObpDefaultObject;//其实是个全局的Event对象
}
//文件对象的结构体中可自带一个事件对象,WaitForSingleObject(FileObject)等待的就是那个事件
else if ((TypeName->Length == 8) && !(wcscmp(TypeName->Buffer, L"File")))
LocalObjectType->DefaultObject =FIELD_OFFSET(FILE_OBJECT,Event);//偏移
else if ((TypeName->Length == 24) && !(wcscmp(TypeName->Buffer, L"WaitablePort")))
LocalObjectType->DefaultObject = FIELD_OFFSET(LPCP_PORT_OBJECT,WaitEvent);//偏移
else
LocalObjectType->DefaultObject = NULL;
InitializeListHead(&LocalObjectType->TypeList);
CreatorInfo = OBJECT_HEADER_TO_CREATOR_INFO(Header);
if (CreatorInfo) //将这个类型对象注册、加入全局链表中,注意这两个TypeList的含义是不一样的
InsertTailList(&ObpTypeObjectType->TypeList,&CreatorInfo->TypeList);
LocalObjectType->Index = ObpTypeObjectType->TotalNumberOfObjects;
//将这个类型对象加入全局数组中
if (LocalObjectType->Index < 32)//对象类型较少,一般够用
ObpObjectTypes[LocalObjectType->Index - 1] = LocalObjectType;
//将类型对象插入 \ObjectTypes 目录中(目录内部的指定hash链中)
bSucc=ObpInsertEntryDirectory(ObpTypeDirectoryObject, &Context, Header);
if (bSucc)
{
ObpReleaseLookupContext(&Context);
*ObjectType = LocalObjectType;
return STATUS_SUCCESS;
}
Else
{
ObpReleaseLookupContext(&Context);
return STATUS_INSUFFICIENT_RESOURCES;
}
}
如上,大致的流程就是创建一个对象类型,然后加入对象目录中的 \ObjectTypes目录中。
内核中的对象管理器在初始化的时候,会初始化对象目录。先注册创建名为“Directory”、“SymbolicLink”的对象类型,然后在对象目录中创建根目录“\”,“\ObjectTypes”目录,“\DosDevices”目录等预定义目录。
内核中的IO管理器在初始化的时候,会注册创建名为“Device”、“File”、“Driver”等对象类型,由于对象类型本身也是一种有名字的对象,所以也会挂入对象目录中,位置分别为:
“\ObjectTypes\Device”
“\ObjectTypes\File”
“\ObjectTypes\Driver”
于是,我们的驱动就可以创建对应类型的对象了。
下面我们具体看几个重点对象类型的创建过程:
OBJECT_TYPE_INITIALIZER Oti = {0};
Oti.Length=sizeof(OBJECT_TYPE_INITIALIZER);
Oti.UseDefaultObject=TRUE;
Oti.MaintainTypeList=TRUE;//一般都会维持类型信息,加入到所属类型的链表中
Oti.PoolType=NonPagePool;//所有内核对象,都默认分配在非分页池中
Oti.InvalidAttributes=OBJ_OPENLINK;//一般都不许打开符号链接
Oti.DefaultNonPagePoolCharge=sizeof(OBJECT_DIRECTORY);
Oti.UseDefaultObject=FALSE;
…
ObCreateObjectType(“Directory”,&oti,&ObpDirectoryType);//创建普通的目录对象类型
Oti.DefaultNonPagePoolCharge=sizeof(OBJECT_SYMBOLIC_LINK);
Oti.ValidAccessMask=SYMBOLIC_LINK_ALL_ACCESS;
Oti.ParseProcedure=ObpParseSymbolicLink;//关键字段。自定义解析后面的路径名
Oti.DeleteProcedure=ObpDeleteSymbolicLink;
ObCreateObjectType(“SymbolicLink”,&oti,&ObSymbolicLinkType);//创建符号链接对象类型
Oti.DefaultNonPagePoolCharge=sizeof(DEVICE_OBJECT);
Oti.ParseProcedure=IopParseDevice; //关键字段。自定义解析后面的路径名
Oti.SecurityProcedure=IopSecurityFile;
…
ObCreateObjectType(“Device”,&oti,&IoDeviceObjectType);//创建设备对象类型
Oti.DefaultNonPagePoolCharge=sizeof(FILE_OBJECT);
Oti.UseDefaultObject=FALSE;//每个文件对象内部有一个自己的事件对象
Oti.ParseProcedure=IopParseFile; //关键字段。自定义解析后面的路径名
Oti.SecurityProcedure=IopSecurityFile;
Oti.QueryNameProcedure=IopQueryNameFile;//文件对象自己负责ObQueryNameString
Oti.CloseProcedure=IopCloseFile;//关闭文件句柄时调用的函数(句柄关完后生成MJ_Cleanup irp)
Oti.DeleteProcedure=IopDeleteFile;//销毁文件对象时调用的函数(对象销毁前生成MJ_Close irp)
…
ObCreateObjectType(“File”,&oti,&IoFileObjectType);//创建文件对象类型
我们看到,符号链接、设备、文件这三类对象都提供了自定义的路径解析函数。(后文中,这册表键对象也会提供一个自定义解析函数)因为这几种对象,对象后面的剩余路径并不在对象目录中,对象目录中的叶节点到这几种对象就是终点了。比如物理磁盘卷设备对象上的某一文件路径 “\Device\Harddisk0\Partition0\Dir1\Dir2\File.txt” 的解析过程是:
先:顺着对象目录中的根目录,按“\Device\Harddisk0\Partition0”这个路径解析到这一层,找到对应的卷设备对象
再:后面剩余的路径“Dir1\Dir2\File.txt”就由具体的文件系统去解析了,最终找到对应的文件对象
另外注意一下,文件对象在句柄关完后,将产生一个IRP_MJ_CLEANUP;文件对象在引用减到0后,销毁前将产生IRP_MJ_CLOSE。这就是这两个irp的产生时机。简单记忆【柄完清理,引完关闭】
句柄:
任意进程,只要每打开一个对象,就会获得一个句柄,这个句柄用来标志对某个对象的一次打开,通过句柄,可以直接找到对应的内核对象。句柄本身是进程的句柄表中的一个结构体,用来描述一次打开操作。
句柄值则可以简单看做句柄表中的索引,并不影响理解。HANDLE的值可以简单的看做一个整形索引值。
每个进程都有一个句柄表,用来记录本进程打开的所有内核对象。句柄表可以简单看做为一个一维数组,每个表项就是一个句柄,一个结构体,一个句柄描述符,其结构体定义如下:
typedef struct _HANDLE_TABLE_ENTRY //句柄描述符
{
union
{
PVOID Object;//关键字段。该句柄指向的内核对象(注意是其头部)
ULONG_PTR ObAttributes;//关键字段。该句柄的属性
PHANDLE_TABLE_ENTRY_INFO InfoTable;
ULONG_PTR Value;//值(可见值本身是一个复合体),最低3位表示该句柄的属性(Value= Object | ObAttributes)
};
union
{
ULONG GrantedAccess;//关键字段。该句柄的访问权限
struct
{
USHORT GrantedAccessIndex;
USHORT CreatorBackTraceIndex;
};
LONG NextFreeTableEntry;//当本句柄是一个空闲表项时,用来链接到句柄表中下一个空闲表项
};
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;
句柄表则定义如下:
typedef struct _HANDLE_TABLE //句柄表描述符
{
ULONG TableCode; //表的地址|表的层数(该字段的最后两位表示表的层数)
PHANDLE_TABLE_ENTRY **Table;
PEPROCESS QuotaProcess;//所属进程
PVOID UniqueProcessId; //所属进程的PID
EX_PUSH_LOCK HandleTableLock[4];
LIST_ENTRY HandleTableList;//用来挂入全局的句柄表链表(间接给出了系统中的进程列表)
EX_PUSH_LOCK HandleContentionEvent;
ERESOURCE HandleLock;
LIST_ENTRY HandleTableList;
KEVENT HandleContentionEvent;
PHANDLE_TRACE_DEBUG_INFO DebugInfo;
LONG ExtraInfoPages;
ULONG FirstFree;//第一个空闲表项的索引位置
ULONG LastFree;//最后一个空闲表项的索引位置
ULONG NextHandleNeedingPool;//本句柄表本身占用的内存页数
LONG HandleCount;//表中的有效句柄总数
union
{
ULONG Flags;
UCHAR StrictFIFO:1;
};
} HANDLE_TABLE, *PHANDLE_TABLE;
进程的EPROCESS结构体中有一个字段HANDLE_TABLE* ObjectTable;指的就是该进程的句柄表
HANDLE ExCreateHandle(PHANDLE_TABLE HandleTable, PHANDLE_TABLE_ENTRY HandleTableEntry)
{
EXHANDLE Handle;
NewEntry = ExpAllocateHandleTableEntry(HandleTable,&Handle);//在句柄表中找到一个空闲表项
*NewEntry = *HandleTableEntry;//复制句柄表项
return Handle.GenericHandleOverlay;//返回句柄值(也即空闲表项的索引位置)
}
上面这个函数与其说是创建一个句柄,不如说是插入一个句柄。在指定句柄表中找到一个空闲未用的表项,
然后将句柄插入到那个位置,最后返回句柄的“索引”。
//下面这个函数用来打开对象,获得句柄
NTSTATUS
ObpCreateHandle(IN OB_OPEN_REASON OpenReason,//4种打开时机
IN PVOID Object, //要打开的对象
IN PACCESS_STATE AccessState, //句柄的访问权限
IN ULONG HandleAttributes, //句柄的属性
IN KPROCESSOR_MODE AccessMode,
OUT PHANDLE ReturnedHandle) //返回的句柄值
{
BOOLEAN AttachedToProcess = FALSE, KernelHandle = FALSE;
NewEntry.Object = ObjectHeader;//关键。将该句柄指向对应的对象头
if (HandleAttributes & OBJ_KERNEL_HANDLE)//如果用户要求创建一个全局型的内核句柄
{
HandleTable = ObpKernelHandleTable;//改用内核句柄表
KernelHandle = TRUE;
//将当前线程挂靠到system进程,也即修改当前的CR3,将页表换成system进程的页表
if (PsGetCurrentProcess() != PsInitialSystemProcess)
{
KeStackAttachProcess(&PsInitialSystemProcess->Pcb, &ApcState);
AttachedToProcess = TRUE;
}
}
else
HandleTable = PsGetCurrentProcess()->ObjectTable;//使用当前进程的句柄表
//检查是否可以独占打开,检查权限,若各项检查通过才打开对象,递增句柄计数,调用对象的OpenProcedure等等工作
Status = ObpIncrementHandleCount(Object,
AccessState,
AccessMode,
HandleAttributes,
PsGetCurrentProcess(),
OpenReason);
if (!NT_SUCCESS(Status))
return Status;
NewEntry.ObAttributes |= (HandleAttributes & OBJ_HANDLE_ATTRIBUTES);//填上句柄的属性
DesiredAccess =AccessState->RemainingDesiredAccess|AccessState->PreviouslyGrantedAccess;
GrantedAccess = DesiredAccess &(ObjectType->TypeInfo.ValidAccessMask);
NewEntry.GrantedAccess = GrantedAccess;//填上句柄的属性
Handle = ExCreateHandle(HandleTable, &NewEntry);//将句柄插入到句柄表中
if (Handle)//if 插入成功
{
if (KernelHandle)
Handle = ObMarkHandleAsKernelHandle(Handle);//将句柄值的最高位设为1,标记为内核句柄
*ReturnedHandle = Handle;
if (AttachedToProcess)
KeUnstackDetachProcess(&ApcState);//撤销挂靠
return STATUS_SUCCESS;
}
Else
{
…
return STATUS_INSUFFICIENT_RESOURCES;
}
}
打开对象,以得到一个访问句柄。有四种打开时机:
1、 创建对象时就打开,如CreateFile在创建一个新文件时,就同时打开了那个文件对象
2、 显式打开,如OpenFile,OpenMutex,OpenProcess显式打开某个对象
3、 DuplicateHandle这个API间接打开对象,获得句柄
4、 子进程继承父进程句柄表中的句柄,也可看做是一种打开
在这四种情况下,都会调用这个函数来打开对象,得到一个句柄。OpenReason参数就是指打开原因、时机
注意句柄值的最高位为1,就表示这是一个内核全局句柄,可以在各个进程中通用。否则,一般的句柄,只能在对应的进程中有意义。
另外有两个特殊的伪句柄,他们并不表示‘索引’,而是一个简单的代号值
GetCurrentProcessHandle 返回的句柄值是-1
GetCurrentThreadHandle 返回的句柄值是-2
对这两个句柄要特殊处理。《Windows核心编程》一书专门强调了这两个句柄的使用误区
句柄不光含有指向对象的指针,每个句柄都还有自己的访问权限与属性,这也是非常重要的。访问权限表示本次打开操作要求的、申请的并且最终得到的权限。句柄属性则表示本句柄是否可以继承,是否是独占打开的,是否是一个内核句柄等属性。
在驱动程序开发中,经常遇到的下面这个结构:
typedef struct _OBJECT_ATTRIBUTES
{
ULONG Length;//本结构体的长度
HANDLE RootDirectory;//相对目录(不一定是父目录)
PUNICODE_STRING ObjectName;//相对RootDirectory这个目录的剩余路径 或者 全路径
//上面两个字段一起间接构成对象的全路径
ULONG Attributes;//对象属性与句柄属性的混合
PVOID SecurityDescriptor;// SD安全描述符
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;
typedef CONST OBJECT_ATTRIBUTES *PCOBJECT_ATTRIBUTES;
创建对象、打开对象时都会用到这个结构。
下面这个函数用来创建一个指定类型的内核对象
NTSTATUS
ObCreateObject(IN POBJECT_TYPE Type,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN KPROCESSOR_MODE AccessMode,
IN ULONG ObjectSize,
IN ULONG PagedPoolCharge OPTIONAL,
IN ULONG NonPagedPoolCharge OPTIONAL,
OUT PVOID *Object)
{
ObjectCreateInfo = ObpAllocateObjectCreateInfoBuffer(LookasideCreateInfoList);
Status = ObpCaptureObjectCreateInformation(ObjectAttributes,FALSE,ObjectCreateInfo,
&ObjectName);//提取ObjectAttributes中的字段
if (!PagedPoolCharge)
PagedPoolCharge = Type->TypeInfo.DefaultPagedPoolCharge;
if (!NonPagedPoolCharge)
NonPagedPoolCharge = Type->TypeInfo.DefaultNonPagedPoolCharge;
ObjectCreateInfo->PagedPoolCharge = PagedPoolCharge;
ObjectCreateInfo->NonPagedPoolCharge = NonPagedPoolCharge;
//从对应池中分配内存,创建对应的对象
Status = ObpAllocateObject(ObjectCreateInfo,&ObjectName,Type,ObjectSize,AccessMode,
&Header);
return Status;
}
其实真正的工作函数是ObpAllocateObject,它内部调用
ExAllocatePoolWithTag(ObjectType->PoolType, 可选头总大小+ ObjectSize, Tag)分配对象内存,然后初始化设置头部中的Flags等其他工作。(绝大多数内核对象都分配在非分页池中)
OBJECT_ATTRIBUTES结构体中的Attributes字段是个混合成员,由句柄属性、对象属性、打开属性复合而成,可以取下面的组合
OBJ_INHERIT://句柄属性,表示句柄是否可继承给子进程
OBJ_PERMANENT://指该对象是否永久存在于对象目录中直到对象销毁.(目录\符号链接\设备\文件 都是)
OBJ_EXLUSIVE://对象属性,指该对象同一时刻只能被一个进程独占打开
OBJ_CASE_INSENSITIVE://打开属性,表示本次打开操作查找比较对象名时大小写不敏感
OBJ_OPENIF://打开属性,表示if对象存在就打开
OBJ_OPENLINK://打开属性,表示本次打开是否可以直接打开符号链接
OBJ_KERNEL_HANDLE://句柄属性,表示要求得到一个内核句柄
而对象头中的Flags字段则完全表示对象的一些属性标志
OB_FLAG_CREATE_INFO;//表示头部中含有创建时的属性信息
OB_FLAG_CREATOR_INFO;//表示含有创建者进程信息
OB_FLAG_KERNEL_MODE://表示PreviousMode是内核模式的代码创建的本对象
OB_FLAG_EXCLUSIVE://表示同一时刻只能被一个进程独占打开
OB_FLAG_PERMANET://永久性对象,直到对象完全销毁时才脱离对象目录
OB_FLAG_SINGLE_PROCESS://表示含有每进程的句柄统计信息
OB_FLAG_DEFER_DELETE;//标记本对象被延迟删除了
创建的对象,如果有名字,就需要插入到对象目录和句柄表中。即使没有名字,也需要插入到句柄表中,这样才能让应用程序得以找到该对象以进行访问。下面这个函数就是做这个的。
NTSTATUS
ObInsertObject(IN PVOID Object,
IN PACCESS_STATE AccessState OPTIONAL,
IN ACCESS_MASK DesiredAccess,
OUT PHANDLE Handle)//返回得到的句柄
{
ObjectHeader = OBJECT_TO_OBJECT_HEADER(Object);
ObjectCreateInfo = ObjectHeader->ObjectCreateInfo;
ObjectNameInfo = ObpReferenceNameInfo(ObjectHeader);
ObjectType = ObjectHeader->Type;
ObjectName = NULL;
if ((ObjectNameInfo) && (ObjectNameInfo->Name.Buffer))
ObjectName = &ObjectNameInfo->Name;
PreviousMode = KeGetPreviousMode();
//无名对象(并且不需要安全控制)就不挂入对象目录,仅仅插入句柄表中
if ( (ObjectName==NULL) && !(ObjectType->TypeInfo.SecurityRequired))
{
ObjectHeader->ObjectCreateInfo = NULL;
Status = ObpCreateUnnamedHandle(Object,DesiredAccess,ObjectCreateInfo->Attributes,
PreviousMode,Handle);
return Status;
}
InsertObject = Object;
if (ObjectName)//若是一个有名对象
{
//这个函数有两种用途。
//当Object不为NULL表示将Object插入到对象目录中的指定位置
//当Object为NULL表示查找指定的对象目录位置处的对象
//两种用途都将指定目录位置处的对象返回到InsertObject中
Status = ObpLookupObjectName(ObjectCreateInfo->RootDirectory,
ObjectName,
ObjectCreateInfo->Attributes,
ObjectType,
ObjectCreateInfo->ParseContext,
Object,//要插入的对象
&InsertObject);//返回最终那个位置处的对象
//如果原位置处已有同名对象,插入失败
if ((NT_SUCCESS(Status)) && (InsertObject) && (Object != InsertObject))
{
OpenReason = ObOpenHandle;//既然插入失败了,那就是要打开对象,获得句柄
if (ObjectCreateInfo->Attributes & OBJ_OPENIF)//检查本次打开操作的要求
{
if (ObjectType != OBJECT_TO_OBJECT_HEADER(InsertObject)->Type)
Status = STATUS_OBJECT_TYPE_MISMATCH;
else
Status = STATUS_OBJECT_NAME_EXISTS;//看到没,应用层经常返回这个出错值
}
else
{
Status = STATUS_OBJECT_NAME_COLLISION;
}
return Status;
}
}
if (InsertObject == Object)//if 插入成功
OpenReason = ObCreateHandle;//只有第一次创建对象的时候才会插入对象目录中
ObjectHeader->ObjectCreateInfo = NULL;//不再需要了
if (Handle)//如果用户要求插入句柄表,就插入句柄表,得到一个句柄
{
Status = ObpCreateHandle(OpenReason,InsertObject,AccessState,
ObjectCreateInfo->Attributes, PreviousMode,Handle);
}
return Status;
}
下面看一下驱动程序经常调用的那些内核函数
NTSTATUS
ObReferenceObjectByHandle(IN HANDLE Handle,IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType,IN KPROCESSOR_MODE AccessMode,
OUT PVOID* Object,OUT POBJECT_HANDLE_INFORMATION HandleInformation)
{
*Object = NULL;
//若句柄是一个内核句柄或当前进程、线程的句柄
if (HandleToLong(Handle) < 0)
{
if (Handle == NtCurrentProcess())//若句柄值是当前进程的句柄(-1),特殊处理
{
if ((ObjectType == PsProcessType) || !(ObjectType))
{
CurrentProcess = PsGetCurrentProcess();
GrantedAccess = CurrentProcess->GrantedAccess;
//if内核模式/要求的权限<=进程对象支持的权限(权限检查)
if ((AccessMode == KernelMode) ||!(~GrantedAccess & DesiredAccess))
{
if (HandleInformation)
{
HandleInformation->HandleAttributes = 0;
HandleInformation->GrantedAccess = GrantedAccess;
}
ObjectHeader = OBJECT_TO_OBJECT_HEADER(CurrentProcess);
InterlockedExchangeAdd(&ObjectHeader->PointerCount, 1);//递增引用计数
*Object = CurrentProcess;//返回得到的对象指针
Status = STATUS_SUCCESS;
}
Else //权限检查不通过
Status = STATUS_ACCESS_DENIED;
}
else
Status = STATUS_OBJECT_TYPE_MISMATCH;
return Status;
}
else if (Handle == NtCurrentThread())//若句柄值是当前线程的句柄(-2),特殊处理
{
if ((ObjectType == PsThreadType) || !(ObjectType))
{
CurrentThread = PsGetCurrentThread();
GrantedAccess = CurrentThread->GrantedAccess;
if ((AccessMode == KernelMode) ||!(~GrantedAccess & DesiredAccess))
{
if (HandleInformation)
{
HandleInformation->HandleAttributes = 0;
HandleInformation->GrantedAccess = GrantedAccess;
}
ObjectHeader = OBJECT_TO_OBJECT_HEADER(CurrentThread);
InterlockedExchangeAdd(&ObjectHeader->PointerCount, 1);
*Object = CurrentThread;
Status = STATUS_SUCCESS;
}
else
Status = STATUS_ACCESS_DENIED;
}
else
Status = STATUS_OBJECT_TYPE_MISMATCH;
return Status;
}
else if (AccessMode == KernelMode)//若句柄是一个内核句柄
{
Handle = ObKernelHandleToHandle(Handle);//去掉最高位的1,转为普通句柄
HandleTable = ObpKernelHandleTable;//采用内核句柄表
}
}
Else //最典型的情况,普通句柄,就使用当前进程的句柄表
HandleTable = PsGetCurrentProcess()->ObjectTable;
//以该句柄的值为“索引”,找到句柄表中对应的句柄表项
HandleEntry = ExMapHandleToPointer(HandleTable, Handle)
if (HandleEntry)//如果找到了,这就是一个有效句柄
{
ObjectHeader = ObpGetHandleObject(HandleEntry);//关键。获得该句柄指向的对应对象
if (!(ObjectType) || (ObjectType == ObjectHeader->Type))
{
GrantedAccess = HandleEntry->GrantedAccess;
if ((AccessMode == KernelMode) ||!(~GrantedAccess & DesiredAccess))//通过权限检查
{
InterlockedIncrement(&ObjectHeader->PointerCount);
Attributes = HandleEntry->ObAttributes & OBJ_HANDLE_ATTRIBUTES;
if (HandleInformation)
{
HandleInformation->HandleAttributes = Attributes;
HandleInformation->GrantedAccess = GrantedAccess;
}
*Object = &ObjectHeader->Body;//返回的是对象体的地址
return STATUS_SUCCESS;
}
Else //权限检查没通过
Status = STATUS_ACCESS_DENIED;
}
else
Status = STATUS_OBJECT_TYPE_MISMATCH;
}
Else //有可能用户给定的句柄值是一个无效句柄,在句柄表中找不到
Status = STATUS_INVALID_HANDLE;
*Object = NULL;
return Status;
}
如上,这个函数从句柄得到对应的内核对象,并递增其引用计数。
两个特殊情况:
#define NtCurrentProcess() (HANDLE)-1
#define NtCurrentThread() (HANDLE)-2
这是两个伪句柄值,永远获得的是当前进程、线程的内核对象。
另外:若句柄值的最高位是1,则是一个内核句柄,各进程通用。内核型句柄是“System”进程的句柄表中的句柄。因此,要获得内核句柄对应的对象,系统会挂靠到“System”进程的地址空间中,去查询句柄表。
根据句柄值在句柄表中找到对应的表项是靠ExMamHandleToPointer这个函数实现的,这个函数又在内部调用ExpLookupHandleTableEntry来真正查找。句柄表组织为一个稀疏数组(目的用来节省内存),但可以
简单的看做一个一维数组,不影响理解,句柄值本身也可简单理解为一个索引。
NTSTATUS
ObReferenceObjectByPointer(IN PVOID Object,IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType,IN KPROCESSOR_MODE AccessMode)
{
POBJECT_HEADER Header;
Header = OBJECT_TO_OBJECT_HEADER(Object);
if ((Header->Type != ObjectType) && ((AccessMode != KernelMode) ||
(ObjectType == ObSymbolicLinkType)))
return STATUS_OBJECT_TYPE_MISMATCH;
InterlockedIncrement(&Header->PointerCount);//递增对象的引用计数
return STATUS_SUCCESS;
}
上面这个函数其实是递增对象的引用计数而已(手握一个引用计数后,就可以防止对象被析构释放,因为对象只有在引用计数减到0后才会释放,从而防止因对象析构引起的莫名其妙的崩溃)
在对象目录中的查找过程:
给定一个对象名,如“\Device\Harddisk0\Partition0\Dir1\Dir2\File.txt”,如何查找到对应的对象呢?
这个路径先在对象目录中一路找到\Device\Harddisk0\Partition0表示的磁盘卷设备对象,然后再沿着剩余路径“Dir1\Dir2\File.txt”找到对应的文件对象,不过后半部的查找过程是文件系统的事了,后面我将详细讲解。这里看前半部的查找,是如何找到对应的卷设备的。
前文我们讲过了一个函数: ObpLookupEntryDirectory,那个函数用来在指定的目录中找到指定名称的子对象,现在就需要沿着路径,反复调用这个函数找到我们的卷设备。下面的函数就是用来这个目的的。可以给定任意一个起点目录,以及相对那个起点目录的任意长的路径,找到指定的对象。这个函数的代码有点长…(请做好心理准备),原函数差不多有800行长,我做了粗略压缩,在我的详尽解释下,相信您可以看明白的。
NTSTATUS
ObpLookupObjectName(IN HANDLE RootHandle OPTIONAL,//搜索的起点对象,如果为NULL,则表示根节点
IN PUNICODE_STRING ObjectName,//相对RootHandle的路径或者全路径
IN ULONG Attributes,//本次查找操作的属性(如是否忽略大小写)
IN POBJECT_TYPE ObjectType,//要查的目标对象的类型
IN OUT PVOID ParseContext,//其它
IN PVOID InsertObject OPTIONAL,//如果不为NULL,则表示找不到就插入这个对象
IN OUT PACCESS_STATE AccessState,//传入当前线程的令牌以及申请的访问权限
OUT POBP_LOOKUP_CONTEXT LookupContext,//用来返回查找结果细节信息
OUT PVOID *FoundObject)//返回最终找到的对象
{
*FoundObject = NULL;//初始为未找到
PVOID Object=NULL;
//分别指相对当前起点对象的剩余路径、剩余路径中的第一个节点名
UNICODE_STRING RemainingName, ComponentName;
BOOLEAN Reparse = FALSE, SymLink = FALSE;//分别表示是否需要重新解析、是否为符号链接的标志
//表示搜索路径中当前的目录,父目录,以及起点目录
POBJECT_DIRECTORY Directory = NULL, ParentDirectory = NULL, RootDirectory;
//表示最终要插入到那个位置的叶目录及父目录
POBJECT_DIRECTORY ReferencedDirectory = NULL, ReferencedParentDirectory = NULL;
OB_PARSE_METHOD ParseRoutine;//对象自带的解析函数
ULONG MaxReparse = 30;//符号链接等,总的重新解析次数最大不能超过30次
NTSTATUS Status = STATUS_SUCCESS;//预期找到成功
ObpInitializeLookupContext(LookupContext);
if (!(ObjectType) || (ObjectType->TypeInfo.CaseInsensitive))
Attributes |= OBJ_CASE_INSENSITIVE;//检查本类对象是否支持对象名可以忽略大小写
if (RootHandle)//如果给定了搜索起点
{
Status = ObReferenceObjectByHandle(RootHandle,AccessMode, &RootDirectory,…);
ObjectHeader = OBJECT_TO_OBJECT_HEADER(RootDirectory);//得到起点对象
if (ObjectHeader->Type != ObDirectoryType)//if给定的起点对象不是目录
{
ParseRoutine = ObjectHeader->Type->TypeInfo.ParseProcedure;
if (!ParseRoutine) return STATUS_INVALID_HANDLE;//非目录对象必须自带解析函数
MaxReparse = 30;
while (TRUE)
{
RemainingName = *ObjectName;//当前的剩余路径=初始路径
Status = ParseRoutine(RootDirectory,//起点
ObjectType,
AccessState,
AccessCheckMode,
Attributes,
IN、OUT ObjectName,//传入初始全路径,可能传出新的初始路径
IN、OUT &RemainingName,//传入当前剩余路径,返回最终的剩余路径
ParseContext,
&Object);//返回找到的对象
// if自带解析函数解析完毕了,不要求重新解析
if ((Status != STATUS_REPARSE) && (Status != STATUS_REPARSE_OBJECT))
{
if (!NT_SUCCESS(Status))//如果未能解析
Object = NULL;
else if (!Object)//如果未能找到目标对象
Status = STATUS_OBJECT_NAME_NOT_FOUND;
*FoundObject = Object;//有可能找到的不是最终的目标对象
return Status;//不管查找成功还是失败都从这儿返回了
}
//if自带解析函数要求从根目录开始按新的初始路径重新解析
else if ( (ObjectName->Buffer[0] == L”\\”) )
{
RootDirectory = ObpRootDirectoryObject;
RootHandle = NULL;
goto ParseFromRoot;//从根目录开始解析
}
//if自带解析函数要求从指定对象开始重新解析剩余路径(很少见)
else if (--MaxReparse)//如果未能超出最大重解析次数,就重定向解析
continue;
else//如果超出了最大重解析次数,失败返回
{
*FoundObject = Object;
if (!Object) Status = STATUS_OBJECT_NAME_NOT_FOUND;
return Status;
}
}
}
}
Else //如果没有给定搜索起点,就从根目录开始搜索,把ObjectName参数当绝对路径使
{
RootDirectory = ObpRootDirectoryObject;//搜素起点=根目录
if ( (ObjectName->Buffer[0] != L”\\”))//绝对路径必须以“\”开头
return STATUS_OBJECT_PATH_SYNTAX_BAD;
}
ParseFromRoot:
if (!SymLink)
{
Reparse = TRUE;//表示是否需要继续重新解析的标志,初始为真
MaxReparse = 30;//最多30次
}
while (Reparse)//这层循环表示处理每一轮从根目录开始的重新解析操作
{
RemainingName = *ObjectName;//该轮重解析的新的初始剩余路径
Reparse = FALSE;//预期本次解析操作一次解析成功,不需要重定向解析
while (TRUE)//遍历该轮重解析的路径中的各个节点
{
Object = NULL;
if ((RemainingName.Length) && (RemainingName.Buffer[0] == L”\\”))
{
RemainingName.Buffer++;
RemainingName.Length -=2;
}
//上面的操作跳过剩余路径中开头的\字符
ComponentName = RemainingName;
while (RemainingName.Length)
{
if (RemainingName.Buffer[0] == L”\\”) break;
RemainingName.Buffer++;
RemainingName.Length -=2;
}
ComponentName.Length -= RemainingName.Length;
//上面的操作获得当前剩余路径中的第一个节点名,即ComponentName
if (!Directory) Directory = RootDirectory;//当前目录
//检查当前线程的令牌是否含有遍历当前目录的权限(先检查令牌特权,再检查用户/组权)
if (!(AccessState->Flags & TOKEN_HAS_TRAVERSE_PRIVILEGE))
{
ReferencedDirectory = Directory;
if (ParentDirectory)
{
if (!ObpCheckTraverseAccess(ParentDirectory,
DIRECTORY_TRAVERSE,//检查目录遍历权限
AccessState,AccessCheckMode,))
break;//权限检查失败,退出整个函数
}
}
if (RemainingName.Length==0)//if ComponentName是路径中的最后一个节点了
{
if (!ReferencedDirectory)
ReferencedDirectory = Directory;//记录叶目录是哪一个
if (InsertObject)//要在叶目录中插入对象,必须锁定整个目录
ObpAcquireDirectoryLockExclusive(Directory, LookupContext);
}
//关键。查找剩余路径中的第一个节点(这个函数前面已分析过)
Object = ObpLookupEntryDirectory(Directory,&ComponentName,Attributes,
InsertObject ? FALSE : TRUE,LookupContext);
if (!Object)
{
if (RemainingName.Length>0)
{
Status = STATUS_OBJECT_PATH_NOT_FOUND;
break;//如果路径中间的某个目录不存在,失败退出整个函数
}
else if (InsertObject==NULL)//if遍历完整个路径了
{
Status = STATUS_OBJECT_NAME_NOT_FOUND;
break;//如果找不到目标对象,又不要求插入新对象,失败返回整个函数(很常见)
}
//if用户要求:找不到目标对象就插入一个(这就体现了该函数的另一功能)
//先检查当前线程的令牌是否具有在这个目录中插入对象的权限
if (!ObCheckCreateObjectAccess(Directory,
ObjectType == ObDirectoryType ?
DIRECTORY_CREATE_SUBDIRECTORY :
DIRECTORY_CREATE_OBJECT,
AccessState,
&ComponentName)) //子对象名
{
break;//没有插入权限,失败返回整个函数
}
ObpInsertEntryDirectory(Directory,LookupContext,ObjectHeader);//插入目录中
ObjectHeader = OBJECT_TO_OBJECT_HEADER(InsertObject);
NewName = ExAllocatePoolWithTag(PagedPool,ComponentName.Length,tag);
ObjectNameInfo = OBJECT_HEADER_TO_NAME_INFO(ObjectHeader);
RtlCopyMemory(NewName,ComponentName.Buffer,ComponentName.Length);
if (ObjectNameInfo->Name.Buffer)//释放该对象原来的名字
ExFreePoolWithTag(ObjectNameInfo->Name.Buffer, OB_NAME_TAG );
ObjectNameInfo->Name.Buffer = NewName;//设置新插入对象的名字
ObjectNameInfo->Name.Length = ComponentName.Length;
ObjectNameInfo->Name.MaximumLength = ComponentName.Length;
Status = STATUS_SUCCESS;
Object = InsertObject;//成功返回
break;
}
ReparseObject://如果找到了剩余路径中的第一个子节点(那个节点在对象目录中存在)
ObjectHeader = OBJECT_TO_OBJECT_HEADER(Object);
ParseRoutine = ObjectHeader->Type->TypeInfo.ParseProcedure;
//if这个节点自带有一个解析函数(那肯定不是目录)
if ((ParseRoutine) && (!(InsertObject) || (ParseRoutine == ObpParseSymbolicLink)))
{
Directory = NULL;//不再有当前目录
InterlockedExchangeAdd(&ObjectHeader->PointerCount, 1);
if (ReferencedDirectory)
ReferencedDirectory = NULL;
if (ReferencedParentDirectory)
ReferencedParentDirectory = NULL;
//调用这个对象自带的解析函数
Status = ParseRoutine(Object,ObjectType,AccessState,AccessCheckMode,
Attributes,
IN、OUT ObjectName,//初始的全路径,可能传出新的初始路径
IN、OUT &RemainingName,//传入初始的剩余路径,传出最终的剩余路径
ParseContext,
&Object);
if ((Status == STATUS_REPARSE) || //要求从重根目录开始重新按新的初始路径解析
(Status == STATUS_REPARSE_OBJECT))//要求从指定对象开始重新解析剩余路径
{
if ((Status == STATUS_REPARSE_OBJECT) || (ObjectName->Buffer[0] == L”\\”))
{
RootHandle = NULL;
ParentDirectory = NULL;
RootDirectory = ObpRootDirectoryObject;
if (Status == STATUS_REPARSE_OBJECT)
{
Reparse = FALSE;//不用回到最外层的循环,直接从内层位置重新解析
goto ReparseObject;
}
else
{
Reparse = TRUE;//重新回到外层循环从根目录开始解析
SymLink = TRUE;
goto ParseFromRoot;
}
}
}
else if (!NT_SUCCESS(Status))
Object = NULL;
else if (Object==NULL)
Status = STATUS_OBJECT_NAME_NOT_FOUND;
break;//失败返回整个函数
}
Else //剩余路径中的第一个节点是个普通的目录或者是路径中的最后一个节点
{
if (RemainingName.Length==0)//若找到的是最后一个节点
{
Status = ObReferenceObjectByPointer(Object,ObjectType,AccessMode);
break;//哈哈,终于舒口气了。成功返回找到的这个对象(最典型的情形)
}
else
{
//若找到的是中间的某个普通目录,修改状态后,回到内层循环继续向后查找
if (ObjectHeader->Type == ObDirectoryType)
{
ReferencedParentDirectory = ReferencedDirectory;
ParentDirectory = Directory;
Directory = Object;//修改当前目录为新的目录
ReferencedDirectory = NULL;
}
Else//很少见
{
…
}
}
}
}
}
//下面返回找到的对象
*FoundObject = Object;
if (Object==NULL)
{
if ((Status == STATUS_REPARSE) || (NT_SUCCESS(Status)))
Status = STATUS_OBJECT_NAME_NOT_FOUND;//强制转为查找失败
}
return Status;
}
至此,对象目录的查找过程也详尽分析结束了。这个函数根据路径在对象目录中查找对象。如果找到了,就返回找到点额对象;如果找不到,并且用户不要求插入新对象在那儿,就返回失败,否则,插入用户指定的对象。(一个函数两种用途,后一个用途用于在创建对象时,把新创的有名对象插入对象目录)
注意:物理的磁盘卷设备对象、文件对象、注册表键对象以及符号链接对象都提供了自定义的名字解析函数。关于具体对象类型的解析,以后的篇章会逐渐讲到。
下面这个函数经常使用,但有诀窍.DDK文档中并未公开这个函数,但是这个函数实际上是导出的,声明一下就可以使用了,这个函数可以直接根据对象名,找到对应的对象(是不是很方便?)其内部原理就是调用上面的函数在对象目录中查找对象.( 这个函数实际上是导出的,虽然DDK中并未有这个函数)
NTSTATUS
ObReferenceObjectByName(IN PUNICODE_STRING ObjectPath,//对象的全路径(以\开头)
IN ULONG Attributes,//查找属性
IN PACCESS_STATE PassedAccessState,//传入的令牌和申请的权限,可选
IN ACCESS_MASK DesiredAccess,//申请的访问权限,与上面那个参数任选其一
IN POBJECT_TYPE ObjectType,//当AccessMode传KernelMode时,可忽略此参数
IN KPROCESSOR_MODE AccessMode,//驱动程序中一般传KernelMode
IN OUT PVOID ParseContext,//其它
OUT PVOID* ObjectPtr)//返回找到的对象
{
PVOID Object = NULL;
Status = ObpCaptureObjectName(&ObjectName, ObjectPath, AccessMode, TRUE);
if (!PassedAccessState)//如果用户没传这个参数,就创建
{
PassedAccessState = &AccessState;
//将当前线程的令牌以及申请的访问权限记录到AccessState中
Status = SeCreateAccessState(&AccessState,&AuxData,DesiredAccess,
&ObjectType->TypeInfo.GenericMapping);
}
*ObjectPtr = NULL;
Status = ObpLookupObjectName(NULL, //看到没,NULL表示根目录
&ObjectName,//绝对路径
Attributes,//查找属性
ObjectType,
AccessMode,
ParseContext,
PassedAccessState,//传入当前线程的令牌
&Context,
&Object);
if (NT_SUCCESS(Status))
{
if (ObpCheckObjectReference(Object,
PassedAccessState, //传入当前线程的令牌
AccessMode,&Status))
*ObjectPtr = Object;
}
return Status;
}
假如要找到名为即DeviceName等于”\Device\MyCdo”的设备对象
这个函数典型的调用方法示例为:
ObReferenceObjectByName(
&DeviceName,
OBJ_CASE_INSENSITIVE,//大小写木敏感
NULL, //PassedAccessState
FILE_ALL_ACCESS, //DesiredAccess
IoDeviceObjectType,//也可以直接传NULL
KernelMode,
NULL, //ParseContext
(PVOID*)&DeviceObject);//返回找到的设备对象
调用上面的那个函数后,可以根据对象名直接得到它的指针,得到指针后,我们还可以趁此打开该对象,以得到一个访问句柄(因为有的场合我们不能使用对象指针,只能使用句柄)。下面的函数就是这个用途
NTSTATUS
ObOpenObjectByPointer(IN PVOID Object,
IN ULONG HandleAttributes,
IN PACCESS_STATE PassedAccessState,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType,
IN KPROCESSOR_MODE AccessMode,
OUT PHANDLE Handle)
{
*Handle = NULL;
Status = ObReferenceObjectByPointer(Object,ObjectType,AccessMode);//递增引用计数
Header = OBJECT_TO_OBJECT_HEADER(Object);
if (!PassedAccessState)//如果用户没传入一个令牌,就创建一个
{
PassedAccessState = &AccessState;
Status = SeCreateAccessState(&AccessState,&AuxData,DesiredAccess,
&Header->Type->TypeInfo.GenericMapping);
}
if (Header->Type->TypeInfo.InvalidAttributes & HandleAttributes)//检查句柄属性是否支持
return STATUS_INVALID_PARAMETER;
//在句柄表中创建一个相应对象的句柄
Status = ObpCreateHandle(ObOpenHandle,//OpenReason
Object,
ObjectType,
PassedAccessState,
0,
HandleAttributes,
NULL,
AccessMode,
NULL,
Handle);//返回得到的句柄
return Status;
}
事实上,更多的场合是,我们有一个对象的名称,想要打开那个对象,一步得到它的访问句柄(比如OpenMutex)。因此,下面的函数就是用于这个常用目的。
NTSTATUS
ObOpenObjectByName(IN POBJECT_ATTRIBUTES ObjectAttributes,//对象的路径
IN POBJECT_TYPE ObjectType,
IN KPROCESSOR_MODE AccessMode,
IN PACCESS_STATE PassedAccessState,//传入的令牌
IN ACCESS_MASK DesiredAccess,//要求的访问权限
IN OUT PVOID ParseContext,
OUT PHANDLE Handle)//返回得到的句柄
{
PVOID Object = NULL;
*Handle = NULL;
TempBuffer = ExAllocatePoolWithTag(NonPagedPool,sizeof(OB_TEMP_BUFFER),tag);
Status = ObpCaptureObjectCreateInformation(ObjectAttributes,
AccessMode,
&TempBuffer->ObjectCreateInfo,
&ObjectName);
if (!PassedAccessState)
{
if (ObjectType) GenericMapping = &ObjectType->TypeInfo.GenericMapping;
PassedAccessState = &TempBuffer->LocalAccessState;
Status = SeCreateAccessState(&TempBuffer->LocalAccessState,&TempBuffer->AuxData,
DesiredAccess,GenericMapping);
}
//调用这个函数完成真正的查找工作
Status = ObpLookupObjectName(TempBuffer->ObjectCreateInfo.RootDirectory,
&ObjectName,
TempBuffer->ObjectCreateInfo.Attributes,
ObjectType,
AccessMode,
ParseContext,
NULL,
PassedAccessState,
&TempBuffer->LookupContext,
&Object);
ObjectHeader = OBJECT_TO_OBJECT_HEADER(Object);
if (ObjectHeader->Flags & OB_FLAG_CREATE_INFO)
{
OpenReason = ObCreateHandle;//说明对象正在创建过程中(也即创建对象时就打开一个句柄)
if (ObjectHeader->ObjectCreateInfo)
{
ObpFreeObjectCreateInformation(ObjectHeader->ObjectCreateInfo);
ObjectHeader->ObjectCreateInfo = NULL; //对象打开得到句柄后就不再需要保留创建信息
}
}
Else //典型情况:说明是对象创建完后的普通显式打开操作(非创建时打开)
OpenReason = ObOpenHandle;
if (ObjectHeader->Type->TypeInfo.InvalidAttributes &
TempBuffer->ObjectCreateInfo.Attributes)
{
Status = STATUS_INVALID_PARAMETER;
}
else
{
//在句柄表中创建句柄
Status = ObpCreateHandle(OpenReason,
Object,
ObjectType,
PassedAccessState,
0,
TempBuffer->ObjectCreateInfo.Attributes,
&TempBuffer->LookupContext,
AccessMode,
NULL,
Handle);
}
return Status;
}
下面是递减对象引用计数的函数:
LONG ObDereferenceObject(IN PVOID Object)
{
Header = OBJECT_TO_OBJECT_HEADER(Object);
if (Header->PointerCount < Header->HandleCount)
return Header->PointerCount;//引用计数必须>=句柄计数(因为句柄也是一种引用)
NewCount = InterlockedDecrement(&Header->PointerCount);//递减
if (NewCount==0)//引用计数递减到0后才删除对象
{
if (!KeAreAllApcsDisabled())
ObpDeleteObject(Object, FALSE);//立即删除该对象
else
ObpDeferObjectDeletion(Header);//让后台的系统工作线程延迟删除这个对象
}
return NewCount;//返回递减后的值
}
VOID ObpDeleteObject(IN PVOID Object, IN BOOLEAN CalledFromWorkerThread)
{
Header = OBJECT_TO_OBJECT_HEADER(Object);
ObjectType = Header->Type;
NameInfo = OBJECT_HEADER_TO_NAME_INFO(Header);
CreatorInfo = OBJECT_HEADER_TO_CREATOR_INFO(Header);
if ((CreatorInfo) && !(IsListEmpty(&CreatorInfo->TypeList)))
RemoveEntryList(&CreatorInfo->TypeList);//从对象类型的对象链表中移除该对象
if ((NameInfo) && (NameInfo->Name.Buffer))
ExFreePool(NameInfo->Name.Buffer);
if (Header->SecurityDescriptor) 。。。
if (ObjectType->TypeInfo.DeleteProcedure)//调用对象类型自身提供的删除函数
{
if (!CalledFromWorkerThread)
Header->Flags |= OB_FLAG_DEFER_DELETE;
ObjectType->TypeInfo.DeleteProcedure(Object);
}
ObpDeallocateObject(Object);//内部调用ExFreePool最终释放对象结构体本身
}
//以上两个函数说明:对象的引用计数一旦减到0,就释放对象及其相关其它结构,从系统中消失
句柄的遗传和复制:
说到句柄,请自觉建立起这样一个完整概念:“具有指定权限和属性的访问句柄”。一个句柄代表一次对对象的打开操作(句柄最终的目的是用来访问对象的,因此叫“访问句柄”)
句柄的权限指本次打开指定对象时,当初申请得到的访问权限,以后凡是使用基于这个句柄的操作都不能超出当初打开时申请得到的访问权限(如打开文件时申请的权限是读),那么使用那个hFile调用WriteFile,将拒绝访问,即使当前用户拥有对那个文件的写权限。
句柄的属性则有重要的一条:是否可继承给子进程。(句柄在打开时通过OBJ_INHERIT标志指示可否继承)
Win32 API:
BOOL CreateProcess(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles, //指是否继承父进程的句柄表中那些可继承的句柄
…
);
父进程句柄表中的每个可继承句柄 继承 到子进程句柄表中的对应索引位置,这样,父子进程就可以使用同一个句柄值,找到同一个内核对象。这就是“继承”一词的由来。我们知道,各个句柄(除开内核型句柄)在各个进程中不能通用,但一旦继承给子进程后,就可以通用了。下面这个函数用来给子进程创建一个句柄表,并从父进程中复制句柄表项,达到继承。
PHANDLE_TABLE
ExDupHandleTable(IN PEPROCESS Process,//子进程
IN PHANDLE_TABLE HandleTable,//父进程的句柄表
IN PEX_DUPLICATE_HANDLE_CALLBACK DupHandleProcedure)//辅助回调函数
{
BOOLEAN Failed = FALSE;
NewTable = ExpAllocateHandleTable(Process, FALSE);//句柄表描述符结构体
while (NewTable->NextHandleNeedingPool < HandleTable->NextHandleNeedingPool)
ExpAllocateHandleTableEntrySlow(NewTable, FALSE);//分配与父句柄表对应大小的内存
NewTable->HandleCount = 0;//有效句柄个数
NewTable->FirstFree = 0;//第一个空闲表项的位置
Handle.Value = SizeOfHandle(1);//索引位置
//遍历父句柄表中的每个表项,复制到子句柄表
while ((NewEntry = ExpLookupHandleTableEntry(NewTable, Handle)))
{
HandleTableEntry = ExpLookupHandleTableEntry(HandleTable, Handle);//得到父表项
do
{
if (!(HandleTableEntry->Value & OBJ_INHERIT))//if 该父句柄不可继承
Failed = TRUE;
else//if 该父句柄可继承
{
if (!ExpLockHandleTableEntry(HandleTable, HandleTableEntry))
Failed = TRUE;//句柄表锁定失败,不管他
else
{
//检查是否可复制,若可以,就递增对象的句柄计数、引用计数,然后复制句柄项
if (DupHandleProcedure(Process,HandleTable,HandleTableEntry,NewEntry))
{
*NewEntry = *HandleTableEntry;//关键。完完全全复制句柄表项
Failed = FALSE;
NewEntry->Value |= EXHANDLE_TABLE_ENTRY_LOCK_BIT;
NewTable->HandleCount++;
}
else
Failed = TRUE;
}
}
if (Failed)//若不可复制
{
NewEntry->Object = NULL;//标记为一个无效的句柄
NewEntry->NextFreeTableEntry = NewTable->FirstFree;
NewTable->FirstFree = Handle.Value;
}
Handle.Value += SizeOfHandle(1);
NewEntry++;//下一个句柄
HandleTableEntry++;//下一个句柄
} while (Handle.Value % SizeOfHandle(LOW_LEVEL_ENTRIES));
Handle.Value += SizeOfHandle(1);
}
//将句柄表插入到全局链表中(这也是一个检测隐藏的方法)
InsertTailList(&HandleTableListHead, &NewTable->HandleTableList);
return NewTable;//返回给子进程创建的句柄表
}
除了通过继承方式可以将句柄复制到子进程的句柄表中外,Windows提供了另一个API:DuplicateHandle,
可以将任意进程的任意句柄复制到其他任意进程的句柄表中,其原理是差不多的,在此不再解释。
通过句柄继承与句柄复制,两个进程就可以共享同一句柄(使用 句柄访问同一内核对象)了。
句柄的关闭:
Win32 API CloseHandle、closesocket会在内部调用NtClose,最终会调用到下面的内核函数来关闭句柄。
NTSTATUS ObpCloseHandle(IN HANDLE Handle,IN KPROCESSOR_MODE AccessMode)
{
BOOLEAN AttachedToProcess = FALSE;
PEPROCESS Process = PsGetCurrentProcess();
if (ObIsKernelHandle(Handle, AccessMode))//内核句柄(即“system”进程中的句柄)
{
HandleTable = ObpKernelHandleTable;
Handle = ObKernelHandleToHandle(Handle);
if (Process != PsInitialSystemProcess)
{
KeStackAttachProcess(&PsInitialSystemProcess->Pcb, &ApcState);//挂靠到system进程
AttachedToProcess = TRUE;
}
}
else
HandleTable = Process->ObjectTable;//典型,使用当前进程的句柄
HandleTableEntry = ExMapHandleToPointer(HandleTable, Handle);//得到对应的句柄表项
if (HandleTableEntry)
{
//调用下面的这个函数真正完成句柄关闭工作
Status = ObpCloseHandleTableEntry(HandleTable,HandleTableEntry,Handle,AccessMode);
if (AttachedToProcess) KeUnstackDetachProcess(&ApcState);
Status = STATUS_SUCCESS;
}
else
{
if (AttachedToProcess) KeUnstackDetachProcess(&ApcState);
Status = STATUS_INVALID_HANDLE;//返回无效句柄错误
}
return Status;
}
实际的句柄关闭工作是下面这个函数ObpCloseHandleTableEntry,请详细看
NTSTATUS
ObpCloseHandleTableEntry(IN PHANDLE_TABLE HandleTable,
IN PHANDLE_TABLE_ENTRY HandleEntry,
IN HANDLE Handle,
IN KPROCESSOR_MODE AccessMode)
{
ObjectHeader = ObpGetHandleObject(HandleEntry);
ObjectType = ObjectHeader->Type;
Body = &ObjectHeader->Body;
GrantedAccess = HandleEntry->GrantedAccess;
//关闭句柄前,检查对象的当前状态是否可以关闭一个句柄
if (ObjectType->TypeInfo.OkayToCloseProcedure)
{
if (!ObjectType->TypeInfo.OkayToCloseProcedure(CurProcess,Body,Handle,AccessMode))
return STATUS_HANDLE_NOT_CLOSABLE;
}
//再检查句柄本身是否可以关闭
if ( HandleEntry->ObAttributes & OBJ_PROTECT_CLOSE )
return STATUS_HANDLE_NOT_CLOSABLE;
ExDestroyHandle(HandleTable, Handle, HandleEntry);//关键,将对应位置处的句柄标记为空闲
ObpDecrementHandleCount(Body,CurProcess,GrantedAccess,ObjectType);//递减总的句柄计数
ObDereferenceObject(Body);//同时递减一个引用计数(句柄本身也占据一个引用)
return STATUS_SUCCESS;
}
VOID
ObpDecrementHandleCount(IN PVOID ObjectBody,IN PEPROCESS Process,
IN ACCESS_MASK GrantedAccess,IN POBJECT_TYPE ObjectType)
{
ObjectHeader = OBJECT_TO_OBJECT_HEADER(ObjectBody);
OldHandleCount = ObjectHeader->HandleCount;
NewCount = InterlockedDecrement(&ObjectHeader->HandleCount);//递减句柄计数
ProcessHandleCount = 当前进程对该对象持有的句柄计数;
if (ObjectType->TypeInfo.MaintainHandleCount) { 递减对应进程对该对象的句柄计数; }
//每次关闭句柄时都会调用对象类型注册的句柄关闭时函数,如文件对象类提供了IopCloseFile
if (ObjectType->TypeInfo.CloseProcedure)
ObjectType->TypeInfo.CloseProcedure(Process,ObjectBody,GrantedAccess,
ProcessHandleCount,OldHandleCount);
ObpDeleteNameCheck(ObjectBody);//这个函数在非永久对象的句柄一关完后,就把对象移出对象目录
InterlockedDecrement((PLONG)&ObjectType->TotalNumberOfHandles);
}
如上:每次关闭前都会检查句柄是否可以关闭,然后关闭句柄,调用注册的句柄关闭时函数。文件对象类自己注册了一个句柄关闭函数IopCloseFile,文件句柄在关完后的工作如下:
VOID
IopCloseFile(IN PEPROCESS Process OPTIONAL,IN PVOID ObjectBody,IN ACCESS_MASK GrantedAccess,
IN ULONG HandleCount,//关闭句柄前,当前进程对该对象持有的句柄计数
IN ULONG SystemHandleCount)// 关闭句柄前,该对象总的句柄计数
{
PFILE_OBJECT FileObject = (PFILE_OBJECT)ObjectBody;
if (HandleCount != 1) return;
if (SystemHandleCount != 1) return;
//上面两条语句说明,文件对象只有在所有句柄都关完后才做下面的附加工作
if (FileObject->Flags & FO_DIRECT_DEVICE_OPEN)
DeviceObject = IoGetAttachedDevice(FileObject->DeviceObject);//物理磁盘卷设备
else
DeviceObject = IoGetRelatedDeviceObject(FileObject);//文件系统中的卷设备
FileObject->Flags |= FO_HANDLE_CREATED;
if (FileObject->Flags & FO_SYNCHRONOUS_IO) IopLockFileObject(FileObject);
KeClearEvent(&FileObject->Event);
KeInitializeEvent(&Event, SynchronizationEvent, FALSE);
Irp = IoAllocateIrp(DeviceObject->StackSize, FALSE);
Irp->UserEvent = &Event;
Irp->UserIosb = &Irp->IoStatus;
Irp->Tail.Overlay.Thread = PsGetCurrentThread();
Irp->Tail.Overlay.OriginalFileObject = FileObject;
Irp->Overlay.AsynchronousParameters.UserApcRoutine = NULL;
Irp->Flags = IRP_CLOSE_OPERATION | IRP_SYNCHRONOUS_API;
StackPtr = IoGetNextIrpStackLocation(Irp);
StackPtr->MajorFunction = IRP_MJ_CLEANUP;//关键
StackPtr->FileObject = FileObject;
IopQueueIrpToThread(Irp);//将该irp挂入线程的irp队列
Status = IoCallDriver(DeviceObject, Irp);//发出irp
if (Status == STATUS_PENDING)
KeWaitForSingleObject(&Event, UserRequest, KernelMode, FALSE, NULL);
KeRaiseIrql(APC_LEVEL, &OldIrql);
IopUnQueueIrpFromThread(Irp);
KeLowerIrql(OldIrql);
IoFreeIrp(Irp);
if (FileObject->Flags & FO_SYNCHRONOUS_IO) IopUnlockFileObject(FileObject);
}
如上:在文件对象的所有句柄关闭了时,系统会生成一个IRP_MJ_CLEANUP irp。另外:文件对象在引用计数递减到0后,在最后销毁前,会生成IRP_MJ_CLOSE irp。简单一句话【柄完清理、引完关闭】,这两个irp的产生时机是非常重要的。