WindowsNT设备驱动程序开发基础

来源:互联网 发布:淘宝优惠网址都有哪些 编辑:程序博客网 时间:2024/04/29 21:23
 

 一、背景介绍

1.1WindowsNT操作系统的组成
1.1.1用户模式(UserMode)与内核模式(KernelMode)

  从Intel80386开始,出于安全性和稳定性的考虑,该系列的CPU可以运行于ring0~ring3从高到低四个不同的权限级,对数据也提供相应的四个保护级别。运行于较低级别的代码不能随意调用高级别的代码和访问较高级别的数据,而且也只有ring0层的代码可以直接进行对物理硬件的访问。由于WindowsNT是一个支持多平台的操作系统,为了与其他平台兼容,它只利用了CPU的两个运行级别。一个被称为内核模式,对应80x86的ring0层,操作系统的核心部分,包括设备驱动程序都运行在该模式;另一个被称为用户模式,对应80x86的ring3层,操作系统的用户接口部分以及所有的用户应用程序都运行在该级别。

1.1.2WindowsNT操作系统的结构
  图1简要地描述了WindowsNT的系统组成。

  图一
  从图中可以看到,在物理硬件(Hardware)与系统核心(Kernel)之间有一个硬件抽象层(HardwareAbstractionLayer),它屏蔽了不同平台硬件的差异,向操作系统的上层提供了一套统一的接口。从图中我们还可以看到,设备驱动程序(DeviceDriver)是被I/O管理器(I/OManager)包围起来的,即驱动程序与操作系统上层的通信全部都要通过I/O管理器。这给驱动程序的编写带来了很大的便利,因为很多诸如接收用户的请求、与用户程序交换数据、内存映射、挂接中断、同步等等麻烦的工作都由I/O管理器代劳了。

1.1.3WindowsNT设备驱动程序的分类

  根据是否直接操作硬件,可以把驱动程序分成两大类:内核模式的驱动程序和专用驱动程序。

  内核模式的驱动程序根据硬件的通信协议,直接对硬件进行端口访问、中断响应、DMA传输。它包括:串、并行口,键盘,文件系统,SCSI,网络等驱动程序;专用驱动程序包括视频,打印,多媒体,虚拟DOS等驱动程序,他们在实现上与前者有很大区别。我在实习期间所做的工作以及本文以下的讨论都局限于内核模式的驱动程序。

p>1.2WindowsNT下内核模式设备驱动程序的结构和运行

  一般来说,设备驱动程序的任务主要有二:第一,接受来自用户程序的读写请求,把用户的数据传送给设备,或把从设备接收到的数据传送给用户;第二,轮询设备或处理来自设备的中断请求,完成数据传输。

1.2.1驱动程序与用户程序的通信

  I/O管理器把每一个设备对上层都抽象成了文件,所以在Win32用户程序中只要通过以下几条简单的文件操作API函数就可以实现与驱动程序中的某个设备通信(请注意,一个驱动程序可以驱动多个设备):

函数名功能


CreateFile打开一个设备,准备进行数据传输。返回一个与设备相关的句柄。
CloseHandle关闭一个由CreateFile打开的设备。
ReadFile从设备读取数据。
WriteFile向设备写数据。
DeviceIoControl对设备进行一些自定义的操作,比如更改设置等。
表一


1.2.2DriverEntry过程

  这是每一个设备驱动程序的入口,每次该程序启动时被系统自动调用。大部分的设备初始化的工作都在这个过程中完成。包括设置响应各种用户请求的过程的入口,使I/O管理器能知道当用户的打开、关闭、读写等请求到来时各应调用那些过程来处理。驱动程序中只有本过程的名字"DriverEntry"是固定的,以下列出的所有过程都要由本过程向系统注册。

  如果该驱动程序不响应任何请求的话,只要一个DriverEntry过程就可以构成一个能运行的驱动程序。


1.2.3Unload和ShutDown过程

  Unload过程负责在驱动程序被停止前做一些必要的处理。比如释放资源,记录最终状态等。ShutDown过程在系统即将关闭时被调用,与前者的区别在于不用释放任何资源。
1.2.4DispatchOpen和DispatchClose过程

  这两个过程在用户调用CreateFile和CloseHandle时被调用,为即将到来的读写操作做备,或做一些读写完成后的必要处理。

1.2.5DispatchRead,DispatchWrite与StartIo过程

  这前两个过程在用户调用ReadFile和WriteFile时被调用。它们先做一些检验用户请求合法性的工作,然后启动一个被称为StartIo的过程开始实际的与硬件间的数据传输。I/O管理器还通过IRP为它们提供了一个指向用户缓冲区的指针,用于与用户程序交换数� 详情请见1.3.2


1.2.6接受自定义的其他请求

  这两个过程在用户调用DeviceIoControl时被调用。它通过IRP获得用户的请求号,以及一个指向用户缓冲区的指针,可以与用户程序进行通信。


1.2.7中断处理过程(ISR)

  这些过程在中断发生时被系统调用。


1.2.8推迟过程(DeferredProcedure)


  这些过程用来在较低的运行级别完成较高运行级别过程(如中断处理过程)的一些任务。详情请见1.3.3



p>1.3实现细节

1.3.1内核代码运行级别

  WindowsNT为它的内核模式的代码分配了不同的级别。在同一个CPU上,级别低的过程可以被任何级别更大的过程中断。级别由低到高排列如下:

  级别名称运行于该级别的过程

PASSIVE_LEVELDriverEntry,Unload,ShutDown,DispatchXxx。

  APC_LEVEL在某些特殊情况下,大存储量设备的驱动程序运行于该级别。

DISPATCH_LEVELStartIo,AdapterControl,ControllerControl,IoTimer,Dpc。

   DIRQLs各种中断处理程序。 表二

1.3.2几个对象

i)I/O请求包(IRP)

  I/O管理器每收到一个来自用户的请求就创建一个该结构,并将其作为参数传给驱动程序的DispatchXxx、StartIo过程。该结构中存放有请求的类型,用户缓冲区的首地 址,用户请求数据的长度等信息。驱动程序处理完这个请求后,也在该结构中添入处理结果的有关信息,调用IoCompleteRequest将其返回给I/O管理器,用户程序的请求随即 返回。

ii)DPC

  当驱动程序中要用到Dpc过程时,需要创建该对象。具体作用请见1.3.3。

iii)驱动程序对象(DriverObject)

  该对象在驱动程序被启动时由I/O管理器创建,保存有该程序处理各种请求的过程入口、该程序所驱动的全部设备对象的链表等。

iv)设备对象(DeviceObject)

每发现一个可以驱动的设备,驱动程序调用IoCreateDevice创建一个该对象。该对象有一个指针Device Extension指向一块由驱动程序定义的结构,其中保存有关此设备的如端口号,中断向量等全部信息。

v)中断对象(Interrupt)

  该对象在驱动程序调用IoConnectInterrupt时创建,存有中断及处理的过程的信息。当一个中断发生时,I/O管理器用它寻找对应的处理过程。

1.3.3推迟过程调用(DeferredProcedureCall)

  由于中断处理过程运行于较高的DIRQL级,它们能屏蔽许多级别小于或等于它们的过程的执行,如果它们占用CPU时间过长,很容易使系统性能下降。因此中断处理过程应将一 些不是很紧急的任务放在被称为Dpc的过程中,在完成数据传输等紧急任务� 一个DPC对象放在系统DPC队列的末尾,然后退出,尽量早地让出CPU。系统将在完成所有DIRQL级 的任务后处理DPC队列,在DISPATCH_LEVEL执行每一个DPC对象指定的Dpc过程,完成中处理断过程未尽的任务。

1.3.4查找硬件信息

i)系统自动搜索到的设备

  在系统启动时,组件NTDETECT会自动地搜索计算机上已有的硬件,包括串、并行口,键盘,鼠标,以及大多数PCI和EISA设备。并将它们的信息,包括总线类型,总线号,用到的端口号及数量、中断向量号、DMA通道号、占用内存等按一定格式添入注册表的\HKEY_LOCAL_MACHINE\Hardware\description\System\键之下。在驱动程序中可以用IoQueryDeviceDescription以及一个回调函数ConfigCallback来查找符合要求的设备,并获取它的配置信息。

ii)系统不能自动搜索到的设备

  一些ISA的设备无法被系统自动检测到,只有在安装驱动程序时在注册表中人工添入它们的配置信息。驱动程序启动时可以用RtlQueryRegistryValues等函数查询注册表获得 这些信息。

1.3.5有关内存

  80386以上的32位CPU可以管理多达4GB的物理内存。它将这些内存分为许多大小为64KB的段和4KB的页来管理,并通过段描述符和页表将物理地址映射成系统地址供程序访问 。由于WindowsNT使用虚拟内存技术,可能某些系统地址对应的物理地址处于硬盘上,每当程序读写这些地址时会产生一个缺页异常,使CPU将这些内存调入物理存储器中。这 部分内存被称为分页内存(Paged)。与之对应的是非分页内存(Nonpaged),这部分内存保证是物理驻留的。驱动程序中运行级别大于等于DISPATCH_LEVEL的过程不能访问分 页内存,否则引起系统崩溃。

1.3.6缓冲的I/O与直接I/O

  在驱动程序创建了一个设备后,可以通过设置DeviceObject的Flags域的值来将设备设置成缓冲的I/O或直接的I/O。

  如果该值被设为DO_BUFFERED_IO,每当I/O管理器收到一个读写请求,就在内存的非分 页区分配一块与用户区大小相同的区域,并将首指针存放于Irp对象的AssociatedIrp.S ystemBuffer中,驱动程序就通过这个缓冲区与用户交换数据。每当一个读请求被完成时 I/O管理器自动将该缓冲区中的内容复制到用户区,并释放该区域。

  如果用户区大于一页(在80x86上为4096字节),一般将该值设为DO_DIRECT_IO。这时每当I/O管理器收到一个读写请求,先锁定用户区的物理内存,然后为其创建一个内 存描述表(MDL),并将该表的首指针存放于Irp对象的MdlAddress中,驱动程序可以通过调用MmGetSystemAddressForMdl获得用户区在系统空间中的地址。每当一个读请求被完 成时I/O管理器自动将该区域解锁。

1.3.7定时

  为了防止当设备出现某种故障时导致读写请求超时,或需要定时轮询某些设备的状态 ,驱动程序需要设置一些定时器。驱动程序中有两种方法可以设置定时器。一种是调用IoInitializeTimer将一个定时器过程IoTimer与一个设备对象联系起来。在调用IoStar tTimer后,系统将每一秒钟调用一次IoTimer,直至驱动程序调用IoStopTimer。如果需要设置更小间隔的定时器,需要用到被称为CustomTimerDpc的一种推迟过程调用机制。 它可以设置系统每隔一定时间将一个设置好的DPC对象放到DPC队列的末尾,执行一个指定的定时器Dpc过程。这个时间间隔可以精确到100ns。

1.3.8同步

  如果驱动程序有可能在某时刻有多个部分在同时运行,比如有中断处理过程,或存在多个设备等,对公共数据或代码的访问就需要同步。方法有:

i)自旋锁(SpinLock)

  驱动程序可以在初始化时调用KeInitializeSpinLock创建该对象。在任何代码段访问被保护的数据之前,先调用KeAcquireSpinLock试图获得该对象的所有权,如果成功,该段代码被系统提升至DISPATCH_LEVEL,进行数据访问。访问完毕后须调用KeRelease SpinLock释放所有权,运行级别也被恢复。此方法只适用于同步运行级别小于等于DISP ATCH_LEVEL的代码,主要用于多CPU的情形。此外,还有一种中断自旋锁用于与中断处理过程同步,可以将较低级别的代码提升到需要与之同步的中断DIRQL。

ii)控制器(Controller)

  该对象主要用于同步一个驱动程序中的多个设备,保证它们能顺序地访问特定的代码或数据。该对象在驱动程序初始化调用IoCreateController被创建。设备在StartIo过程中调用IoAllocateController请求获得Controller对象的独占权。使用完后调用IoFreeController释放。驱动程序停止时调用IoDeleteController从内存删除该对象。该对象有一个指针ControllerExtension指向一块由驱动程序定义的结构,其中保存有此驱程序的公共数据。

iii)适配器(Adapter)

  该对象用于同步多个设备(不一定在一个驱动程序中)对DMA通道的使用。该对象在系统启动侦测硬件时自动被创建。驱动程序在初始化时调用HalGetAdapter获得该对象的指针。设备在StartIo过程中调用IoAllocateAdapterChannel请求获得DMA通道的独占权,然后开始传输数据。使用完后调用IoFreeControllerChannel释放DMA通道。

iv)DPC

  由于DPC队列中的对象总是被系统顺序地处理,所以也可以将需要同步的代码做成Dpc过程,需要调用时将相应的DPC对象放到队列的末尾即可。

v)其他

  同用户模式的应用程序类似,驱动程序也可以使用多线程,也提供了一套用来同步的对象,如Event,Mutex,Semaphore,Timer,Thread。其中Event对象可以被命名,不同的驱动程序可以利用同名的Event对象同步对公共数据的访问。

1.3.9分层

  I/O管理器一个有用的功能是允许把一个驱动程序堆在另一个驱动程序之上。这样在分编写如网络驱动等有协议栈程序时,可以为各层编写相对独立的代码。当驱动程序需要在不同的平台上移植时,只需重新编写最下层的硬件驱动程序即可。高层驱动程序的另一个功能是可以对用户请求进行予处理,比如把较大的请求分割成较小的请求分多次传给给下层的程序。

1.3.10设备名及其符号连接

  WindowsNT系统维护着一个对象名字空间,把所有在系统内注册过的对象的名字分类存在一个树状空间里,用Win32SDK提供的WinObj工具可以浏览这个空间。如果希望设备能被用户的CreateFile函数打开,就需要在调用IoCreateDevice创建该设备对象时赋予 它一个名字,位于\Device\下,并调用IoCreateSymbolicLink在\DosDevices\下创建一个符号连接。这样,用户程序就能用CreateFile("\\\\.\\符号连接名",……)打开该设备,并获得其句柄。

1.4驱动程序的编译链接,调试、安装和启动

  WindowsNT下编写驱动程序的环境被称为为DDK(DeviceDriverKit)ForMicrosoftWindowsNT,这是一个命令行下的工作环境。但是在安装DDK之前需要安装Win32 SDK(SoftwareDevelopmentKit)以及MicrosoftVisualC++。

  编译链接器为Build.exe,他从配置文件Sources中读出待编译的程序的配置,包括源文件、目标文件等,从环境变量Include中得到引用文件的地址,然后调用Visual C++的编译链接器Nmake.exe进行实际的编译链接工作。日志文件build.log,build.wrn,build.err中分别记录了编译链接中执行的命令行,遇到的错误,遇到的警告。编译完成后的文件后缀为.sys.

  安装过程分两步:第一,将编译成的.sys文件拷贝到WindowsNT的System32\Drivers\下;第二,在注册表的HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\下创建与.sys文件同名的键,然后在之下创建名为Start,Type,ErrorControl的三个REG_DWORD类型的数值键。其中Start的键值控制该驱动程序在系统启动的哪个阶段被 启动。小于3的数设定该驱动程序在系统启动的某个阶段被自动启动;3表示需要管理员手动启动;4表示该程序被禁用。设置完毕后需要重新启动系统。

  手动启动和停止一个驱动程序需要使用控制面板(ControlPanel)中的设备(Device)图标。

  由于驱动程序的结构比较复杂,而且调试内核模式的代码需要两台安装有WindowsNT的计算机,比较麻烦,所以在编写一个较复杂的驱动程序的过程中应分步来进行测试。 在完成任何一部分工作后都应进行测试,以便及早地发现错误。根据本人的经验,驱动程序中的大多数错误都是由于不正确地访问内存造成的。比如使用未被初始化的指针,释放已经被释放的内存,在DISPATCH_LEVEL或以上的运行级别引用分页的内存。