11.2 Windows Vista编程

现在开始Windows Vista的技术研究。但是,在研究详细的内部结构之前,我们首先看看系统调用的本地NT API和Win32编程子系统。尽管有可移植操作系统接口(POSIX),但实际上为Windows编写的代码不是Win32就是.NET,其中.NET本身也是运行在Win32之上的。

图11-6介绍的是Windows操作系统的各个层次。在Windows应用程序和图形层下面是构造应用程序的程序接口。和大多数操作系统一样,这些接口主要包括了代码库(DLL),这些代码库可以被应用程序动态链接以访问操作系统功能。Windows也包含一些被实现为单独运行进程的服务的应用程序接口。应用软件通过远程过程调用(RPC)与用户态服务进行通信。

阅读 ‧ 电子书库
图 11-6 Windows的编程层

NT操作系统的核心是NTOS内核态程序(ntoskrnl.exe),它提供了操作系统的其他部分的实现所依赖的传统的系统调用接口。在Windows中,只有微软的程序员编写系统调用层。已经公开的用户态接口属于操作系统本身,它通过运行在NTOS层顶层的子系统(subsystem)来实现的。

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

最早的NT支持三个个性化子系统:OS/2、POSIX、Win32。OS/2在Windows XP中已经不使用了。POSIX也同样不使用了,但是客户可以得到一个叫做Interix的改进版POSIX的子系统,它是微软面向UNIX的服务(SFU)的一部分,因此所有设备都支持系统中原有的POSIX。尽管微软支持其他的API,但大多数Windows的应用软件都是用Win32写的。

不同于Win32,.NET并不是原来NT的内核接口上的正式的子系统。相反,.NET是建立在Win32编程模型之上的。这样就可以使.NET与现有的Win32程序很好地互通,而不必关心POSIX和OS/2子系统。WinFX API包含了很多Win32的功能,而实际上WinFX基本类库(Base Class Library)中大多数的功能都是Win32 API的简单包装器。WinFX的优点是有丰富的对象类型支持、简单一致的界面、使用.NET公共语言运行库(CLR)和垃圾收集器。

如图11-7所示,NT子系统建立了四个部分:子系统进程、程序库、创建进程(CreateProcess)钩子、内核支持。一个子系统进程只是一个服务。它唯一特殊的性质就是通过smss.exe程序(一个由NT启动的初始用户态程序)开始,以响应来自Win32的CreateProcess或不同的子系统中相应的API的请求。

阅读 ‧ 电子书库
图 11-7 用于构建NT子系统的模块

程序库同时实现了高层的操作系统功能和特定的子系统进程。这些高层的操作系统功能是特定于子系统以及子系统所包含的桩程序(stub routine)的。桩程序是进行不同的使用子系统的进程间通信的。对子系统进程的调用通常是利用内核态的本地过程调用LPC(Local Procedure Call)所提供的功能。LPC实现了跨进程的进程调用。

在Win32 CreateProcess中的钩子函数(hook)通过查看二进制图像来检测子系统中每个程序请求。(如果它没有运行)通过smss.exe启动子系统进程csrss.exe。然后子系统进程开始加载程序。在其他子系统中也有类似的钩子函数(例如POSIX中的exec系统调用)。

NT内核有很多一般用途的设备,可以用来编写操作系统特定的子系统。但是为了准确地执行每一个子系统还需要加入一些特殊的代码。例如,本地NtCreateProcess系统调用通过重复使用进程实现POSIXF fork函数调用,内核提供一个Win32特殊类型串表(叫atoms),通过进程有效实现只读字符串的共享。

子系统进程是本地端NT程序,其使用NT内核和核心服务提供的使用本地系统调用,例如smss.exe和lsass.exe(本地安全管理)。本地系统调用包括管理虚拟地址的跨进程功能(facility)、线程、句柄和为了运行用来使用特定子系统的程序而创建的进程中的异常。

11.2.1 内部NT应用编程接口

像所有的其他操作系统一样,Windows Vista也拥有一套系统调用。它们在Windows Vista的NTOS层实施,在内核态运行。微软没有公布内部系统调用的细节。它们被操作系统内部一些底层程序使用,这些底层程序通常是以操作系统的一部分(主要是服务和子系统),或者是内核态的设备驱动程序的形式交付的。本地的NT系统调用在版本的升级中并没有太大的改变,但是微软并没有选择公开,而Windows的应用程序都是基于Win32的,因此Win32 API在不同Windows操作系统中是通用的,从而能够让这些应用程序在基于MS-DOS和NT Windows的系统中正确运行。

大多数内部的NT系统调用都是对内核态对象进行操作的,包括文件、线程、管道、信号量等。图11-8中给出了一些Windows Vista中NT所支持的常见内核态对象。以后,我们讨论内核对象管理器时,会讨论具体对象类型细节的。

阅读 ‧ 电子书库
图 11-8 内核态对象类型的普通类别

有时使用术语“对象”来指代操作系统所控制的数据结构,这样就会造成困惑,因为错误理解成“面向对象”了。操作系统的对象提供了数据隐藏和抽象,但是缺少了一些面向对象体系基本的性质,如继承和多态性。

在本地NT API调用中存在创建新的内核态对象或操作已经存在的对象的调用。每次创建和打开对象的调用都返回一个结果叫句柄(handle)给调用者(caller)。句柄可在接下来用于执行对象的操作。句柄是特定于创建它们的具体的进程的。通常句柄不可以直接交给其他进程,也不能用于同一个对象。然而,在某些情况下通过一个受保护的方法有可能把一个句柄复制到其他进程的句柄表中进行处理,允许进程共享访问对象——即使对象在名字空间无法访问。复制句柄的进程必须有来源和目标进程的句柄。

每一个对象都有一个和它相关的安全描述信息,详细指出对于特定的访问请求,什么对象能够或者不能够针对一个特定的目标进行何种操作。当句柄在进程之间复制的时候,可添加具体的被复制句柄相关的访问限制。从而一个进程能够复制一个可读写的句柄,并在目标进程中把它改变为只读的版本。

并不是所有系统创建的数据结构都是对象,并不是所有的对象都是内核对象。那些真正的内核态对象是那些需要命名、保护或以某种方式共享的对象。通常,这些内核态对象表示了在内核中的某种编程抽象。每一个内核态的对象有一个系统定义类型,有明确界定的操作,并占用内核内存。虽然用户态的程序可以执行操作(通过系统调用),但是不能直接得到数据。

图11-9为一些本地API的示例,通过特定的句柄操作内核对象,如进程、线程、IPC端口和扇区(用来描述可以映射到地址空间的内存对象)。NtCreateProcess返回一个创建新进程对象的句柄,SectionHandle代表一个执行实例程序。当遇到异常时控制进程(例如异常、越界),DebugPort Handle用来在出现异常(例如,除零或者内存访问越界)之后把进程控制权交给调试器的过程中与调试器通信。

阅读 ‧ 电子书库
图 11-9 在进程之间使用句柄来管理对象的本地NT API调用示例

NtCreate线程需要ProcHandle,因为ProcHandle可以在任意一个含有句柄的进程中(有足够的访问权限)创建线程。同样,NtAllocateVirtualMemory、NtMapViewOfSection、NtReadVirtualMemory和NtWriteVirtualMemory可使进程不仅在自己的地址空间操作,也可以在分配虚拟地址和映射段,还可以读写其他进程的虚拟内存。NtCreateFile是一个内部API调用,用来创建或打开文件。NtDuplicateObject,可以在不同的进程之间复制句柄的API调用。

当然不是只有Windows有内核态对象。UNIX系统也同样支持内核态对象,例如文件、网络数据包、管道、设备、进程、共享内存的IPC设备、消息端口、信号和I/O设备。在UNIX中有各种各样的方式命名和访问对象,例如文件描述符、进程ID、System V IPC对象的整形ID和设备节点。每一类的UNIX对象的实现是特定于其类别的。文件和socket使用不同的设施facility,并且是System V IPC机制、程序、装置之外的。

Windows中的内核对象使用一个的基于NT名字空间中关于对象的句柄和命名统一设备指代内核对象,而且使用一个统一的集中式对象管理器。句柄是进程特定的,但正如上文所述,可以在被另一个进程使用。对象管理器在创建对象时可以给对象命名,可以通过名字打开对象的句柄。

对象管理器在NT名字空间中使用统一的字符编码标准(宽位字符)命名。不同于UNIX,NT一般不区分大小写(它保留大小写但不区分)。NT名字空间是一个分层树形结构的目录,象征联系和对象。

对象管理器提供统一的管理同步、安全和对象生命期的设备。对于对象管理器提供给用户的一般设备是否能为任何特定对象的用户所获得,这是由执行体部件来决定的,它们都提供了操纵每一个对象类型的内部API。

这不仅是应用程序使用对象管理器中的对象。操作系统本身也创建和使用对象——而且非常多。大多数这些对象的创建是为了让系统的某个部分存储相当一段长时间的信息或者将一些数据结构传递给其他的部件,但这都受益于对象管理器对命名和生存周期的支持。例如,当一个设备被发现,一个或多个设备创建代表该设备对象,并在理论上说明该设备如何连接到系统的其他部分。为了控制设备而加载设备的驱动程序,创建驱动程序对象用来保存属性和提供驱动程序所实现的函数的指针,这些函数是实现对I/O请求的处理。操作系统中在以后使用其对象时会涉及这个驱动。驱动也可以直接通过名字来访问,而不是间接的通过它所控制的设备来访问的(例如,从用户态来设置控制它的操作的参数)。

不像UNIX把名字空间的根放在了文件系统中,NT的名字空间则是保留在了内核的虚拟内存中。这意味着NT在每次系统启动时,都得重新创建最上层的名字空间。内核虚拟内存的使用,使得NT可以把信息存储在名字空间里,而不用首先启动文件系统。这也使得NT更加容易地为系统添加新类型的内核态的对象,原因是文件系统自身的格式不需要为每种新类型的目标文件进行改变。

一个命名的目标文件可以标记为永久性的,这意味着这个文件会一直存在,即使在没有进程的句柄指向该对象条件下,除非它被删除或者系统重新启动。这些对象甚至可以通过提供parse例程来扩展NT的名字空间,这种例程方式类似于允许对象具有UNIX中挂载点的功能。文件系统和注册表使用这个工具在NT的名字空间上挂载卷和储巢。访问到一个卷的设备对象即访问了原始卷(raw volume),但是设备对象也可以表明一个卷可以加载到NT名字空间中去。卷上的文件可以通过把卷相关文件名加在卷所对应的设备对象的名称后面来访问。

永久性名字也用来描述同步的对象或者共享内存,因此它们可以被进程共享,避免了当进程频繁启动和停止时来不断重建。设备文件和经常使用的驱动程序会被给予永久性名字,并且给予特殊索引节点持久属性,这些索引节点保存在UNIX的/dev目录下。

我们将在下一节中描叙纯NT API的更多特征,讨论Win32 API在NT系统调用的封装性。