【Windows 操作系统】 内核对象|句柄


内核对象简介

内核对象就是 一些数据结构该结构用来描述存储内核中的一个内存块中的数据信息。

??内存块是一种数据结构,其中的数据成员负责维护该对象的相应信息,这个数据结构以及其中的数据成员只能由内核访问,应用程序是无法访问到的,更别说修改其中的数据成员了。

 概念:内核对象可以供系统和应用程序使用来管理各种各样的资源,Windows程序员可以调用Windows API最终调用ntdll.dll中的函数 去创建、打开和操作各种内核对象。常见的内核对象有:访问令牌、事件对象、文件对象、文件映射对象、I/O完成端口、作业对象、邮件槽对象、互斥量对象、管道对象、进程对象、信号量对象、线程对象、可等待计时器对象以及线程池工厂对象等。

个人理解

首先大家别被这个名字唬住了,内核对象=内核+对象;先解释“对象”这个名字,这个与C++中的类的实例是一个道理,即类的实例化便是一个到底,再往底层了说就是按照类这个模板,为其分配一块内存,并作相应的初始化,就这么简单,可别忘了,操作系统也是用代码写成的,只不过不同于C++中的类,而是用的C中的结构体。在一个概念就是“内核”,可别被这玩意整蒙了,这里所谓的内核无非就是跑在系统空间里的实例对象,即对象的内存地址分配在系统空间中的对象即为内核对象。好了,名字的事情搞明白了

内核对象和用户对象相对,由操作系统创建和管理。内核对象的访问时间远大于用户对象。所以创建内核对象的性能损失是比较大的

2、内核对象的构成

我们都知道,像CPP,JAVA,C#等等,这类面向对象的编程语言有一个很重要的特性就是“继承”。所谓的继承一句话总结下就是:拿来主义,子承父业。无非是实现代码复用,而这种复用的语法很 简单,就是通过”继承”来实现了,简化了复用的整个过程。除了复用之外,还有一个好处便是能够实现多态,通过基类指针就能够访问子类中该写过的虚函数,这种基于同一个接口实现不同功能的方式便是多态的核心了。通过一套统一的接口就能够达到统一管理的目的,不得不说,CPP的成功本质是编程思想的成功。然而C语言中,没有继承这个概念,那么多态啥的就跟他没一丁点关系了。难道伟大的C语言真就被这么个小玩意难住了吗?显然没有,诺,这里的内核对象管理就另辟蹊径,把C玩出了CPP的味道,不得不说,C就是这么伟大。微软的解决方案是:

对象头+对象体
对象头便是相当于CPP中的基类,负责管理一些简单的,所有对象共有的属性;诸如进程对象头,线程对象头,文件对象头,调试对象头等等;
对象体相当于CPP中的继承的子类,代表着具体的某个对象;诸如进程对象,线程对象,文件对象,调试对象等等;
具体的内存布局如下图所示:

但别觉得对象头就很简单,类似于CPP中的一个类中有很多字段属性一样,Windows内核中实现的对象头又是有很多个不同的组成部分组成的,这些独立的组成部分是否存在则由一个总的字段来进行管理的,具体的简化图如下图所示:

二.内核对象结构

每个对象都有对象头和对象体组成。所有类型的对象头结构都是相同的,而结构体部分却各不相同的。下面是内核对象的结构图:

一个对象指针总是 指向对象体而不是对象头。如果要访问对象头,需要将对象体指针减去一个特定的偏移值,以获取 OBJECT_HEADER 结构,通过 OBJECT_HEADER 结构定位从而访问其他对象结构辅助信息。对象体内部一般会有一个 type 和一个 size 成员,用来表示对象的类型和大小。

内核对象句柄

通过调用内核对象创建函数(Windows API,如CreateFileMapping)会返回一个内核对象句柄,该句柄的位数与操作系统的位数一致。为了增强操作系统可靠性,句柄与进程相关联(不同进程句柄可以一样)。

句柄是一个指向对象得指针。对象具有唯一性,但是句柄可以有多个,也就是说一个对象可以有多个句柄来引用它,对它进行操作。用来管理命名对象的东西,叫做对象目录

前面讲过句柄与进程相关联,这是通过进程的句柄表来实现的,进程初始化时,系统将为它分配一个句柄表,用以存在内核对象。可以将句柄表看成一个数组,每个数组成员存放一个句柄的相关信息:指向内核对象的指针、一个访问掩码和一些标志。

 索引:表示内核对象的信息保存在进程句柄表中的具体位置。
指向内核对象内存块的指针:
访问掩码:访问权限控制,类似网络掩码
访问掩码的功能是以压缩形式描述访问权限。 为简化访问管理,访问掩码包含一组四位(一般权限),这些权限通过使用函数 RtlMapGenericMask转换为一组更详细的权限。
标志:标识子进程是否继承其内核对象。

如何访问这些内核对象(内存块)呢?

??操作系统为使用者封装了一组API,使用者可以通过这些API访问内核对象(内存块)。比如,创建内核对象(内存块)时,使用者调用API中的创建内核对象函数,由内核创建一个内核对象(分配一块内存)。

??内核对象创建好之后,用一个句柄来标识该内核对象(内存块),这个句柄作为函数值返回。这个句柄就是个整数,32位机句柄就是32bit,64位机句柄就是64bit,同一进程中的任何线程都能使用这个句柄,当需要操作内核对象(内存块)时,通过API将这个句柄传给内核,内核就知道是对哪个内核对象(内存块)进行操作了。

内核中知道了内核对象的地址就可以直接访问这个内核对象了,但是在用户程序中却不能这样访问。Windows为内核对象的访问提供了一系列的函数。当调用一个用于创建内核对象的函数时,函数调用完便返回一个句柄值。句柄值是进程独立的,一个进程中的句柄值在另一个进程中是无效的。

句柄值是一个进程句柄表的索引。每个进程都有一个进程句柄表,而所有进程的句柄表串成一个句柄表链。这个链的头部地址保存在内核变量HandleTableListHead中。

 

使用计数

内核对象的所有者是操作系统

而非进程,即内核对象的生命周期并不一定会随着创建该对象的进程的消亡而消亡,这一点是通过使用计数来实现的。使用计数是所有内核对象固有的属性,操作系统通过使用计数维护内核对象的生命周期,当使用计数为0的时候,操作系统将销毁该内核对象。内核对象被创建时,其使用计数为1,另一个进程访问该内核对象后,使用计数加1,当进程终止时,使用计数减1。

内核对象类型

内核对象容器

 线程内核对象管理方法:

由多条不同类型的链表组织方式 

 创建和关闭内核对象

当进程首次初始化的时候,其句柄表为空,当进程内的一个线程调用一个会创建内核对象的函数时,操作系统将为这个对象分配并初始化一块内存,然后扫描进程的句柄表,查找到一个空白的记录项,填入该内核对象的相关信息,最后返回对象句柄。可以调用CloseHandle来关闭内核对象句柄,该函数首先检查主调进程的句柄表,验证被关闭的对象句柄值是该经常确实有权访问的一个对象,如果句柄有效,系统将获得内核对象的数据结构地址,并将使用计数减1,如果使用计数变为0,内核对象将被销毁,并从内存中除去。一个内核对象不用了应该调用CloseHandle关闭对象,否则程序运行时将泄漏内核对象,当然,当进程终止后,也能保证内核对象被正确清除。

内核对象结构图