对象,即Object。广义的说,对象就是“目标”,行为的受体,所以,任何客观的事物从广义上讲都可以成为对象。在计算机领域,对象是个专有名词。其定义是:一个(一组)数据结构,以及定义在其上的操作,也就是数据结构 +操作,这个跟我们程序设计的思想是一致的,所谓“基于对象的程序设计”和“面向对象的程序设计”的概念都是基于此的。Windows把文件和设备都看做特殊对象。 同样,我们将空间分成用户空间和系统空间,自然也就有内核对象(存在于内核之中)和用户对象(用户空间的)。从操作系统的角度来看,我们关心的当然是内核对象,并且如果一个内核对象只是用于内核本身,而不向用户空间开放,对于软件而言,那自然是没有多大意义的,所以我们关心的是对用户程序开放的,可以为用户程序所用的内核对象,当然,我们使用内核对象的媒介自然是系统调用,也即向用户开放的本质,是开放相应的系统调用接口。 常见的对象类型,以及对应的操作 这些对象类型大都为对象的创建和打开配备了专门的系统调用。例如对象类型Key,就有NtCreateKey和NtOpenKey。但是对于NamedPipe类型,却又有些特殊,这个对象类型有专门的“创建”系统调用NtCreateNamedPipe,却没有专门“打开”的系统调用,而只是借用了NtOpenFile,读/写则借用了NtReadFile和NtWriteFile。除了NamedPipe还有很多对象,例如Device,Driver,等用于设备驱动的对象类型,这些对象类型也没有专门的系统调用,而是借用了NtOpenFile作为创建/打开的方法。 事实上,NtOpenFile是创建/打开对象的通用方法,凡是没有专门为其配备系统调用的对象类型,就都以NtOpenFile作为创建/打开的方法,并借用NtReadFile,NtWriteFile等本来用于文件操作的方法用来操作这些对象类型。 并且,不管是什么方法创建/打开的对象,关闭对象的方法都是NtClose,当然,尽管都用NtClose,但是在内核中实际执行的函数自然会因为对象类型的不一致而执行不同的函数。 对于几乎所有的对象,Windows内核都提供了一个统一的操作模式,就是先通过系统调用“打开”或创建目标对象,让当前进程与目标对象之间建立起连接,当然,在打开对象的时候要指定对象的类别,另一方面说明怎样找到具体的对象,即提供目标对象的**“路径名”。然后通过别的系统调用进行操作,最后通过系统调用"关闭"对象解除当前进程与目标对象的连接关系**。 建立连接是维持对于目标对象进行操作的上下文所必须的。在Windows的对象管理机制中,除少数例外以外,对于几乎所有对象的操作都是遵循先打开后操作最后关闭的统一模式。 内核在为当前进程打开目标对象后,会返回一个叫“Handle”的识别号,作为后续操作的依据。Windows内核中“对象”实质上就是数据结构,就是一些带有“对象头”(Object Head)的特殊数据结构。 任何对象的数据结构都由对象头和具体对象类型这两部分构成(大方向上)。但是,由于对象头的结构较为特殊,其中有些成分是可选的,所以实际上分成三部分。
其一是对象头OBJECT_HEAD数据结构其二是具体对象类型的数据结构本身,根据对象类型本身的不同,数据结构不同,例如KEVENT,FILE_OBJECT最后是几个作为可选项的附加信息(也就是所谓的可选成分),包括OBJECT_HEADER_CREATOR_INFO(关于创建者的信息,用来将所创建的对象挂入其创建者的对象队列),OBJECT_HEADER_NAME_INFO(载有对象名和目录节点指针),OBJECT_HEADER_HANDLE_INFO(关于句柄的信息),关于耗用内存配额的信息 三个部分按特殊的方式连在一片。按地址由低到高,首先当然是OBJECT_HEADER,它的上方(即高地址处)是具体对象类型的数据结构;附加信息是在OBJECT_HEADER的下面,即低地址处。先看下对象头的数据结构
// // Object Header // 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;Body显然是具体的对象的数据结构本身,其类型是QUAD,但事实上是不同的对象自然有不同的数据结构,所以它只是相当于一个占位置的,告诉我们Body在OBJECT_HEADER之后。NameInfoOffset,HandleInfoOffset, QuotaInfoOffset他们的类型是UCHAR,不难看出他们的位移是有限的,而ObjectCreateInfo和SecurityDescriptor相比之下则是指针,是独立存在的。 这样依次是配额信息,句柄信息,对象名信息,创建者信息,对象头信息,对象主体信息。因为附加信息都是可选的,而具体的数据结构大小又不定,将NAME_INFO等紧贴OBJECT_HEADER下面,8位字节就可以指定它的偏移了。注意的是,在对象头信息中没有说明Creator_Info的偏移,因为它的偏移根据OBJECT_HEADER-sizeof(OBJECT_HEADER_CREATOR_INFO),创建者信息的大小是固定的,一定是sizeof(OBJECT_HEADER_CREATOR_INFO)。
一些针对于获取该结构指定偏移的宏
#define OBJECT_TO_OBJECT_HEADER(o) \ //根据具体对象的数据结构,转换到OBJECT_HEADER处 CONTAINING_RECORD是根据第二个结构体和第三个结构体的一个成员以及作为第一个参数的该成员的真实地址,返回指向该结构体的一个指针 CONTAINING_RECORD((o), OBJECT_HEADER, Body) #define OBJECT_HEADER_TO_NAME_INFO(h) \ //根据NameInOffset是否为空决定是否能找到该偏移 找到对象名结构信息 ((POBJECT_HEADER_NAME_INFO)(!(h)->NameInfoOffset ? \ NULL: ((PCHAR)(h) - (h)->NameInfoOffset))) #define OBJECT_HEADER_TO_HANDLE_INFO(h) \ //根据Handle偏移是否为空决定是否能找到该对象的句柄结构信息 ((POBJECT_HEADER_HANDLE_INFO)(!(h)->HandleInfoOffset ? \ NULL: ((PCHAR)(h) - (h)->HandleInfoOffset))) #define OBJECT_HEADER_TO_QUOTA_INFO(h) \ //根据内存配额信息获取配额结构信息 ((POBJECT_HEADER_QUOTA_INFO)(!(h)->QuotaInfoOffset ? \ NULL: ((PCHAR)(h) - (h)->QuotaInfoOffset))) #define OBJECT_HEADER_TO_CREATOR_INFO(h) \ //传入对象头信息,返回对象创建者信息 这个有点特殊 因为需要对象头Flag中有 ((POBJECT_HEADER_CREATOR_INFO)(!((h)->Flags & \ OB_FLAG_CREATOR_INFO) ? NULL: ((PCHAR)(h) - \ sizeof(OBJECT_HEADER_CREATOR_INFO)))) #define OBJECT_HEADER_TO_EXCLUSIVE_PROCESS(h) \ ((!((h)->Flags & OB_FLAG_EXCLUSIVE)) ? \ NULL: (((POBJECT_HEADER_QUOTA_INFO)((PCHAR)(h) - \ (h)->QuotaInfoOffset))->ExclusiveProcess))创建一个对象并返回句柄后,创建该对象的进程就可以通过使用句柄访问它,这样对象可以是无名对象,但是在许多情况下对象名是必要的。
一个进程创建一个对象后,别的进程需要共享这个对象。而句柄是针对于特定进程而言的,所以别的进程就需要对象名来打开同一对象了。同一个进程可能需要有访问同一个的对象的多个上下文(例如文件的读/写位置)。所谓“创建”对象,是在创建一个目标对象的同时创建一个上下文,此后每次需要一个新的上下文的时候,就需要再次“打开”同一对象,此时往往也需要对象名。有些对象的内容是永久性的,例如磁盘上的文件,这次写入的可能要等将来的某个时候读出,而句柄显然不具备永久性,所以对象名显然是有必要的。由于对象名在大多数情况下是必要的,所以需要对 对象名加以组织和管理。这就得需要对象目录,显然,只有有命名的对象才可以进入对象目录。 对象目录是有多个节点连接而成的树状结构,树的root是一个“目录”对象,即类型为OBJECT_DIRECTORY的对象。树中的每个节点都是一个对象,节点名就是对象名。除根结点之外,树中的中间节点都必须是目录对象或符号链接对象(符号链接对象的类型OBJECT_SYMBOLIC_LINK),而普通对象只能成为叶节点。对于对象目录中的任何节点,如果我们从根节点或某个中间节点出发,记下沿途的各个节点的节点名,并以分隔符“\”加以分割,就形成了一个“路径名”。若路径名的第一个节点是根结点,就是全路径名/绝对路径名,若是相对于某个中间节点而出发的,则是相对路径名。根节点的节点名是“\”,内核中对应的全局指针ObpRootDirectoryObject指向的就是根节点的数据结构。
看下目录节点对象的数据结构
typedef struct _OBJECT_DIRECTORY { struct _OBJECT_DIRECTORY_ENTRY *HashBuckets[NUMBER_HASH_BUCKETS]; #if (NTDDI_VERSION < NTDDI_WINXP) ERESOURCE Lock; #else EX_PUSH_LOCK Lock; #endif #if (NTDDI_VERSION < NTDDI_WINXP) BOOLEAN CurrentEntryValid; #else struct _DEVICE_MAP *DeviceMap; #endif ULONG SessionId; #if (NTDDI_VERSION == NTDDI_WINXP) USHORT Reserved; USHORT SymbolicLinkUsageCount; #endif } OBJECT_DIRECTORY, *POBJECT_DIRECTORY;我们这里只关心第一个成员,它的数据结构是OBJECT_DIRECTORY_ENTRY *,这是个结构指针数组.是个散列表,数组中每个指针都可以用来维持一个"对象目录项"即OBJECT_DIRECTORY_ENTRY结构的队列.目录项结构本身并非对象,但是除根节点以外的节点都要靠目录项结构才能插入目录,所以这种结构起着类似于螺丝钉的作用.
typedef struct _OBJECT_DIRECTORY_ENTRY { struct _OBJECT_DIRECTORY_ENTRY *ChainLink; PVOID Object; #if (NTDDI_VERSION >= NTDDI_WS03) ULONG HashValue; #endif } OBJECT_DIRECTORY_ENTRY, *POBJECT_DIRECTORY_ENTRY;指针ChainLink用来构成队列,指针Object指向其所连接的对象. 除了根目录节点外,对象中的每个节点必须挂在某个目录节点的某个散列队列中.具体挂在哪一个队列取决于节点名(对象名)的哈希值。
所以总结一下,OBJECT_DIRECTORY中有n个OBJECT_DIRECTORY_ENTRY队列,具体挂在哪一个队列是由对象名的哈希值决定,OBJECT_DIRECTORY中的OBJECT_DIRECTORY_ENTRY队列中,每一个目录项可指向一个结点,也就是说OBJECT_DIRECTORY_ENTRY起着一个中转的作用,也就是螺丝钉的作用,也就意味着任何节点是挂靠在目录项中,目录项既可以指定一个具体的对象,自然也可以指定一个OBJECT_DIRECTORY,而该对象目录又会派生其他的OBJECT_DIRECTORY_ENTRY。这就是对象名所构成的树型结构。 可以这么理解,每一个目录结点是纵向连接,而目录项是横向连接的(队列)。 只有两种对象可以充任中间节点,一种是对象目录,另一种是符号链接,符号链接使用全路径来链接到某个对象。符号链接的对象可以是目录节点,也可以是叶节点,或者是另一个符号链接节点。所以,从某种意义上而言,符号链接的作用是为一个路径名起了一个别名。 例如此图/M的别名,及符号链接名是/J/Y。
为了更好的理解对象目录的结构,我们看一下ObpLookupEntryDirectory。通过一个给定的节点名Name,这个函数在给定的Directory中寻找同名的节点。
/*++ * @name ObpLookupEntryDirectory * * The ObpLookupEntryDirectory routine <FILLMEIN>. * * @param Directory * <FILLMEIN>. * * @param Name * <FILLMEIN>. * * @param Attributes * <FILLMEIN>. * * @param SearchShadow * <FILLMEIN>. * * @param Context * <FILLMEIN>. * * @return Pointer to the object which was found, or NULL otherwise. * * @remarks None. * *--*/ PVOID NTAPI ObpLookupEntryDirectory(IN POBJECT_DIRECTORY Directory, IN PUNICODE_STRING Name, IN ULONG Attributes, IN UCHAR SearchShadow, IN POBP_LOOKUP_CONTEXT Context) { BOOLEAN CaseInsensitive = FALSE; POBJECT_HEADER_NAME_INFO HeaderNameInfo; POBJECT_HEADER ObjectHeader; ULONG HashValue; ULONG HashIndex; LONG TotalChars; WCHAR CurrentChar; POBJECT_DIRECTORY_ENTRY *AllocatedEntry; POBJECT_DIRECTORY_ENTRY *LookupBucket; POBJECT_DIRECTORY_ENTRY CurrentEntry; PVOID FoundObject = NULL; PWSTR Buffer; PAGED_CODE(); /* Check if we should search the shadow directory */ if (!ObpLUIDDeviceMapsEnabled) SearchShadow = FALSE; /* Fail if we don't have a directory or name */ if (!(Directory) || !(Name)) goto Quickie; /* Get name information */ TotalChars = Name->Length / sizeof(WCHAR); //计算出结点名长度 Buffer = Name->Buffer; /* Set up case-sensitivity */ if (Attributes & OBJ_CASE_INSENSITIVE) CaseInsensitive = TRUE; //若开启了大小写敏感 /* Fail if the name is empty */ if (!(Buffer) || !(TotalChars)) goto Quickie; /* Create the Hash */ for (HashValue = 0; TotalChars; TotalChars--) //计算Name的哈希值 { /* Go to the next Character */ CurrentChar = *Buffer++; /* Prepare the Hash */ HashValue += (HashValue << 1) + (HashValue >> 1); /* Create the rest based on the name */ if (CurrentChar < 'a') HashValue += CurrentChar; else if (CurrentChar > 'z') HashValue += RtlUpcaseUnicodeChar(CurrentChar); else HashValue += (CurrentChar - ('a'-'A')); } /* Merge it with our number of hash buckets */ HashIndex = HashValue % 37; //这里也就是NUMBER_HASH_BUCKETS /* Save the result */ Context->HashValue = HashValue; //保存Name哈希值和对应的OBJECT_DIRECTORY_ENTRY的队列index Context->HashIndex = (USHORT)HashIndex; /* Get the root entry and set it as our lookup bucket */ AllocatedEntry = &Directory->HashBuckets[HashIndex];//获取对应目录中要查找的目录项队列 LookupBucket = AllocatedEntry; /* Check if the directory is already locked */ if (!Context->DirectoryLocked) { /* Lock it */ ObpAcquireDirectoryLockShared(Directory, Context); } /* Start looping */ while ((CurrentEntry = *AllocatedEntry)) //CurrentEntry是当前的哈希之后的目录项队列 { /* Do the hashes match? */ if (CurrentEntry->HashValue == HashValue)//判断我们要查找的节点名跟该目录项对应的具体对象的对象名的哈希值是否匹配 { /* Make sure that it has a name */ ObjectHeader = OBJECT_TO_OBJECT_HEADER(CurrentEntry->Object);//CurrentEntry->Object显然指向的是Body,根据Body可以找到对象头 /* Get the name information */ ASSERT(ObjectHeader->NameInfoOffset != 0); HeaderNameInfo = OBJECT_HEADER_TO_NAME_INFO(ObjectHeader); //从对象头中获取NameInfo /* Do the names match? */ if ((Name->Length == HeaderNameInfo->Name.Length) && //若长度相等并且Name一致 表明找到了一个所匹配的节点 (RtlEqualUnicodeString(Name, &HeaderNameInfo->Name, CaseInsensitive))) { break; } } /* Move to the next entry */ AllocatedEntry = &CurrentEntry->ChainLink;//若不匹配 则移到下一个目录项 } /* Check if we still have an entry */ if (CurrentEntry) { //CurrentEntry非0,说明找到了目标 /* Set this entry as the first, to speed up incoming insertion */ if (AllocatedEntry != LookupBucket)//目标节点不是第一个节点 注意 AllocatedEntry是匹配的目录项 LookupBucket是该目录项队列的第一个节点 { /* Check if the directory was locked or convert the lock */ if ((Context->DirectoryLocked) || (ExConvertPushLockSharedToExclusive(&Directory->Lock)))//相当于把查找到的节点放到开头 用于下次加快搜索速度 { /* Set the Current Entry */ *AllocatedEntry = CurrentEntry->ChainLink;//之后的目录项队列覆盖当前目录项 也就相当于让前一个POBJECT_DIRECTORY_ENTRY指针指向了查找到的元素的后一个 /* Link to the old Hash Entry */ CurrentEntry->ChainLink = *LookupBucket;//将第一个目录项放到查找到的目录项的下一个 /* Set the new Hash Entry */ *LookupBucket = CurrentEntry;//将当前目录项设置为开头 } } /* Save the found object */ FoundObject = CurrentEntry->Object;//保存找到的对象 goto Quickie; } else { /* Check if the directory was locked */ if (!Context->DirectoryLocked) { /* Release the lock */ ObpReleaseDirectoryLock(Directory, Context); } /* Check if we should scan the shadow directory */ if ((SearchShadow) && (Directory->DeviceMap)) { /* FIXME: We don't support this yet */ KEBUGCHECK(0); } } Quickie: /* Check if we inserted an object */ if (FoundObject) //如果找到了匹配对象名的对象 { /* Get the object name information */ ObjectHeader = OBJECT_TO_OBJECT_HEADER(FoundObject);//获取对象头 ObpAcquireNameInformation(ObjectHeader);//获取命名信息 /* Reference the object being looked up */ ObReferenceObject(FoundObject); //引用目标对象 /* Check if the directory was locked */ if (!Context->DirectoryLocked) { /* Release the lock */ ObpReleaseDirectoryLock(Directory, Context); } } /* Check if we found an object already */ if (Context->Object)//若不为空 这说明上下文保存的对象是之前曾用过的对象 对它进行解除引用 { /* We already did a lookup, so remove this object's query reference */ ObjectHeader = OBJECT_TO_OBJECT_HEADER(Context->Object); HeaderNameInfo = OBJECT_HEADER_TO_NAME_INFO(ObjectHeader); ObpReleaseNameInformation(HeaderNameInfo); /* Also dereference the object itself */ ObDereferenceObject(Context->Object); } /* Return the object we found */ Context->Object = FoundObject;//在上下文中保存新的被引用的对象 return FoundObject; //返回所找到的对象 }这个函数在给定的目录中查找该目录中的所有目录项,其函数流程非常清晰
首先根据该对象名获取对应的HashValue和HashIndex(根据HashValue % NUMBER_OF_HASHBUCKETS而来),并保存在上下文Context中根据HashIndex找到节点目录中对应的HashBucket队列,从头开始遍历,通过对每一个节点项的HashValue跟所要找的Name生成的HashValue 进行一次匹配,看哈希值是否相等,若哈希值相等,则再次前提下进行二进制比较,比较逐一字符是否一致。若找到,则保存起来了,并且会将该目录项移到该目录项队列的开头,用于在下次搜索时加快搜索速度。若找到该对象,对该对象的引用计数加一,并将该对象和该对象的命名信息保存在上下文Context中,若该Context中有过对象,则先进行对之前对象和该对象命名信息的一个清除。下面以NtCreateTimer为例,粗略看下对象创建的过程。许多用于对象创建的系统调用都有大致相同的程序结构,只是具体的对象类型不同。
NTSTATUS NTAPI NtCreateTimer(OUT PHANDLE TimerHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN TIMER_TYPE TimerType) { PETIMER Timer; HANDLE hTimer; KPROCESSOR_MODE PreviousMode = ExGetPreviousMode(); NTSTATUS Status = STATUS_SUCCESS; PAGED_CODE(); /* Check for correct timer type */ if ((TimerType != NotificationTimer) && //Timer只有这两种,若参数值不是这些 则说明有问题 (TimerType != SynchronizationTimer)) { /* Fail */ return STATUS_INVALID_PARAMETER_4; } /* Check Parameter Validity */ if (PreviousMode != KernelMode) { _SEH_TRY { ProbeForWriteHandle(TimerHandle); } _SEH_EXCEPT(_SEH_ExSystemExceptionFilter) { Status = _SEH_GetExceptionCode(); } _SEH_END; if(!NT_SUCCESS(Status)) return Status; } /* Create the Object */ Status = ObCreateObject(PreviousMode, //创建Timer对象 ExTimerType, ObjectAttributes, PreviousMode, NULL, sizeof(ETIMER), 0, 0, (PVOID*)&Timer); if (NT_SUCCESS(Status)) //若成功 在这里会进行适当的初始化 { /* Initialize the DPC */ KeInitializeDpc(&Timer->TimerDpc, ExpTimerDpcRoutine, Timer); /* Initialize the Kernel Timer */ KeInitializeTimerEx(&Timer->KeTimer, TimerType); /* Initialize the timer fields */ KeInitializeSpinLock(&Timer->Lock); Timer->ApcAssociated = FALSE; Timer->WakeTimer = FALSE; //目标对象本身的初始化 Timer->WakeTimerListEntry.Flink = NULL; /* Insert the Timer */ Status = ObInsertObject((PVOID)Timer, //将所创建的对象插入对象目录以及当前进程的句柄表 NULL, DesiredAccess, 0, NULL, &hTimer); /* Check for success */ if (NT_SUCCESS(Status)) { /* Make sure it's safe to write to the handle */ _SEH_TRY { *TimerHandle = hTimer; //将获得的句柄作为结果返回 } _SEH_EXCEPT(_SEH_ExSystemExceptionFilter) { } _SEH_END; } } /* Return to Caller */ return Status; }流程大概是 对该对象类型参数的一些校验->ObCreateObject(当然是针对不同的对象类型创建不同的对象)->初始化->ObInsertObject(将创建的对象插入对象目录和该进程的句柄表)->将上一步获得的句柄作为返回值返回给用户进程
对象是分类的,也就是说对象是以类型作为区分,内核函数可以向已有的对象类型集合中添加新的类型,一旦增加了新的类型,用户可以通过相应的创建/打开操作建立进程与该对象的联系。用户从用户空间自然无法向内核安装新的对象类型,但是用户可以把内核模块,即sys模块动态安装到内核中,比如我们写的驱动就可以被安装到内核,所以可以自定义用户的对象类型。
Windows内核为新对象的类型的定义提供了一个全局的OBJECT_TYPE_INITIALIZER,作为需要填写和递交的“申请单”。
// // Object Type Initialize for ObCreateObjectType // 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; OB_OKAYTOCLOSE_METHOD OkayToCloseProcedure; } OBJECT_TYPE_INITIALIZER, *POBJECT_TYPE_INITIALIZER;字段Length描述目标数据结构的长度,OpenProcedure,CloseProcedure…等均为函数指针,是为目标对象类型所定义的操作。其中ParseProcedure提供了解析路径名以找到目标对象的方法。一般对象的ParseProcedure都是很简单的,因为是从对象目录中寻找目标对象的,但是文件对象的ParseProcedure是个特例,因为搜寻文件对象跟文件系统挂钩,需要转入文件目录查找。 当需要创建一种新的对象类型时,就填写好一个OBJECT_TYPE_INITIALIZER数据结构,然后调用ObCreateObjectTYpe,这个函数根据我们填写的数据结构,创建新的对象类型的数据结构,然后将其挂入对象目录。对象类型的数据结构是OBJECT_TYPE(在对象头中有指向OBJECT_TYPE的指针),内部有个字段TypeInfo也是OBJECT_TYPE_INITIALIZER类型,我们提交的“申请单”会被复制到这里。
再次通过一个定时器的实例,说明对象类型的创建。
VOID INIT_FUNCTION NTAPI //NTAPI表示是内核导出的函数 即可在安装模块调用的函数 ExpInitializeTimerImplementation(VOID) { OBJECT_TYPE_INITIALIZER ObjectTypeInitializer; UNICODE_STRING Name; /* Create the Timer Object Type */ RtlZeroMemory(&ObjectTypeInitializer, sizeof(ObjectTypeInitializer)); //对 对象类型初始化结构清零 RtlInitUnicodeString(&Name, L"Timer"); ObjectTypeInitializer.Length = sizeof(ObjectTypeInitializer); //该结构的大小 ObjectTypeInitializer.InvalidAttributes = OBJ_OPENLINK; ObjectTypeInitializer.DefaultNonPagedPoolCharge = sizeof(ETIMER); //消耗的物理内存 ObjectTypeInitializer.GenericMapping = ExpTimerMapping; //用于访问控制 ObjectTypeInitializer.PoolType = NonPagedPool; //为该数据结构分配缓冲区时用不可置换池 ObjectTypeInitializer.ValidAccessMask = TIMER_ALL_ACCESS; ObjectTypeInitializer.DeleteProcedure = ExpDeleteTimer; //删除定时器需要的函数 ObCreateObjectType(&Name, &ObjectTypeInitializer, NULL, &ExTimerType); //创建对象类型 /* Initialize the Wait List and Lock */ KeInitializeSpinLock(&ExpWakeListLock); InitializeListHead(&ExpWakeList); }这里也就是填写好一个OBJECT_TYPE_INITIALIZER,然后对该申请单填写必要的字段,然后交给ObCreateObjectType,这里值得注意的是,ExTimerType是个全局的OBJECT_TYPE结构指针,用来返回所创建的数据结构。这个数据结构是定时器这个对象类型的定义,以后创建定时器对象即类型为ETIMER的对象时,就要以该指针作为参数,以便快速找到这个类型的定义。
这段有点长,我们来梳理一下
首先ObCreateObjectType对传过来的 对象类型名 和 创建对象类型的表单进行了一个严格的校验类比于 对象目录有一个指向根目录的指针 , 对象类型也有一个目录 ObpTypeDirectoryObject就是指向这个数据目录的指针,对象类型目录有些特殊,它是单层的,也就意味着只有根目录是目录结构,其他都是叶子结点,并且每个叶子结点代表着一个对象类型的数据结构,也就是OBJECT_TYPE数据结构,同时 对象类型拥有自己的数据结构,所以它其实算成是特殊的对象。对于这个单层目录,我们要调用ObpLookupEntryDirectory来查找这个单层目录是否有跟我们要Create的TypeName“重名”的目录项,要是有的话 自然是不允许的。接着就是拷贝对象类型的类型的类型名到对象名,因为我们刚才说了Type对象也是对象的一种。调用ObpAllocateObject创建一个Type对象,Header用来返回所创建的Type对象的ObjectHeader。接着就是堆返回过来的Type对象进行适当的初始化,Type对象的TypeInfo域会被填写为我们提交的表单,其HeaderSize的大小要注意是对象头大小 + 对象命名信息 + 对象句柄信息对于Type对象的DefaultObject域而言,它的初始化是很重要的。这个域指向的是等待对象,例如WaitForSingleObject,某些操作时是需要等待的,这样的操作是可以同步也可以异步的。有些对象类型以全局的默认等待对象ObpDefaultObject作为默认等待对象。而File类型的对象则是以专门的Event对象作为默认等待对象。同样,WaitablePort对象以其数据内部的WaitEvent对象作为默认等待对象。最后,当创建者信息存在的时候,InsertTailList将新创建的对象类型挂入ObpTypeObjectType队列。在OBJECT_TYPE内部和对象头部都没有用于这个目的的队列头,这样的队列头在OBJECT_HEADER_CREATOR_INFO结构中。此外,内核中还有个全局的OBJECT_TYPE结构指针数组ObpObjectTypes[],用来指向系统中的各种对象类型。最后,通过ObpInsertEntryDirectory将新建的Type对象挂入到ObpTypeDirectoryObject中。注意这里是以ObpTypeDirectoryObject非空为前提的,如果对象类型目录为空,则只是将创建的ObjectType返回。OBJECT_HEADER中没有用来将其挂入某个队列的队列头或指针,OBJECT_TYPE结构中虽然有个队列头,但只是用来将对象类型连接在一起的。那么代表对象类型的数据结构又怎样被连接到对象目录中呢? 我们继续往下看
ObCreateObjectType->ObpInsertEntryDirectory
BOOLEAN NTAPI ObpInsertEntryDirectory(IN POBJECT_DIRECTORY Parent, //插入的父目录 IN POBP_LOOKUP_CONTEXT Context, //上下文 IN POBJECT_HEADER ObjectHeader) //对象头 { POBJECT_DIRECTORY_ENTRY *AllocatedEntry; POBJECT_DIRECTORY_ENTRY NewEntry; POBJECT_HEADER_NAME_INFO HeaderNameInfo; /* Make sure we have a name */ ASSERT(ObjectHeader->NameInfoOffset != 0); /* Validate the context */ if ((Context->Object) || !(Context->DirectoryLocked) || (Parent != Context->Directory)) { /* Invalid context */ DPRINT1("OB: ObpInsertEntryDirectory - invalid context %p %ld\n", Context, Context->DirectoryLocked); KEBUGCHECK(0); return FALSE; } /* Allocate a new Directory Entry */ NewEntry = ExAllocatePoolWithTag(PagedPool, //分配一个目录项空间用于存放目录项 sizeof(OBJECT_DIRECTORY_ENTRY), OB_DIR_TAG); if (!NewEntry) return FALSE; /* Save the hash */ NewEntry->HashValue = Context->HashValue; //要插入对象的HashValue /* Get the Object Name Information */ HeaderNameInfo = OBJECT_HEADER_TO_NAME_INFO(ObjectHeader); /* Get the Allocated entry */ AllocatedEntry = &Parent->HashBuckets[Context->HashIndex]; //获得相应Hash队列的指针 /* Set it */ NewEntry->ChainLink = *AllocatedEntry;//新Type对象插入到开头 *AllocatedEntry = NewEntry; /* Associate the Object */ NewEntry->Object = &ObjectHeader->Body;//对象设置为我们的Type对象 /* Associate the Directory */ HeaderNameInfo->Directory = Parent; //命名信息的父目录填写为该父目录 return TRUE; }ObpInsertEntryDirectory还是很一目了然的。首先根据上下文寻找新的目录项插入到该目录的哪个队列,然后将分配的目录项插入到该队列的开头,然后初始化该目录项,具体就是将目录项的Object域指向ObjectHeader的Body处,即我们的Type对象,最后在将我们的Type对象的NameInfo的父目录设置成该对象类型目录。
下面再通过对象目录的初始化过程 ObInit进一步说明对象类型的创建
BOOLEAN INIT_FUNCTION NTAPI ObInit(VOID) { OBJECT_ATTRIBUTES ObjectAttributes; UNICODE_STRING Name; OBJECT_TYPE_INITIALIZER ObjectTypeInitializer; OBP_LOOKUP_CONTEXT Context; HANDLE Handle; PKPRCB Prcb = KeGetCurrentPrcb(); PLIST_ENTRY ListHead, NextEntry; POBJECT_HEADER Header; POBJECT_HEADER_CREATOR_INFO CreatorInfo; POBJECT_HEADER_NAME_INFO NameInfo; NTSTATUS Status; ... /* Create the Type Type */ RtlZeroMemory(&ObjectTypeInitializer, sizeof(ObjectTypeInitializer)); //初始化一个提交单 RtlInitUnicodeString(&Name, L"Type"); //该提交单要创建的对象类型的类型名是 “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; ObCreateObjectType(&Name, &ObjectTypeInitializer, NULL, &ObpTypeObjectType); //创建一个“Type” Type对象 挂入对象类型目录 /* Create the Directory Type */ RtlInitUnicodeString(&Name, L"Directory"); //创建一个“Directory” Type对象 挂入对象类型目录 ObjectTypeInitializer.ValidAccessMask = DIRECTORY_ALL_ACCESS; ObjectTypeInitializer.UseDefaultObject = FALSE; ObjectTypeInitializer.MaintainTypeList = FALSE; ObjectTypeInitializer.GenericMapping = ObpDirectoryMapping; ObjectTypeInitializer.DefaultNonPagedPoolCharge = sizeof(OBJECT_DIRECTORY); ObCreateObjectType(&Name, &ObjectTypeInitializer, NULL, &ObDirectoryType); //都是提交同一个表单... /* Create 'symbolic link' object type */ RtlInitUnicodeString(&Name, L"SymbolicLink"); //创建一个“SymobolLink”的 Type对象 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, &ObSymbolicLinkType); /* Phase 0 initialization complete */ ObpInitializationPhase++; return TRUE; ObPostPhase0: ...这里分两部分讲解,函数ObInit()在系统的初始化阶段被调用两次,第一次是在所谓的Phase0阶段,第二次是在Phase1阶段,或者说“后Phase0”阶段。上一段代码是在Phase0中执行的。 这个初始化一共创建了三个“对象类型”的对象。首先创建的对象类型是Type,这是所有其他对象类型的模板,是“元类型”。注意ObCreateObjectType的最后一个参数是&ObpTypeObjectType,这是因为“Type”是系统中第一个对象类型,其后在创建其他对象类型的时候都需要引用这个指针。在前面的定时器对象类型创建时,第三个参数就是指针ObpTypeObjectType。 创建了元类型后,又创建了Directory和SymbolicLink 两种对象类型。 其中Type 这种对象类型是不对用户空间开放的,而Directory和SymbolLink类型则是对用户空间开放的,所以是真正意义上的“对象”类型。Phase0完成了创建对象目录的准备。
到系统初始化的Phase1阶段,Obinit()又会被调用,这一次就会直接转到ObPostPhase0下面,这一次就是创建对象目录了
ObPostPhase0: /* Re-initialize lookaside lists */ ObInit2(); RtlInitUnicodeString(&Name, L"\\");//创建对象目录的根 InitializeObjectAttributes(&ObjectAttributes, //封装成对象属性 &Name, OBJ_CASE_INSENSITIVE | OBJ_PERMANENT, NULL, SePublicDefaultUnrestrictedSd); /* Create the directory */ Status = NtCreateDirectoryObject(&Handle, //对象目录的根是个目录对象 DIRECTORY_ALL_ACCESS, &ObjectAttributes); if (!NT_SUCCESS(Status)) return FALSE; /* Get a handle to it */ Status = ObReferenceObjectByHandle(Handle, //引用根目录这个目录对象 0, ObDirectoryType, KernelMode, (PVOID*)&ObpRootDirectoryObject, NULL); if (!NT_SUCCESS(Status)) return FALSE; /* Close the extra handle */ Status = NtClose(Handle);//内核通过ObpRootDirectoryObject来访问根目录对象,而不需要Handle 所以关闭该对象 if (!NT_SUCCESS(Status)) return FALSE; /* Initialize Object Types directory attributes */ RtlInitUnicodeString(&Name, L"\\ObjectTypes"); //创建子目录 "\ObjectTypes" InitializeObjectAttributes(&ObjectAttributes, &Name, OBJ_CASE_INSENSITIVE | OBJ_PERMANENT, NULL, NULL); /* Create the directory */ Status = NtCreateDirectoryObject(&Handle, //创建该目录对象 建立连接后返回句柄 DIRECTORY_ALL_ACCESS, &ObjectAttributes); if (!NT_SUCCESS(Status)) return FALSE; /* Get a handle to it */ Status = ObReferenceObjectByHandle(Handle, //引用“\ObjectTypes”对象 0, ObDirectoryType, KernelMode, (PVOID*)&ObpTypeDirectoryObject, NULL); if (!NT_SUCCESS(Status)) return FALSE; /* Close the extra handle */ Status = NtClose(Handle); //关闭该句柄 if (!NT_SUCCESS(Status)) return FALSE; /* Initialize lookup context */ ObpInitializeDirectoryLookup(&Context);这里通过NtCreateDirectoryObject创建了两个目录对象,即创建根目录"" 和 子目录对象"、ObjectTypes"两个目录对象,前者显然就是ObpRootDirectoryObject指向的目录对象,后者则是对象类型目录 ObpTypeDirectoryObject,NtCreateDirectoryObject是系统调用,这里因为在内核中不带系统调用框架直接调用其内核函数。 NtCreateDirectoryObject是用来创建各种对象的若干系统调用之一。凡是创建/打开对象的系统调用都有个重要的参数ObjectAttributes,指向一个OBJECT_ATTRIBUTES的数据结构,里面包含有对象的路径名,访问权限,属性,访问控制等信息。这里通过NtCreateDirectoryObject创建一个“目录类型”,对象名为“\”的对象作为整个对象目录的根。 这个函数会返回一个属于当前进程的Handle。在这里,当前进程就是初始化进程。 每个进程会有一个句柄表,表中维持着句柄与目标对象数据结构之间的对应关系。 另一方面,创建一个对象就相当于打开了这个对象,一个句柄就代表着对于目标的一次打开。但是,因为在实际使用中,访问当前句柄的多半不会是系统初始化进程,所以在内核中使用句柄是不方便的,因此内核中总是使用指针ObpRootDirectoryObject访问根结点。 所以通过NtClose来关闭这个多余的句柄。而又因为NtClose执行后,对这个目录对象的引用计数会减1,我们在创建的时候引用计数会加1,所以如果之间调用NtClose的话,会导致该对象的引用计数减至0,说明这个对象的数据结构已经不再使用而应该将其释放,所以直接调用NtClose关闭该句柄是不可行的,要先调用ObReferenceObjectByHandle先增加一次引用计数,这样NtClose后还会是1。 以前讲过,以调用NtClose为例,其对应的内核中调用应该是ZwClose,可是这里的两个系统调用所用的都是Nt版本,实际上是可以调用NtClose的,只是此时系统空间的堆栈上没有自陷框架。而这里的ObInit因为本身就是内核的一部分,所以不存在这个问题
有了目录节点“\ObjectTypes”即ObpTypeDirectoryObject以后,还要把前面Phase0时所创建的几个代表着对象类型的对象插入到这个目录,也就是下面
/* Lock it */ ObpAcquireDirectoryLockExclusive(ObpTypeDirectoryObject, &Context); /* Loop the object types */ ListHead = &ObpTypeObjectType->TypeList; //在ObpTypeObjectType队列中 NextEntry = ListHead->Flink; while (ListHead != NextEntry) //遍历这个队列 { /* Get the creator info from the list */ CreatorInfo = CONTAINING_RECORD(NextEntry, //可以看出NextEntry是OBJECT_HEADER_CREATOR_INFO类型 这里是获取每一个创建者信息 OBJECT_HEADER_CREATOR_INFO, TypeList); /* Recover the header and the name header from the creator info */ Header = (POBJECT_HEADER)(CreatorInfo + 1);//这里的1 是 sizeof(OBJECT_HEADER_CREATOR_INFO ) NameInfo = OBJECT_HEADER_TO_NAME_INFO(Header);//根据对象头获取 命名信息 /* Make sure we have a name, and aren't inserted yet */ if ((NameInfo) && !(NameInfo->Directory)) //若对象存在命名 并且 没有被挂到对象类型目录中 { /* Do the initial lookup to setup the context */ if (!ObpLookupEntryDirectory(ObpTypeDirectoryObject, //在ObpTypeDirectoryObject即对象类型目录中查找 &NameInfo->Name, OBJ_CASE_INSENSITIVE, FALSE, &Context)) { /* Insert this object type */ ObpInsertEntryDirectory(ObpTypeDirectoryObject,//若找不到,则将该对象类型挂入队列 &Context, Header); } } /* Move to the next entry */ NextEntry = NextEntry->Flink; } /* Cleanup after lookup */ ObpCleanupDirectoryLookup(&Context); /* Initialize DOS Devices Directory and related Symbolic Links */ Status = ObpCreateDosDevicesDirectory(); if (!NT_SUCCESS(Status)) return FALSE; return TRUE; }可以看出,这里通过遍历CreatorInfo中的TypeList队列,将这个队列里的每个节点与对象类型目录的已有对象类型进行比较,若不存在对象类型目录,那就调用ObpInsertEntryDirectory插入到对象类型目录的相应Bucket队列。 总结一下,phase0创建了三个Type对象,postPhase0,也就是Phase1先是创建了两个目录对象,即一个根目录和一个对象类型目录,用ObpRootDirectoryObject,ObpTypeDirectoryObject可索引到。现在目录和最基础的对象类型都有了,但是还没有建立联系,所以要将这三个对象类型插入到对象类型目录,但如何找到这三个对象类型呢?所以引入第三个队列ObpTypeObjectType,其TypeList域是对象类型的队列,其中每一个节点是一个OBJECT_HEADER_CREATOR_INFO类型,也说明了创建者信息中也有TypeList域,通过遍历每一个节点,我们可以获取创建者信息,继而获取它们的对象头信息,这样对象的信息就可以获取,从而通过ObpLookupEntryDirectory与现有的对象类型目录中未有的对象类型通过ObpInsertEntryDirectory添加进去,通过ObInit,一环扣一环,Windows的设计是很巧妙的!!!。
ObpRootDirectoryObject 对象目录 ObpTypeDirectoryObject 对象类型目录 CreatorInfo != NULL -> Creator->TypeList入 ObpTypeObjectType->TypeList队列 NameInfo->Directory 从对象头的Header处可以找到NameInfoOffset继而获取NameInfo可以找到该对象被Insert到哪个目录下
初始化的时候(ObInit),首先填写“提交单”,然后调用ObCreateObjectType创建了三个 对象类型 的对象(存储到了 ObpTypeObjectType,ObDirectoryType,ObSymbolicLinkType) ,注意,此时ObCreateObjectType并不会将这三个Insert到ObpTypeDirectoryObject(对象类型目录)下,因为此时对象类型目录就没有被构建!!!。 所以在Phase1的时候,创建了两个目录对象(OBJECT_DIRECTORY)即根目录(ObpRootDirectoryObject)和ObjectType目录(对象类型目录,ObpTypeDirectoryObject),所以其实对象类型目录是根目录的孩子。在ObpTypeObjectType中有条TypeList队列,这个队列是每个对象类型对象的CreatorInfo,即OBJECT_HEADER_CREATOR_INFO结构串接的,通过找到CreatorInfo不难会推断出对象头ObjectHeader的位置,接着通过ObpLookupEntryDirectory将对象目录中没有的对象类型添加进去。