Delphi研究之驱动开发篇(三)

    (注:本篇的原理部分均摘自罗云彬大侠翻译的驱动开发教程)
     在前面的两篇教程中我们写了三个玩具驱动程序,为什么说是玩具驱动呢?因为它们确确实实是驱动程序,而且也能完成一些有趣的功能,但是它们都不完整,没有同用户交流的功能,这一篇就让我们来完成一个简单的全功能驱动程序。
    在写程序之前,我们有必要了解一些基础知识。
在用户模式下,我们可以通过访问某个地址来直接调用dll中的函数,但是在内核模式下,从系统的稳定性考虑,这样做是非常危险的。所以,系统提供了和内核模式通讯的媒介--I/O管理器,它是I/O子系统的部件之一。I/O管理器将应用程序、系统部件和设备连接起来,并定义了一个架构来支持设备驱动程序。
    一般来说,用户模式的操作都被转换成了对具体硬件设备的I/O操作,仅对于某些设备,设备由驱动程序来创建和控制,这些设备就是虚拟设备。当然,创建这些设备并不意味着你创造了什么硬件,而仅仅是在内存中创建了一个新的对象而已。每个对象和一个物理设备或者逻辑设备对应,用于描述它们的特征。
    创建设备后,驱动程序告诉I/O管理器:“这里有个我控制的设备,如果你收到了操作这个设备的I/O请求的话,直接发给我好了,剩下的由我来搞定!”。驱动程序知道如何对自己管理的设备进行I/O操作,I/O管理器唯一的职责在于创建I/O请求并把它发送给适当的设备驱动程序。用户模式的代码不知道(也不必知道)其中的细节,也不用知道究竟是哪个驱动程序在管理哪个设备。
下面先让我们来看一下用户模式下的控制程序:

代码:
program VirToPhys;

{$APPTYPE CONSOLE}

uses
  SysUtils, Windows, WinSvc, Dialogs, nt_status;

const
  NUM_DATA_ENTRY =4;
  DATA_SIZE = sizeof(DWORD) * NUM_DATA_ENTRY;
  _DELETE = $10000;

var
  hSCManager:THANDLE;
  hService:THANDLE;
  acModulePath: array [0..MAX_PATH] of char;
  _ss:SERVICE_STATUS;
  hDevice:THANDLE;

  adwInBuffer: array [0..NUM_DATA_ENTRY] of DWORD;
  adwOutBuffer: array [0..NUM_DATA_ENTRY] of DWORD;
  dwBytesReturned:DWORD;
  IOCTL_GET_PHYS_ADDRESS: DWORD;
  lpTemp: PChar;
  iRetValue: boolean;

{生成控制码}  
function CTL_CODE(DeviceType, Func, Method, Access: DWORD): DWORD;
begin
  result := (((DeviceType) SHL 16) OR ((Access) SHL 14) OR ((Func) SHL 2) OR (Method));
end;

begin
  IOCTL_GET_PHYS_ADDRESS := CTL_CODE(FILE_DEVICE_UNKNOWN,
                            $800, METHOD_BUFFERED,
                            FILE_READ_ACCESS + FILE_WRITE_ACCESS);
  hSCManager := OpenSCManager(nil, nil, SC_MANAGER_ALL_ACCESS);
  if hSCManager <> 0 then
  begin
    GetFullPathName(PChar('VirtToPhys.sys'), sizeof(acModulePath), acModulePath, lpTemp);
    hService := CreateService(hSCManager, 'VirtToPhys', 'Virtual To Physical Address Converter',
                              SERVICE_START + SERVICE_STOP + _DELETE, SERVICE_KERNEL_DRIVER,
                              SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, acModulePath,
                              nil, nil, nil, nil, nil);
    if hService <> 0 then
    begin
      {驱动程序的DriverEntry过程将被调用}
      if StartService(hService, 0, lpTemp) = true then
      begin
        {驱动程序将接收IRP_MJ_CREATE I/O请求包(IRP)}
        hDevice := CreateFile('\\.\slVirtToPhys', GENERIC_READ+GENERIC_WRITE,
                              0, nil, OPEN_EXISTING, 0, 0);
        if hDevice <> INVALID_HANDLE_VALUE  then
        begin
          {准备送往驱动程序的数据包}
          adwInBuffer[0] := GetModuleHandle(nil);
          adwInBuffer[1] := GetModuleHandle('kernel32.dll');
          adwInBuffer[2] := GetModuleHandle('user32.dll');
          adwInBuffer[3] := GetModuleHandle('comctl32.dll');
          {驱动程序将接收IRP_MJ_DEVICE_CONTROL I/O请求包}
          iRetValue := DeviceIoControl(hDevice, IOCTL_GET_PHYS_ADDRESS,
                                       @adwInBuffer, sizeof(adwInBuffer),
                                       @adwOutBuffer, sizeof(adwOutBuffer),
                                       dwBytesReturned, nil);
          if (iRetValue = true) and (dwBytesReturned <> 0) then
          begin
            {取程序名}
            GetModuleFileName(adwInBuffer[0], acModulePath, sizeof(acModulePath));
            ShowMessage(Format('Modules          BASE     Physical'#13#10 +
                               '----------------------------------'#13#10 +
                               '%s    %8.8X %8.8X'#13#10 +
                               'kernel32.dll     %8.8X %8.8X'#13#10 +
                               'user32.dll       %8.8X %8.8X'#13#10 +
                               'comctl32.dll     %8.8X %8.8x',
                               [ExtractFileName(acModulePath), adwInBuffer[0], adwOutBuffer[0],
                                adwInBuffer[1], adwOutBuffer[1],
                                adwInBuffer[2], adwOutBuffer[2],
                                adwInBuffer[2], adwOutBuffer[2]]));
          end else
          begin
            ShowMessage('Can''t send control code to device.');
          end;
          {Driver will receive IRP of type IRP_MJ_CLOSE}
          CloseHandle(hDevice);
        end else
        begin
          ShowMessage('Device is not present.');
        end;
        {DriverUnload proc in our driver will be called}
        ControlService(hService, SERVICE_CONTROL_STOP, _ss);
      end else
      begin
        ShowMessage('Can''t start driver.');
      end;
      DeleteService(hService);
      CloseServiceHandle(hService);
    end else
    begin
      ShowMessage('Can''t register driver.');
    end;
  CloseServiceHandle(hSCManager);
  end else
  begin
    ShowMessage('Can''t connect to Service Control Manager.');
  end;
end.
      这个代码包括了注册和启动驱动程序,以及作为客户端程序和设备进行通讯的代码。代码将输入的数据发送给设备,并将设备返回的数据格式化并显示出来。
    驱动被装载后,VirtToPhys驱动程序创建了一个名为"devVirtToPhys"的设备,内部设备名称无法被Win32应用程序使用,因此,如果我们的驱动程序对应的设备对象希望被用户模式的代码打开的话,就必须在"\??"目录中创建一个符号连接,指向"\Device"目录中的设备对象,然后,当调用者需要获取设备句柄时,I/O管理器就能够找到它。
    VirtToPhys驱动程序在"\??"目录中创建了指向"devVirtToPhys"设备的符号连接"slVirtToPhys",真实设备的全名是"\Device\devVirtToPhys",这样,当StartService函数执行后,系统中就多了三个新的对象:"\Driver\VirtToPhys"驱动、"\Device\devVirtToPhys"设备和符号连接"\??\slVirtToPhys"。
    现在来看看控制程序源代码,当驱动被启动后,我们只需要使用CreateFile函数来打开驱动,以此获得一个文件句柄。函数原型如下:
    
代码:
function CreateFile(lpFileName: PChar; 
                    dwDesiredAccess, dwShareMode: DWORD;
                    lpSecurityAttributes: PSecurityAttributes;dwCreationDisposition, dwFlagsAndAttributes: DWORD;
                    hTemplateFile: THandle): THandle; stdcall;
这个函数可以创建或者打开一个已存在的对象,而不仅仅是文件。函数的参数描述如下:
◎ lpFileName--指向以0结尾的表示设备名称的字符串,这里要用到指向设备对象的符号连接名
◎ dwDesiredAccess--指定访问设备的方式,可以有两个取值:GENERIC_READ表示写操作,允许将数据写到设备中;GENERIC_WRITE表示读操作,允许从设备读取数据,这两个值可以合并起来使用
◎ dwShareMode--指定设备是否可以被共享,0表示设备不能被共享,这样并发的访问会失败,直到句柄被关闭为止;要共享设备的话,可以指定下面的两个值:FILE_SHARE_READ表示可以并发地读取设备,FILE_SHARE_WRITE表示可以并发地写设备
◎ lpSecurityAttributes--指向SECURITY_ATTRIBUTES结构的指针,在此无用,所以可以指定为NULL
◎ dwCreationDistribution--指明当文件存在或不存在时函数采取的动作,对于设备来说,这个参数应该使用OPEN_EXISTING
◎ dwFlagsAndAttributes--文件属性,在这里总是使用0
◎ hTemplateFile--指定文件模板的句柄,在这里总是使用0

    如果CreateFile函数成功地创建或者打开了指定的设备,那么返回值就是设备句柄,否则返回值是INVALID_HANDLE_VALUE。我们这样使用CreateFile函数:
代码:
hDevice := CreateFile('\\.\slVirtToPhys', GENERIC_READ+GENERIC_WRITE,0, nil, OPEN_EXISTING, 0, 0);
    来看看第一个参数,这是指向符号连接名称的字符串指针,名称格式是"\\.\slVirtToPhys","\\.\"是Win32中定义本地计算机的方法,CreateFile函数实际上是NtCreateFile函数的封装(位于\%SystemRoot%\System32\ntdll.dll中),后者将访问定向到系统服务中去(注意不要和Win32服务进程混淆)。
    NtCreateFile将本地计算机的别名"\\.\"用"\??"代替(这样\\.\slVirtToPhys就变成了\?? \slVirtToPhys)并调用内核的ObOpenObjectByName函数,通过符号连接名称,ObOpenObjectByName函数找到\Device\devVirtToPhys对象并返回它的指针。NtCreateFile使用这个指针创建新的文件对象并返回句柄。
    操作系统把所有的I/O操作请求抽象化成一个虚拟的文件,隐藏了目标设备的I/O操作可能在结构上并不等同于文件的事实,然后由驱动程序负责将对虚拟文件的请求转换成对具体硬件的请求,这种抽象可以推广到所有用户进程和设备之间的界面,所有对这种虚拟文件的读写操作都被当作是简单的流操作来处理。
    在CreateFile返回前,I/O管理器创建了IRP_MJ_CREATE类型的IRP并将其传给驱动处理,驱动中负责响应这个IRP的子程序代码会在发起I/O请求(就是调用CreateFile函数的代码)的线程环境中执行,该线程的IRQL等于PASSIVE_LEVEL。如果驱动的子程序成功返回,那么对象管理器在进程的句柄表中为文件对象创建一个句柄,然后将其一层层地返回,直到返回到CreateFile函数。
    现在来总结一下,"\\.\slVirtToPhys"会被转换成符号连接"\??\slVirtToPhys",最终用来找到需要的"\Device\devVirtToPhys"设备。然后可以从负责维护该设备的驱动程序中取得设备对象DEVICE_OBJECT,接下来I/O管理器将IRP_MJ_CREATE请求传递给驱动,驱动程序知道如何处理这个请求。如果驱动打算处理该请求,那么它返回成功代码,这样对象管理器创建对应这个设备的虚拟文件句柄并将它返回给用户模式代码。
    句柄和符号连接为间接访问系统资源提供服务,这种"间接"方式将应用程序和系统的数据结构隔离开来。
    CreateFile函数返回有效的设备句柄后,我们将它保存在hDevice变量中,现在可以用ReadFile、WriteFile以及DeviceIoControl函数来和设备通讯了。DeviceIoControl函数是用来和设备通讯的通用函数,它的原型如下:
代码:
function DeviceIoControl(hDevice: THandle; 
    dwIoControlCode: DWORD; 
    lpInBuffer: Pointer;          
    nInBufferSize: DWORD; 
    lpOutBuffer: Pointer;  
    nOutBufferSize: DWORD;
    var lpBytesReturned: DWORD; 
    lpOverlapped: POverlapped): BOOL; stdcall;
    DeviceIoControl函数的参数比CreateFile多,但用起来都很简单。
   ◎ hDevice--设备的句柄 
   ◎ dwIoControlCode--控制代码,指出要进行什么操作,详细内容后面再做解释
   ◎ lpInBuffer--指向包含操作所需的数据的缓冲区指针,如果控制代码指明的操作并不需要输入数据的话,这里可以用NULL
     ◎ nInBufferSize--lpInBuffer参数指向的缓冲区的大小
   ◎ lpOutBuffer--指向用来接收输出数据的缓冲区,如果dwIoControlCode指明的操作不产生输出数据的话,这里可以用NULL
    ◎ nOutBufferSize--lpOutBuffer参数指向的缓冲区的大小
   ◎ lpBytesReturned--指向一个变量,用来返回放入lpOutBuffer缓冲区的数据的数量
   ◎ lpOverlapped--指向OVERLAPPED结构,这个参数仅在异步操作的时候才需要。我们的操作是同步的(就是在驱动的过程返回前DeviceIoControl函数也不返回),所以在这里使用NULL
    设备驱动程序可以被当作内核模式函数包来看待,I/O控制代码就是用来指定访问其中的哪个函数的。DeviceIoControl函数的dwIoControlCode参数就是这个代码,它指出了我们需要进行的操作,以及如何进行操作。
控制代码是32位数字型常量,可以CTL_CODE函数来生成。控制代码的组在见下图:
 
    控制代码中各数据位字段的含义如下:

◎ DeviceType--设备类型(16bit)指出了设备的类型,微软保留了0-7FFFh的取值,剩下的8000h-0FFFFh供开发商定义新的内核模式驱动程序。我们可以在\include\w2k\ntddk.inc文件中找到一组FILE_DEVICE_XXX符号常量,这些值都是微软保留的值,我们可以使用其中的FILE_DEVICE_UNKNOWN。当然你也可以定义另外一个FILE_DEVICE_XXX值
◎ Access--存取代码(2bit)指明应用程序存取设备的方式,由于这个字段只有2位,所以只有4种可能性:
 FILE_ANY_ACCESS (0)--最大的存取权限,就是什么操作都可以
 FILE_READ_ACCESS (1)--读权限,设备将数据传递到指定的缓冲区
 FILE_WRITE_ACCESS (2)--写权限,可以从内存中向设备传递数据
 FILE_READ_ACCESS or FILE_WRITE_ACCESS (3)--读写权限,设备和内存缓冲区之间可以互相传递数据
◎ Function--功能代码(12bit)用来描述要进行的操作,我们可以用800h-0FFFh来定义自己的I/O控制代码,0-7FFh之间的值是被微软保留的,用来定义公用的I/O控制代码
◎ Method--缓冲模式(2bit)表示I/O管理器如何对输入和输出的数据进行缓冲,这个字段的长度是2位,所以有4种可能性:
 METHOD_BUFFERED (0)--对I/O进行缓冲
 METHOD_IN_DIRECT (1)--对输入不进行缓冲
 METHOD_OUT_DIRECT (2)--对输出不进行缓冲
 METHOD_NEITHER (3)--都不缓冲

    虽然进行缓冲会带来一些额外的内存开销,但却是最安全的,因为系统已经做好了相关的全部工作。在传输的数据小于一页(4Kb)的时候,驱动程序通常使用缓冲方式的I/O,因为对大量小块内存进行内存锁定带来的开销也是很大的。在VirtToPhys驱动程序中,我们使用带缓冲的方式。
    我们在adwInBuffer缓冲区中填写需要进行转换的虚拟地址。
    
代码:
adwInBuffer[0] := GetModuleHandle(nil);
      adwInBuffer[1] := GetModuleHandle('kernel32.dll');
      adwInBuffer[2] := GetModuleHandle('user32.dll');
      adwInBuffer[3] := GetModuleHandle('comctl32.dll');
      然后调用DeviceIoControl将缓冲区传给驱动程序,驱动程序会将虚拟地址转换成物理地址。如果DeviceIoControl函数成功返回,那么dwBytesReturned中的数值就等于驱动程序在adwOutBuffer缓冲区中返回的数据的长度,然后我们只要把返回值格式化一下并显示出来就可以了。
最后就是扫尾工作,调用CloseHandle关闭设备句柄。这时I/O管理器向设备驱动程序发送两个IRP,第一个是IRP_MJ_CLEANUP,它告诉驱动程序设备句柄将要被关闭了;然后是IRP_MJ_CLOSE,它告诉驱动程序设备句柄已经被关闭了。你可以在收到IRP_MJ_CLEANUP时返回一个错误代码,这样就可以阻止设备句柄被关闭。驱动程序的子程序在处理这些IRP时,代码都是在发出I/O请求的线程环境中执行的(也就是调用CloseHandle的线程),它们的IRQL = PASSIVE_LEVEL。
以上就是用户模式下的控制程序了,下面来看看我们的驱动程序。

代码:
unit VirtToPhys;

interface

uses
  nt_status, ntoskrnl, ntutils;

function _DriverEntry(pDriverObject:PDRIVER_OBJECT; pusRegistryPath:PUNICODE_STRING): NTSTATUS; stdcall;

implementation

const
  NUM_DATA_ENTRY = 4;
  DATA_SIZE = sizeof(DWORD) * NUM_DATA_ENTRY;

var
  g_usDeviceName, g_usSymbolicLinkName: UNICODE_STRING;

function DispatchCreateClose(p_DeviceObject:PDEVICE_OBJECT; p_Irp:PIRP): NTSTATUS; stdcall;
begin
  p_Irp^.IoStatus.Status := STATUS_SUCCESS;
  p_Irp^.IoStatus.Information := 0;

  IofCompleteRequest(p_Irp, IO_NO_INCREMENT);
  result := STATUS_SUCCESS;
end;

function DispatchControl(p_DeviceObject: PDEVICE_OBJECT; p_Irp:PIRP): NTSTATUS; stdcall;
var
  status:NTSTATUS;
  dwBytesReturned:DWORD;
  psl:PIO_STACK_LOCATION;
  IOCTL_GET_PHYS_ADDRESS: DWORD;
  pSystemBuffer: PULONG;
  iCnt: DWORD;
  liPhysicalAddress: PHYSICAL_ADDRESS;
begin
  dwBytesReturned := 0;

  IOCTL_GET_PHYS_ADDRESS := CTL_CODE(FILE_DEVICE_UNKNOWN,
    $800, METHOD_BUFFERED, FILE_READ_ACCESS + FILE_WRITE_ACCESS);


  psl := IoGetCurrentIrpStackLocation(p_Irp);

  if psl^.Parameters.DeviceIoControl.IoControlCode = IOCTL_GET_PHYS_ADDRESS then
  begin
    if (psl^.Parameters.DeviceIoControl.OutputBufferLength >= DATA_SIZE)
       and (psl^.Parameters.DeviceIoControl.InputBufferLength >= DATA_SIZE) then
    begin
      pSystemBuffer := p_Irp^.AssociatedIrp.SystemBuffer;
      iCnt := 0;
      for iCnt := 1 to NUM_DATA_ENTRY do
      begin
        liPhysicalAddress := MmGetPhysicalAddress(PVOID(pSystemBuffer^));
        pSystemBuffer^ := liPhysicalAddress.LowPart;
        inc(pSystemBuffer);
      end;
      dwBytesReturned := DATA_SIZE;
      status := STATUS_SUCCESS;
    end else
    begin
      status := STATUS_BUFFER_TOO_SMALL;
    end;
  end else
  begin
    status := STATUS_INVALID_DEVICE_REQUEST;
  end;
  
  p_Irp^.IoStatus.Status := status;
  p_Irp^.IoStatus.Information := dwBytesReturned;

  IofCompleteRequest(p_Irp, IO_NO_INCREMENT);
  result := status;
end;

procedure DriverUnload(p_DriverObject:PDRIVER_OBJECT); stdcall;
begin
  IoDeleteSymbolicLink(@g_usSymbolicLinkName);
  IoDeleteDevice(p_DriverObject^.DeviceObject);
end;


function _DriverEntry(pDriverObject:PDRIVER_OBJECT; pusRegistryPath:PUNICODE_STRING): NTSTATUS;
var
  status:NTSTATUS;
  pDeviceObject:TDeviceObject;
begin
  status := STATUS_DEVICE_CONFIGURATION_ERROR;
  RtlInitUnicodeString(g_usDeviceName, '\Device\devVirtToPhys');
  RtlInitUnicodeString(g_usSymbolicLinkName, '\??\slVirtToPhys');

  if (IoCreateDevice(pDriverObject, 0, @g_usDeviceName,
                     FILE_DEVICE_UNKNOWN, 0, FALSE,
                     pDeviceObject) = STATUS_SUCCESS) then
  begin
    if (IoCreateSymbolicLink(@g_usSymbolicLinkName,
                             @g_usDeviceName) = STATUS_SUCCESS) then
    begin
      pDriverObject^.MajorFunction[IRP_MJ_CREATE] := @DispatchCreateClose;
      pDriverObject^.MajorFunction[IRP_MJ_CLOSE] := @DispatchCreateClose;
      pDriverObject^.MajorFunction[IRP_MJ_DEVICE_CONTROL] := @DispatchControl;
      pDriverObject^.DriverUnload := @DriverUnload;

      status := STATUS_SUCCESS;
    end else
    begin
      IoDeleteDevice(@pDeviceObject);
    end;
  end;
  result := status;
end;

end.
  程序很简单,所以一行注释也没加^_^。既然驱动程序的主要作用是用于控制一些设备,包括物理设备、虚拟设备或者逻辑设备,那么我们首先必须将这些设备创建起来(本例中是虚拟设备),这可以通过调用IoCreateDevice函数来完成,函数将创建并初始化一个由驱动程序使用的设备对象(DEVICE_OBJECT结构),其原型如下:
  
代码:
function IoCreateDevice(DriverObject: PDRIVER_OBJECT;
                        DeviceExtensionSize: ULONG;
                        DeviceName: PUNICODE_STRING;
                        DeviceType: DEVICE_TYPE;
                        DeviceCharacteristics: ULONG;
                        Exclusive: BOOLEAN;
                        var DeviceObject:TDeviceObject):NTSTATUS; stdcall;
  函数的参数描述如下:
◎ DriverObject--指向驱动对象(DRIVER_OBJECT结构),每个驱动程序在DriverEntry过程中会通过参数收到一个指向它的驱动对象的指针
◎ DeviceExtensionSize--指定设备扩展结构的大小(注:I/O管理器将自动分配这个内存,并把设备对象中的DeviceExtension指针指向这块内存),扩展结构的数据结构定义由驱动程序自己决定,我们的驱动太简单了,就没必要使用这个了
◎ DeviceName--指向一个Unicode字符串,用来指定设备名称,该名称必须是全路径的名称,在这里路径的含义并不是指硬盘上的路径,而是指对象管理器命名空间中的路径。这个参数在本例中是必须的,因为我们必须创建一个命名的设备,否则就无法创建一个符号连接,那样用户模式的进程也就无法访问设备了。设备名称在系统中必须是唯一的(注:在其他的应用中,你也可以创建不命名的设备)
◎ DeviceType--在系统定义的FILE_DEVICE_XXX常数中选定一个,用于指定设备的类型,当然也可以使用自定义的类型来表示一个新的类别,这里我们使用FILE_DEVICE_UNKNOWN
◎ DeviceCharacteristics--指明设备的额外属性,本例中使用0
◎ Exclusive--指明设备对象是否必须被独占使用,也就是说同时只能有一个句柄可以向设备发送I/O请求,在CreateFile函数的dwShareMode参数中可以指明是否独占设备。我们并不需要独占设备,这样这里使用FALSE
◎ DeviceObject--指向一个变量,如果函数调用成功的话,变量中将返回指向新创建的设备对象(DEVICE_OBJECT结构)的指针。
接下来,如果对IoCreateSymbolicLink的调用失败的话,我们需要从系统中将设备删除,所以要将IoCreateDevice函数返回的设备对象指针保存起来,以便在删除设备的时候使用。
设备对象指针在卸载驱动的DriverUnload过程中也要用到,但是那时在驱动对象中也可以得到设备对象指针,所以没有必要专门定义一个全局变量将设备对象指针保留到那个时候。
如果设备被成功地创建,为了使它能被Windows子系统"看见",我们还需要调用IoCreateSymbolicLink函数创建符号链接,接下来就是指定分派过程:
代码:
pDriverObject^.MajorFunction[IRP_MJ_CREATE] := @DispatchCreateClose;
pDriverObject^.MajorFunction[IRP_MJ_CLOSE] := @DispatchCreateClose;
pDriverObject^.MajorFunction[IRP_MJ_DEVICE_CONTROL] := @DispatchControl;
每个驱动程序都包括一个过程入口指针数组,用来指明不同的I/O请求被分派到那个函数来处理。每个驱动程序必须至少设置一个过程入口,用来处理IRP_MJ_XXX类型的请求。不同的驱动程序可以设置多个不同的过程入口,用来处理不同的IRP_MJ_XXX请求代码。例如,如果你需要得到"系统将要关闭"的通知的话,就必须"申明"处理该请求的分派过程,也就是在驱动对象的MajorFunction表中的IRP_MJ_SHUTDOWN一栏中填入该分派过程的地址。如果不需要处理某个请求,那么什么都不用做,因为I/O管理器在调用DriverEntry前默认将MajorFunction表中的每一项都填成了系统内部的IopInvalidDeviceRequest过程的地址,该过程会返回一个错误代码。
所以,你的责任就是要为每个你想要响应的I/O代码提供分派过程。
在驱动中我们必须至少处理3种I/O请求包,每个内核模式的驱动程序必须支持功能码IRP_MJ_CREATE,这样才能响应Win32的CreateFile函数调用,没有这个分派过程的话,Win32应用程序将无法获取设备的句柄;同样,IRP_MJ_CLOSE也是必须被支持的,否则就无法响应Win32的CloseHandle调用;最后,IRP_MJ_DEVICE_CONTROL允许用户模式程序通过Win32的DeviceIoControl调用来和驱动程序通讯,所以也必须被支持。
下面是这些功能码的说明:
◎ IRP_MJ_CREATE--用户模式代码调用CreateFile函数来获取目标设备对象的文件对象句柄时,I/O管理器发送此代码
◎ IRP_MJ_DEVICE_CONTROL--用户模式代码调用DeviceIoControl函数时,I/O管理器发送此代码
◎ IRP_MJ_CLOSE--用户模式代码调用CloseHandle函数来关闭目标设备对象的文件对象句柄时,I/O管理器发送此代码

代码:
pDriverObject^.DriverUnload := @DriverUnload;
DriverUnload过程的意图在于清理DriverEntry过程申请的一些资源,如果驱动需要被动态卸载的话,我们就必须提供卸载进程的分派过程,当用户模式代码使用SERVICE_CONTROL_STOP参数调用ControlService函数时,该分派过程就会被调用。
如果调用IoCreateSymbolicLink失败,那么我们必须释放前面申请的一些资源,这里我们要删除前面用IoCreateDevice创建的设备对象,这可以通过调用IoDeleteDevice函数来完成。如果你还申请了别的一些资源的话,在这里也应该全部将它归还给系统。
请不要忘了,你必须随时留意你申请的内存和其他一些系统资源,在不需要再使用的话,要将它们释放掉。因为你现在是在内核模式下运行,这些清理工作必须自己完成,没人会帮你做这些事情。
最后,我们向系统返回状态代码,如果代码是STATUS_SUCCESS的话,驱动程序将保留在内存中,接下来I/O管理器会将对应的IRP请求发送给它;如果返回的是其他数值的话,系统会将驱动程序从内存中清除。
DriverEntry成功返回后,系统中多了三个新的对象,驱动"\Driver\VirtToPhys",设备"\Device\devVirtToPhys"以及到设备的符号连接"\??\slVirtToPhys"。
驱动对象描述了系统中存在的独立的驱动程序,I/O管理器通过驱动对象获取每个驱动中不同的分派过程的入口地址。
设备对象描述了系统中的一个设备,包括设备的各种特征。通过设备对象,I/O管理器得到管理这个设备的驱动对象的指针。
文件对象是设备对象在用户模式上的表现,通过文件对象,I/O管理器得到设备对象的指针。
符号连接对用户模式是可见的,它被对象管理器所使用。
下图显示了各对象之间的相互联系,它能帮你更彻底地理解后面的内容。
 
  
  I/O管理器调用分派过程来响应用户模式或者内核模式的请求,在单层或者多层中的最高层的驱动中,分派过程保证是在发起I/O请求的线程上下文中执行的,就像DriverEntry过程一样,分派过程也是在IRQL = PASSIVE_LEVEL下执行的,这意味着它们可以存取分页的系统资源。
所有的分派过程的申明如下:
代码:
Function DispatchRoutine(pDeviceObject: PDEVICE_OBJECT, 
pIrp:PIRP): NTSTATUS; stdcall;
  参数描述如下:
◎ pDeviceObject--指向设备对象(DEVICE_OBJECT结构),如果同一个驱动程序负责多个设备的话,从这个参数就能分辨出是哪个设备发送过来的IRP
◎ pIrp--指向描述I/O请求的IRP结构

I/O管理器创建一个IRP结构,用来描述I/O请求,并把它的指针通过pIrp参数传递给设备驱动程序,具体怎样处理就是设备驱动程序的事情了。
这种统一格式的接口的好处在于:I/O管理器可以用同样的方法调用任何的分派过程,而不需要知道驱动程序内部的细节知识(注:反过来想一下,如果不同分派过程的调用格式不同,那么I/O管理器必须知道所有的过程的调用格式和参数定义)。
为什么不同类型的IRP可以用同一个分派过程来处理呢?这是因为在我们这个简单的驱动程序中,唯一要在IRP_MJ_CREATE和IRP_MJ_CLOSE中要做的事情就是将IRP标记为已处理。
如果两者的处理方法不同的话,你还是应该创建独立的DispatchCreate的DispatchClose过程。
前面已经说过,处理IRP_MJ_CREATE是为了响应CreateFile的调用,如果不处理这个代码的话,Win32应用程序将无法获取设备句柄;同样处理IRP_MJ_CLOSE代码是为了响应对CloseHandle的调用。

代码:
p_Irp^.IoStatus.Status := STATUS_SUCCESS;
p_Irp^.IoStatus.Information := 0;
我们填写I/O状态块来表示IRP的处理结果。
I/O状态块的Information字段被设置为0,表示设备句柄可以被打开。该字段对关闭的请求来说没有什么含义,但对其他的请求可能有不同的含义。
返回值决定了CreateFile或CloseHandle的调用是否成功返回,所以我们要在这里填写STATUS_SUCCESS。

代码:
IofCompleteRequest(p_Irp, IO_NO_INCREMENT);
result := STATUS_SUCCESS;
现在必须调用IoCompleteRequest函数来表示驱动程序已经完成了IRP的处理,并将IRP返回给I/O管理器;然后返回STATUS_SUCCESS表示设备已经可以接收另一个I/O请求的处理了。
IoCompleteRequest的第一个参数告诉I/O管理器哪个IRP已经被处理完毕,第二个参数返回一个系统定义的表示实时优先级的常数,这是驱动程序为了补偿其他线程(在驱动的执行中)进行等待而给予的瞬间的优先级提高,例如对于音频设备,DDK建议使用IO_SOUND_INCREMENT值(等于8)。
例子程序中使用IO_NO_INCREMENT(等于0),也就是说当前线程的优先级保持不变。
I/O管理器提供了3种缓冲管理方式:buffered方式、direct方式和neither方式。三种方式在这里我就不详细介绍了,这方面的资料很多^_^。
    程序的其他部分都很简单,大家一看就能明白,这里还有两个地方要说明一下:
    一是关于虚拟地址转换成物理地址的问题,程序中我们使用了ntoskrnl.exe中的MmGetPhysicalAddress,其函数原型在C语言中定义如下:
   
代码:
NTKERNELAPI PHYSICAL_ADDRESS MmGetPhysicalAddress (IN PVOID BaseAddress);
    这里PHYSICAL_ADDRESS是LARGE_INTEGER的别名,LARGE_INTEGER是一个结构,转换成Pascal记录后定义如下:
   
代码:
TLargeInteger=record
       LowPart:Cardinal;
       HighPart:Integer;
    end;
    当执行liPhysicalAddress := MmGetPhysicalAddress(PVOID(pSystemBuffer^))这条语句时,由于返回的是记录,delphi会把返回值liPhysicalAddress当成MmGetPhysicalAddress的第二个参数压入堆栈,程序编译后会生成如下的代码:
    
代码:
move ax, [pSystemBuffer] 
      push eax
      lea  eax,liPhysicalAddress
      push eax
      call  MmGetPhysicalAddress
    这显然是不对的,这样不但的不到想要的结果,还会破坏堆栈,结果就是可爱的蓝屏。此处我写了个Delphi包装函数解决这个问题,就像下面这样:
代码:
function krnlMmGetPhysicalAddress(BaseAddress: PVOID): PHYSICAL_ADDRESS; external NtKernel name '_MmGetPhysicalAddress';{引入MmGetPhysicalAddress}

function MmGetPhysicalAddress(BaseAddress: PVOID): PHYSICAL_ADDRESS;
var
  liPa :PHYSICAL_ADDRESS;
begin
  liPa.LowPart := 0;
  liPa.HighPart := 0;
  asm
    pushad {保存寄存器,内核程序安全第一,有把握可以不要^_^}
    push BaseAddress
    call krnlMmGetPhysicalAddress
    mov liPa.LowPart, eax
    popad
  end;
  result := liPa;
end;
    实际上MmGetPhysicalAddress也只返回了一个32位整数到EAX中,可不知道为什么microsoft要把返回值定义成LARGE_INTEGER,可能是为了今后扩展吧。
   二是关于fastcall调用,内核APIs有一部分是fastcall调用约定的,Delphi也支持fastcall调用约定,就是register call,但是register call和microsoft的fastcall是有区别的,microsoft的fastcall使用两个寄存器ECX和EDX保存前两个参数,其余的按从右到左的顺序压栈传送;而register call使用三个寄存器EAX、EDX、ECX传送头三个参数,其余按从左到右的顺序压栈传送。所以对使用fastcall调用约定的APIs也必须用delphi做个包装函数,举例如下:
代码:
procedure krnlObfDereferenceObject(_Object: PVOID); register; external NtKernel name '_ObfDereferenceObject';
procedure ObfDereferenceObject(_Object: PVOID); assembler;
asm
  mov ecx, _Object
  call krnlObfDereferenceObject
end;
    makefile就不列出了,附件包含了本篇教程的相关源码以及KmdKit4D新增的一些常量、结构、APIs的定义。
上传的附件 KmdKit4D_patch0114.rar[附件请到论坛下载:http://bbs.pediy.com/showthread.php?t=58301 ]