1.6 句柄和对象

Windows内核系统显露了多种类型的对象供用户模式进程、内核本身以及内核模式驱动程序使用。这些类型的实例是位于系统空间的数据结构,由对象管理器(执行体的一部分)在用户模式或者内核模式代码请求时创建。对象是引用计数的—只有当对象的最后一个引用被释放之后,对象才会被销毁并从内存中释放。

由于这些对象位于系统空间,所以它们不能被用户模式直接访问。用户模式必须使用一种间接的访问机制,这种机制被称为句柄。句柄是指向一个表格的入口索引,该表格在进程的基础上维护,逻辑上指向驻留在系统空间的一个内核对象。有各种Create*和Open*函数用来创建/打开对象,并返回指向对象的句柄。举个例子,CreateMutex这个用户模式的函数允许创建或者打开一个互斥量(依赖于对象是否有名称以及是否存在)。如果执行成功,此函数返回一个指向对象的句柄。返回值为零则表示返回的是一个无效句柄(表示函数调用出错)。另一方面,OpenMutex函数会试图打开一个有名字的互斥量。如果有该名字的互斥量不存在,此函数就会失败并返回null(0)。

内核(和驱动程序)代码可以使用句柄或者对象的直接指针。选择哪个通常由代码调用的API决定。在某些情况下,由用户模式传给驱动程序的句柄必须用ObReferenceObjectByHandle函数转换成对象指针。我们会在下一章讨论这些细节。

000

多数函数在失败时返回null(0),但是有些不是。特别值得注意的是,CreateFile函数在失败时返回INVALID_HANDLE_VALUE(-1)。

句柄的值是4的倍数,第一个有效的句柄值是4,0永远都不是有效的句柄值。

内核模式代码在创建/打开对象的时候可以使用句柄,也可以直接使用指向内核对象的指针。这通常根据使用的API的要求来决定。内核模式代码可以调用ObReferenceObjectByHandle函数从一个有效的句柄得到指针。如果调用成功,对象的引用计数会加1,所以即使拥有这个句柄的用户模式客户决定关闭句柄,也不会对拥有指针的内核模式代码造成危险。在内核代码调用ObDerefenceObject函数之前,不管句柄的拥有者做什么,通过指针访问对象都是安全的。ObDerefenceObject函数会将对象的引用计数减1,如果内核代码忘记调用该函数,就会造成资源泄漏,这个泄漏只能通过重启系统来解决。

所有的对象都是引用计数的。对象管理器维护着句柄计数和对象引用总数。一旦某个对象不再被需要,该对象的客户必须关闭句柄(如果用句柄来访问对象的话)或者解除对此对象的引用(如果内核模式的客户使用指针的话)。从这里开始,客户程序需认为句柄/指针已经无效。对象管理器会在引用计数成为零时销毁对象。

每个对象指向一个对象类型,对象类型保存着此类型本身的信息,这意味着每一类对象都有一个对象类型。对象类型也作为全局内核变量暴露出来,其中有一些在内核头文件中有定义。如同我们会在随后的章节中看到的那样,这些信息在某些情况下非常有用。

1.6.1 对象名称

一些类型的对象可以有名称。可以通过合适的Open函数使用名称来打开对象。请注意并非所有对象都有名称,比如,进程和线程就没有名称—它们有标识符。这就是为什么OpenProcessOpenThread函数需要进程/线程标识符(一个数字),而不需要字符串名称。另一个有些奇怪的无名称对象是文件,文件名并非对象名—这是两个不同的概念。

从用户模式代码里使用名称调用Create函数,在此名称的对象不存在的情况下,会创建一个对象,但是如果该对象已经存在,它只会打开已经存在的对象。在后面的情况下,调用GetLastError将会返回ERROR_ALREADY_EXISTS,表示这不是一个新创建的对象,并且返回的句柄仅是已经存在的对象的又一个句柄。

提供给Create函数的名称并非对象的最终名称。名称的前面会加上\Sessions\x\BaseNamedObjects\,其中x是调用者的会话标识符。如果是0号会话,名称的前面会加上\BaseNamedObjects\。如果调用者在应用容器内(一般是一个通用Windows平台(Universal Windows Platform)进程)运行,那么加到前面的字符串会更加复杂,包含了唯一的应用容器SID: \Sessions\x\AppContainerNamedObjects\{AppContainerSID} 。

以上这些情况都意味着,对象的名称是相对于会话的(在应用容器的情形下,还相对于包(package))。如果一个对象需要在会话之间共享,可以通过加上前缀Global\在0号会话中创建它。例如,调用CreateMutex函数创建一个叫作Global\MyMutex的对象,系统会在\BaseNamedObjects\下创建这个对象。请注意,应用容器没有使用0号会话名字空间的能力。名字空间的层次结构可以用Sysinternals的WinObj工具(通过提升权限)来查看,如图1-9所示。

000

图1-9 Sysinternals WinObj工具

图1-9显示了对象管理器名字空间,由有名称的对象的层次结构组成。整个结构都保留在内存里,由对象管理器(执行体的一部分)根据需要进行操作。请注意没有名称的对象不在这个结构里,所以WinObj里能看到的并不是所有存在的对象,而是所有使用名称创建的对象。

每个进程有一个私有的、指向内核对象的句柄表格,而无论这些对象有没有名称。这个表格可以通过Sysinternals的Process Explorer和/或Handles工具进行查看。图1-10是一个Process Explorer的截屏,其中显示了一些进程的句柄。在句柄视图中,默认显示的列只有对象类型和名称。然而,还有其他的列可供选择,如图1-10所示。

000

图1-10 用Process Explorer查看进程的句柄

默认情况下,Process Explorer只显示有名称的对象的句柄(根据Process Explorer对于“名称”的定义,稍后讨论)。要显示进程的所有句柄,需要从Process Explorer的View菜单中选择Show Unnamed Handles and Mappings。

句柄视图的各列提供了关于句柄的更多信息。句柄值列和对象类型列的意思从名字就能了解,名称列就比较复杂了。对互斥量(类型列中显示Mutant)、信号量、事件、节、ALPC端口、任务、时钟和其他一些较少用到的对象,它显示的是真正的对象名称,而对另外一些对象,显示的则是别的含义:

  • 对进程和线程对象,显示的是它们的唯一标识符。
  • 对文件对象,显示的是文件对象所指向的文件名(或者设备名)。这不同于对象名称,因为无法从文件名得到文件对象的句柄—只能创建一个新的文件对象去访问底下的同一个文件或者设备(假定在原来的文件对象共享设置中允许这么做)。
  • (注册表)键值对象显示注册表的键值路径。这不是一个对象名称,理由同文件对象。
  • 目录对象显示的是路径,而不是真正的对象名称。这个目录对象并不是文件系统里的目录,而是对象管理器的目录—可以用Sysinternals的WinObj工具来查看。
  • 令牌对象显示的是存储在令牌中的用户名。

1.6.2 访问已经存在的对象

Process Explorer的句柄视图中的访问(access)列显示了打开或者创建句柄时使用的访问掩码(access mask)。访问掩码是允许特定的句柄做哪些操作的关键。例如,假设客户代码希望终止一个进程,它必须指定含有PROCESS_TERMINATE的访问掩码来调用OpenProcess函数得到进程的句柄,否则得到的句柄是无法用来终止进程的。如果调用成功了,用得到的句柄调用TerminateProcess才会成功。这里有一段用户模式代码,用来终止一个给定进程标识符的进程:

000

访问解码(decoded access)列提供了访问掩码值(对部分对象类型)的文本描述,这样辨别某个句柄允许哪些访问就比较简单了。

在一个句柄行上双击会显示一些对象的属性。图1-11显示了事件对象的属性示例。

000

图1-11 Process Explorer里的对象属性

图1-11中的属性包括了对象的名称(如果有的话)、类型、描述、在内核内存中的地址、打开的句柄数以及一些特定的对象信息,比如图中显示的事件对象的状态和事件类型。请注意,“引用(References)”显示的并不是对象实际正在使用的引用计数。查看对象引用计数的一个正确方法是使用内核调试器的!trueref命令,如下所示:

000

我们会在后面的章节里更仔细地考察对象的属性和内核调试器。

现在,我们写一个非常简单的驱动程序,展示并使用一下许多我们在本书中将要用到的工具。