【转帖】驱动开发之一 --- 创建一个简单的设备驱动 【译文】

驱动开发之一 --- 创建一个简单的设备驱动 【译文】
2008-04-19 15:50
在这里,我们将描述如何创建一个简单的设备驱动程序,动态的加载和卸载,以及在用户模式与之通讯。

理论:
在开始之前,我们需要了解几个概念:
我们知道一个程序经过编译器和连接器,最终生成一个一定格式二进制文件,这个格式可以被操作系统识别。在windows系统中,这个格式就是PE格式。在这个格式中,有一个描述项称为subsystem(子系统),它和PE文件头中的其他项,描述了如何加载一个可执行文件。

很多人使用VC++ IDE环境来创建工程,都是使用IDE对编译器和连接器默认设置选项。因而对子系统可能不了解。你曾经写过控制台程序吗?曾经写过图形界面的WINDOWS应用程序吗?Windows控制台程序和图形界面程序, 就是不同的子系统。这两者经过编译连接后生成的PE文件含有不同的子系统信息。这也是为什么控制台程序使用"main"函数,而windows图形界面程序使用"WinMain“函数。当你在VC IDE中创建这两类程序时,vc++简单的生成一个工程,对应的子系统信息是/subsystem:console 或者/subsystem:windows。如果你不小心建错了这两种工程,只要修改下这个连接选项菜单就行了,而不需要重新创建一个新的工程。如图:

而驱动程序使用不同的子系统,驱动程序对应的子系统设置是/subsystem:native。“NATIVE"并不是专用于驱动程序,也可以用于用户模式应用,即:
native application 。我们知道在windows下写程序,无非就是两种程序,一种是应用程序,一种是驱动程序。其实还存在第三种程序:Native Application。具体可以参考文章Inside Native Applications》。

Native Application使用native api,直接与系统内核交互;它的运行时机是在WINNT CORE启动之后,在驱动程序和WIN32等子系统加载之前运行。

Native api数量有限,而且大部分是undocumented,这些函数都封装在ntdll.dll中, 网上有本电子书叫做《Windows NT 2000 Native API Reference》,写了这些函数的介绍,有兴趣可以搜索看看。

Native application的入口函数不是mainwinmainDriverEntry,而是NtProcessStartup

void   NtProcessStartup (PSTARTUP_ARGUMENT Argument);

Native application自己不会返回,所以需要在NtProcessStartup的最后,自己结束线程:

NtTerminateProcess(NtCurrentProcess(), 0);

native application其实也是很简单的:


void NtProcessStartup(PSTARTUP_ARGUMENT Argument)
{
NTSTATUS status;

status = ChangeHostName();
if (status != STATUS_SUCCESS)
{
   swprintf(g_sMsg, L"[sxg] Change computer name failed/n");
   NtDisplayString(g_sMsg);
}
else
{
   swprintf(g_sMsg, L"[sxg] Change computer name success/n");
   NtDisplayString(g_sMsg);
}

NtTerminateProcess(NtCurrentProcess(), 0);
}

1. 调用ntdll.dll中的函数,完成你想要的功能

2. 利用DDK进行编译,以下是makefilesources的内容

MAKEFILE文件:

!INCLUDE $(NTMAKEENV)/makefile.def

SOURCES文件:

TARGETNAME=hostnamex

TARGETPATH=obj

TARGETTYPE=PROGRAM

INCLUDES=$(DDK_INC_PATH)

SOURCES= hostnamex.c

编译生成可执行程序,此处为hostnamex.exe

3. hostnamex.exe拷贝到系统的system32目录中

4. 修改注册表值HKLM/System/CurrentControlSet/Control/Session Manager/BootExecute,增加字符串hostnamex

重新启动之后,我们的native application就会运行了。

值得注意的是,native application需要自行进行堆的管理。例如:


RTL_HEAP_DEFINITION heapParams;   
memset( &heapParams, 0, sizeof( RTL_HEAP_DEFINITION ));  
heapParams.Length = sizeof( RTL_HEAP_DEFINITION );  
Heap = RtlCreateHeap( 2, 0, 0x100000, 0x1000, 0, &heapParams );


 

native application应用还是比较广的,例如瑞星的开机杀毒软件,就是一个典型的native application.

 

 

关于native application得更多信息, MJ0011大牛翻译过一篇文章,《深入Native应用程序》大家可以借鉴。


大部分的驱动开发者和微软都约定俗成的使用“DriverEntry"作为入口点。即:
#pragma comment(linker,"/entry:DriverEntry")
大家是否还记得,动态链接Dll的子系统设置也是/subsystem:windows ,只不过它还有一个附加的开关/dll .对于驱动也有一个类似的开关,即/driver:WDM。连接器生成最后的二进制码,并且基于PE头中的选项以及怎样加载这个二进制码来定义加载系统的行为。加载系统进行不同层级的验证。因此对于驱动开发,我们的连接设置为:
/subsystem:native /driver:WDM /entry:DriverEntry

下面谈谈IRQL(Interrupt Request Level). 提到IRQL,在MSDN中有20页关于IRQL地描述。简单的说IRQL就是中断执行的优先等级。若某个中断产生了,且IRQL等于或小于目前处理器的IRQL设置。那么它将不会影响目前的程序执行。反之,若中断的IRQL高于目前处理器的IRQL设置,那么将会造成目前的执行中断,而去执行中断的代码。 总之,较高优先级的中断会中断较低优先级的中断。当这个情况发生时,所有其他等于或者小于这个IRQL的中断都将成为等待状态。透过KeGetCurrentIRQL()这个系统例程可以得到目前处理器的IRQL.可用的IRQL如下:
Software IRQL
  • PASSIVE_LEVEL 0 // Passive release level
  • LOW_LEVEL 0 // Lowest interrupt level
  • APC_LEVEL 1 // APC interrupt level
  • DISPATCH_LEVEL 2 // Dispatch level
Hardware IRQL
  • DIRQL: from 3 to 26 for device ISR
  • PROFILE_LEVEL 27, 0x1B // Timer used for profiling.
  • CLOCK1_LEVEL 28, 0x1C // Interval clock 1 level - not used on x86
  • CLOCK2_LEVEL 28, 0x1C // Interval clock 2 level
  • SYNCH_LEVEL 28, 0x1C // Synchronization level
  • IPI_LEVEL 29, 0x1D // Interprocessor interrupt level
  • POWER_LEVEL 30, 0x1E // Power failure level
  • HIGH_LEVEL 31, 0x1F // Highest interrupt leve
数值越大代表其IRQL的优先级越高。在驱动开发中,常见的有:
PASSIVE_LEVEL.
APC_LEVEL
DISPATCH_LEVEL
DIRQL(Device IRQL)

在msdn中介绍每一个kernel API时,都描述
了你可以在什么样的IRQL下使用这个API. 例如: DriverEntry执行在PASSIVE_LEVEL。
PASSIVE_LEVEL: 这是最低级别的IRQL, 没有中断可以被屏蔽,用户模式的线程执行在这个级别。分页内存可以访问。
APC_LEVEL: 处理器执行在这个优先级别上时,只有apc级别的中断可以被屏蔽。这个级别上可以执行异步过程调用APC. 分页内存可以访问。当一个APC(异步过程调用)出现时,处理器就被提升到APC_LEVEL级别。这样抑制了其他APCS的产生。驱动可以被人为提升到这个级别为了执行一些APCS的同步。
DISPATCH_LEVEL: 处理器执行在这个优先级别上时, DPC及其以下级别的中断可以被屏蔽。分页内存不能访问,只能访问非分页内存。由于只能使用非分页内存,因此当你在这个级别时,可以使用的API大大减少了。
DIRQL(Device IRQL): 通常上层驱动不会处理这个优先级的IRQL.
IRQL 仅仅解决了单个处理器中的同步问题, 使用SpinLock可以解决在多个处理器中的同步问题。DDK提供了两组函数。
  • KeAcquireSpinLock()
  • KeReleaseSpinLock()

  • KeAcquireSpinLockAtDpcLevel()
  • KeReleaseSpinLockAtDpcLevel()
几乎所有的Windows2000/XP/2003的I/O都是包驱动的,系统采取一种称为“I/O请求包(IRP--­I/O request packet)”的数据结构来完成与内核模式驱动程序通信。IRP由I/O管理器根据用户态程序提出的请求创建并传给相应的驱动程序。在分层的驱动程序中,这个过程很复杂,一个IRP常常要穿越几层驱动程序。另外,驱动程序也能够建立新的IRP并传递给其它驱动程序。 IRP功能的复杂性决定了它结构的复杂性,正确的理解IRP的结构是理解WDM框架的基础。IRP由I/O管理器在非分页内存池内创建,它包括两部分:头部区域和I/O堆栈位置。

IRP头部可见字段

AssociatedIrp.SystemBuffer      设备执行缓冲I/O时,指向系统空间缓冲区

PMDL MdlAddress                    设备执行直接I/O时,指向用户空间的内存描述表

IO_STATUS_BLOCK IoStatus   包含了I/O请求的状态;驱动程序在最终完成请求时设置这个结构。IoStatus.Status域将收到一个NTSTATUS代码。

PVOID UserBuffer                    对于METHOD_NEITHER方式的

IRP_MJ_DEVICE_CONTROL请求,该域包含输出缓冲区的用户模式虚拟地址。该域还用于保存读写请求缓冲区的用户模式虚拟地址,但指定了DO_BUFFERED_IO或DO_DIRECT_IO标志的驱动程序,其读写例程通常不需要访问这个域。当处理一个METHOD_NEITHER控制操作时,驱动程序能用这个地址创建自己的MDL。

BOOLEAN Cancel                     指示IRP已经被取消

 

I/O堆栈位置

MajorFunction                           指示执行什么I/O操作以及如何解释Parameters 字段

MinorFunction                           由文件系统合SCSI驱动程序使用

Parameters                               MajorFunction代码决定此联合的内容

DeviceObject                             I/O请求的目标设备

FileObject                                 请求的文件对象

 

       上层驱动调用IoCallDriver,将DeviceObject成员设置成下层驱动目标设备对象。当上层驱动完成IRP时,IoCompletion 函数被调用,I/O管理器传送给IoCompletion函数一个指向上层驱动的设备对象的指针。
为了便于理解
IRP 和 IO_STACK_LOCATION的关系 ,我们举个例子,有三个人,他们分别是木工、管道工和电工,他们要一起建造一所房子,他们需要有一个总体的设计和一组工具就像他们的工具箱。我们可以把这个看作是IRP. 为了建造房子他们每个人都有自己的工作,例如管道工,需要设计需要多少管材,在哪里铺设等等。木工需要搭建整个房子的框架。每一个人的工作可以看作是 IO_STACK_LOCATION。整个的IRP是建造一所房子,每个人的工作是 IO_STACK_LOCATION,当每个人都完成了自己的工作,IRP就完成了。

例子:
/**********************************************************************
*
* Toby Opferman
*
* Driver Example
*
* This example is for educational purposes only. I license this source
* out for use in learning how to write a device driver.
*
*     Driver Entry Point
**********************************************************************/
#define _X86_


#include <wdm.h>
#include "example.h"


   
VOID Example_Unload(PDRIVER_OBJECT DriverObject);   
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath);

/*
* These compiler directives tell the Operating System how to load the
* driver into memory. The "INIT" section is discardable as you only
* need the driver entry upon initialization, then it can be discarded.
*
*/
#pragma alloc_text(INIT, DriverEntry)
#pragma alloc_text(PAGE, Example_Unload)

/**********************************************************************
*
* DriverEntry
*
*    This is the default entry point for drivers. The parameters
*    are a driver object and the registry path.
*
**********************************************************************/
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    UINT uiIndex = 0;
    PDEVICE_OBJECT pDeviceObject = NULL;
    UNICODE_STRING usDriverName, usDosDeviceName;

    DbgPrint("DriverEntry Called /r/n");

    RtlInitUnicodeString(&usDriverName, L"//Device//Example");
    RtlInitUnicodeString(&usDosDeviceName, L"//DosDevices//Example");

    NtStatus = IoCreateDevice(pDriverObject, 0, &usDriverName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &pDeviceObject);

    if(NtStatus == STATUS_SUCCESS)
    {

        /*
         * The "MajorFunction" is a list of function pointers for entry points into the driver.
         * You can set them all to point to 1 function, then have a switch statement for all
         * IRP_MJ_*** functions or you can set specific function pointers for each entry
         * into the driver.
         *
         */
        for(uiIndex = 0; uiIndex < IRP_MJ_MAXIMUM_FUNCTION; uiIndex++)
             pDriverObject->MajorFunction[uiIndex] = Example_UnSupportedFunction;
   
        pDriverObject->MajorFunction[IRP_MJ_CLOSE]             = Example_Close;
        pDriverObject->MajorFunction[IRP_MJ_CREATE]            = Example_Create;
        pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]    = Example_IoControl;
        pDriverObject->MajorFunction[IRP_MJ_READ]              = Example_Read;
        pDriverObject->MajorFunction[IRP_MJ_WRITE]             = USE_WRITE_FUNCTION;
   
        /*
         * Required to unload the driver dynamically. If this function is missing
         * the driver cannot be dynamically unloaded.
         */
        pDriverObject->DriverUnload = Example_Unload;

        /*
         * Setting the flags on the device driver object determine what type of I/O
         * you wish to use.
         *  
         * Direct I/O - MdlAddress describes the Virtual Address list. This is then
         *                 used to lock the pages in memory.
         *
         *                 PROS: Fast, Pages are not copied.
         *                 CONS: Uses resources, needs to lock pages into memory.
         *
         * Buffered I/o - SystemBuffer is then used by the driver to access the data. The I/O
         *                   manager will copy the data given by the user mode driver into this buffer
         *                   on behalf of the driver.
         *
         *                   CONS: Slower operation (Use on smaller data sets)
         *                         Uses resources, allocates Non-paged memory
         *                         Large allocations may not work since it would
         *                         require allocating large sequential non-paged memory.
         *                   PROS: Easier to use, driver simply accesses the buffer
         *                         Usermode buffer is not locked in memory
         *
         *
         * Neither Buffered or Direct - This is when you simply read the buffer directly using the user-mode address.
         *                     Simply omit DO_DIRECT_IO and DO_BUFFERED_IO to perform this action.
         *
         *                     PROS: No copying or locking pages occurs.
         *
         *                     CONS: You *MUST* be in the context of the user-mode thread that made the request.
         *                           being in another process space you the page tables would not point to
         *                           the same location.
         *                           You have to perform some checking and probeing in order to verify
         *                           when you can read/write from the pages.
         *                           You cannot access a user mode address unless it's locked into memory
         *                           at >= DPC level.
         *                           The usermode process could also change the access rights of the
         *                           buffer while the driver is trying to read it!
         *
         *
         * If your driver services lower level drivers you will need to set this field to the same type of
         * I/O.
         *
         * The flags for Read/Write is:
         *      DO_BUFFERED_IO, DO_DIRECT_IO, Specify neither flag for "Neither".
         *
         * The flags (defined in the IOCTL itself) for Control I/O is:
         *     METHOD_NEITHER, METHOD_BUFFERED, METHOD_IN_DIRECT or METHOD_OUT_DIRECT
         *
         * From MSDN:
         *    For IRP_MJ_READ and IRP_MJ_WRITE requests, drivers specify the I/O method by using flags in each
         *    DEVICE_OBJECT structure. For more information, see Initializing a Device Object.
         *
         *    For IRP_MJ_DEVICE_CONTROL and IRP_MJ_INTERNAL_DEVICE_CONTROL requests, the I/O method is determined
         *    by the TransferType value that is contained in each IOCTL value. For more information, see Defining
         *    I/O Control Codes.
         */
        
        pDeviceObject->Flags |= IO_TYPE;
   
        /*
         * We are not required to clear this flag in the DriverEntry as the I/O Manager will
         * clear it for us, but we will anyway. Creating a device in any other location we
         * would need to clear it.
         */
   
        pDeviceObject->Flags &= (~DO_DEVICE_INITIALIZING);
   
        /*
         * Create a Symbolic Link to the device. Example -> /Device/Example
         */
   
        IoCreateSymbolicLink(&usDosDeviceName, &usDriverName);
    }


    return NtStatus;
}


/**********************************************************************
*
* Example_Unload
*
*    This is an optional unload function which is called when the
*    driver is unloaded.
*
**********************************************************************/
VOID Example_Unload(PDRIVER_OBJECT DriverObject)
{   
   
    UNICODE_STRING usDosDeviceName;
   
    DbgPrint("Example_Unload Called /r/n");
   
    RtlInitUnicodeString(&usDosDeviceName, L"//DosDevices//Example");
    IoDeleteSymbolicLink(&usDosDeviceName);

    IoDeleteDevice(DriverObject->DeviceObject);
}

 

I/O堆栈位置的主要目的是,保存一个I/O请求的函数代码和参数。而I/O堆栈数量实际上就是参与I/O请求的I/O层的数量。在一个IRP中,上层驱动负责负责为下层驱动设置堆栈位置指针。驱动程序可以为每个IRP调用IoGetCurrentIrpStackLocation来获得指向其自身堆栈位置的指针,而上层驱动程序必须调用IoGetNextIrpStackLocation来获得指向下层驱动程序堆栈位置的指针。因此,上层驱动可以在传送IRP给下层驱动之前设置堆栈位置的内容。

 

驱动开发之一 --- 创建一个简单的设备驱动(续) 【译文】
2008-04-20 16:11
接上继续。
/**********************************************************************
*
* Toby Opferman
*
* Driver Example
*
* This example is for educational purposes only. I license this source
* out for use in learning how to write a device driver.
*
*     Driver Functionality
**********************************************************************/

#define _X86_

#include <wdm.h>
#include "example.h"


/**********************************************************************
* Internal Functions
**********************************************************************/
BOOLEAN Example_IsStringTerminated(PCHAR pString, UINT uiLength);


#pragma alloc_text(PAGE, Example_Create)
#pragma alloc_text(PAGE, Example_Close)
#pragma alloc_text(PAGE, Example_IoControl)
#pragma alloc_text(PAGE, Example_Read)
#pragma alloc_text(PAGE, Example_WriteDirectIO)
#pragma alloc_text(PAGE, Example_WriteBufferedIO)
#pragma alloc_text(PAGE, Example_WriteNeither)
#pragma alloc_text(PAGE, Example_UnSupportedFunction)
#pragma alloc_text(PAGE, Example_IsStringTerminated)




/**********************************************************************
*
* Example_Create
*
*    This is called when an instance of this driver is created (CreateFile)
*
**********************************************************************/
NTSTATUS Example_Create(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    DbgPrint("Example_Create Called /r/n");

    return NtStatus;
}

/**********************************************************************
*
* Example_Close
*
*    This is called when an instance of this driver is closed (CloseHandle)
*
**********************************************************************/
NTSTATUS Example_Close(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    DbgPrint("Example_Close Called /r/n");

    return NtStatus;
}



/**********************************************************************
*
* Example_IoControl
*
*    This is called when an IOCTL is issued on the device handle (DeviceIoControl)
*
**********************************************************************/
NTSTATUS Example_IoControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    DbgPrint("Example_IoControl Called /r/n");

    return NtStatus;
}



/**********************************************************************
*
* Example_Read
*
*    This is called when a read is issued on the device handle (ReadFile/ReadFileEx)
*
**********************************************************************/
NTSTATUS Example_Read(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    DbgPrint("Example_Read Called /r/n");

    return NtStatus;
}



/**********************************************************************
*
* Example_WriteDirectIO
*
*    This is called when a write is issued on the device handle (WriteFile/WriteFileEx)
*
*    This version uses Direct I/O
*
**********************************************************************/
NTSTATUS Example_WriteDirectIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    PIO_STACK_LOCATION pIoStackIrp = NULL;
    PCHAR pWriteDataBuffer;

    DbgPrint("Example_WriteDirectIO Called /r/n");
   
    /*
     * Each time the IRP is passed down the driver stack a new stack location is added
     * specifying certain parameters for the IRP to the driver.
     */
    pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
   
    if(pIoStackIrp)
    {
        pWriteDataBuffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
   
        if(pWriteDataBuffer)
        {                            
            /*
             * We need to verify that the string is NULL terminated. Bad things can happen
             * if we access memory not valid while in the Kernel.
             */
           if(Example_IsStringTerminated(pWriteDataBuffer, pIoStackIrp->Parameters.Write.Length))
           {
                DbgPrint(pWriteDataBuffer);
           }
        }
    }

    return NtStatus;
}

/**********************************************************************
*
* Example_WriteBufferedIO
*
*    This is called when a write is issued on the device handle (WriteFile/WriteFileEx)
*
*    This version uses Buffered I/O
*
**********************************************************************/
NTSTATUS Example_WriteBufferedIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    PIO_STACK_LOCATION pIoStackIrp = NULL;
    PCHAR pWriteDataBuffer;

    DbgPrint("Example_WriteBufferedIO Called /r/n");
   
    /*
     * Each time the IRP is passed down the driver stack a new stack location is added
     * specifying certain parameters for the IRP to the driver.
     */
    pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
   
    if(pIoStackIrp)
    {
        pWriteDataBuffer = (PCHAR)Irp->AssociatedIrp.SystemBuffer;
   
        if(pWriteDataBuffer)
        {                            
            /*
             * We need to verify that the string is NULL terminated. Bad things can happen
             * if we access memory not valid while in the Kernel.
             */
           if(Example_IsStringTerminated(pWriteDataBuffer, pIoStackIrp->Parameters.Write.Length))
           {
                DbgPrint(pWriteDataBuffer);
           }
        }
    }

    return NtStatus;
}

/**********************************************************************
*
* Example_WriteNeither
*
*    This is called when a write is issued on the device handle (WriteFile/WriteFileEx)
*
*    This version uses Neither buffered or direct I/O. User mode memory is
*    read directly.
*
**********************************************************************/
NTSTATUS Example_WriteNeither(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    PIO_STACK_LOCATION pIoStackIrp = NULL;
    PCHAR pWriteDataBuffer;

    DbgPrint("Example_WriteNeither Called /r/n");
   
    /*
     * Each time the IRP is passed down the driver stack a new stack location is added
     * specifying certain parameters for the IRP to the driver.
     */
    pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
   
    if(pIoStackIrp)
    {
        /*
         * We need this in an exception handler or else we could trap.
         */
        __try {
       
                ProbeForRead(Irp->UserBuffer, pIoStackIrp->Parameters.Write.Length, TYPE_ALIGNMENT(char));
                pWriteDataBuffer = Irp->UserBuffer;
           
                if(pWriteDataBuffer)
                {                            
                    /*
                     * We need to verify that the string is NULL terminated. Bad things can happen
                     * if we access memory not valid while in the Kernel.
                     */
                   if(Example_IsStringTerminated(pWriteDataBuffer, pIoStackIrp->Parameters.Write.Length))
                   {
                        DbgPrint(pWriteDataBuffer);
                   }
                }

        } __except( EXCEPTION_EXECUTE_HANDLER ) {

              NtStatus = GetExceptionCode();    
        }

    }

    return NtStatus;
}
                      

/**********************************************************************
*
* Example_UnSupportedFunction
*
*    This is called when a major function is issued that isn't supported.
*
**********************************************************************/
NTSTATUS Example_UnSupportedFunction(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    NTSTATUS NtStatus = STATUS_NOT_SUPPORTED;
    DbgPrint("Example_UnSupportedFunction Called /r/n");

    return NtStatus;
}


/**********************************************************************
*
* Example_IsStringTerminated
*
*    Simple function to determine a string is NULL terminated.
*
**** We could validate also the characters in the string are printable! ***
*
**********************************************************************/
BOOLEAN Example_IsStringTerminated(PCHAR pString, UINT uiLength)
{
    BOOLEAN bStringIsTerminated = FALSE;
    UINT uiIndex = 0;

    while(uiIndex < uiLength && bStringIsTerminated == FALSE)
    {
        if(pString[uiIndex] == '/0')
        {
            bStringIsTerminated = TRUE;
        }
        else
        {
           uiIndex++;
        }
    }

    return bStringIsTerminated;
}

分析:
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath);
DRIVER_OBJECT是一个数据结构,用来代表一个驱动。DriverEntry例程使用它来设置其他处理特殊I/O请求的驱动入口点。
在这个结构中还有一个指向DEVICE_OBJECT的指针。DEVICE_OBJECT用来代表一个特定的设备。一个驱动实际上可以创建多个设备。
这些设备组成一个链表,存放在DEVICE_OBJECT.NextDevice.
pRegistryPath:是一个字符串指针,指出了驱动信息在注册表位置。驱动程序可以利用它在注册表中存储一些特殊的信息。
内核程序使用IopLoadDriver来加载驱动,读读这个函数,可以完全了解DRIVER_OBJECT结构中各项的意义。

下一步我们来创建一个设备,你或许奇怪我们如何来创建设备,创建什么类型的设备?平时我们谈到驱动都是与硬件相关的
,如声卡驱动,网卡驱动等等。其实驱动有不同的类型,分别工作在不同的层级,并不是所有的驱动都是直接与硬件打交道的。
由于驱动是分层的,最高层的驱动与用户态通讯。最底层的驱动与硬件打交道。每一层驱动都有自己的堆栈他们会将请求分解
成为一个对于更底层的驱动而言更加易于处理的一个请求。

我们来看看我们自己的“DriverEntry”的第一部分吧。
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
UINT uiIndex = 0;
PDEVICE_OBJECT pDeviceObject = NULL;
UNICODE_STRING usDriverName, usDosDeviceName;

DbgPrint("DriverEntry Called /r/n");

RtlInitUnicodeString(&usDriverName, L"//Device//Example");
RtlInitUnicodeString(&usDosDeviceName, L"//DosDevices//Example");

NtStatus = IoCreateDevice(pDriverObject, 0,
&usDriverName,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE, &pDeviceObject);

其中的DbgPrint函数的工作很像“printf”,它会把数据输出到调试器或者是调试输出窗口上。你可以从www.sysinternals.com获取一个名为“DbgView”的工具,所有使用这个函数输出的信息这个工具都会显示出来。

之后你会注意到的是我们调用的函数“RtlInitUnicodeString”,这个函数只是初始化一个UNICODE_STRING数据块。这种数据块包含三个项目,第一个项是
Unicode字符串的长度,第二个项是Unicode的最大长度,第三个项是一个指向Unicode字符串的指针。这种数据块是用来描述一个
Unicode字符串,它在驱动中用的很多。唯一的一个关于UNICODE_STRING需要记住的事情就是,它们不是以NULL来结尾的,而是以数据结
构中的描述“大小”的参数来决定的。对于那些想当然的认为UNICODE_STRING是以NULL来结尾的驱动开发新手来说,这很容易写出“蓝屏驱
动”。大多数传递到你的驱动中的Unicode字符串都不会以NULL来结尾,所以这也是一个你需要意识到的问题。

设备也有名字。它们的名字通常类似于/Device/<somename>
这其实是一个我们通过IoCreateDevice来创建的一个字符串。第二个字符串是“/DosDevices/Example”,我们稍后将会讲到
他,因为我们现在的驱动中还用不到。对于函数IoCreateDevice而言,我们传入的有:一个驱动程序对象,一个指向包含有我们调用驱动所使用字串
的Unicode字符串,如果这个驱动程序和任何设备都无关的话,那么还会有一个被成为“UNKNOWN”的驱动类型。我们也要传入一个指针,以便于接收
新创建的驱动对象。
第二个参数我们传0进去,这个参数是指定用来创建新设备扩展的字节数的。这是一个驱动可以由驱动开发人员来声明的,并且对于设备并非唯一的一个数据结构。
你可以利用这个数据结构来扩展能储存数据的实例所传入信息,比如对驱动程序和创建驱动上下文等。不过在我们的例子中,我们不会使用这些东西。
现在我们已经成功的创建了我们的//Device//Example驱动程序,为了让我们的驱动能够响应用户态的IRP主要请求,我们应当设置驱动对象。当然,在IRP栈中也存在一些子请求,这些子请求就是次要请求。
以下代码组织好了所有确定的请求:
for(uiIndex = 0; uiIndex < IRP_MJ_MAXIMUM_FUNCTION; uiIndex++)
pDriverObject->MajorFunction[uiIndex] = Example_UnSupportedFunction;

pDriverObject->MajorFunction[IRP_MJ_CLOSE] = Example_Close;
pDriverObject->MajorFunction[IRP_MJ_CREATE] = Example_Create;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = Example_IoControl;
pDriverObject->MajorFunction[IRP_MJ_READ] = Example_Read;
  pDriverObject->MajorFunction[IRP_MJ_WRITE] = USE_WRITE_FUNCTION;

我们把创建,关闭,I/O控制,读和写都组织好了。这也例程意味着什么呢?当我们和用户态的应用程序通讯的时候,这些API调用会直接把参数传入驱动!
  • CreateFile -> IRP_MJ_CREATE
  • CloseHandle -> IRP_MJ_CLEANUP & IRP_MJ_CLOSE
  • WriteFile -> IRP_MJ_WRITE
  • ReadFile-> IRP_MJ_READ
  • DeviceIoControl -> IRP_MJ_DEVICE_CONTROL
需要解释的是,IRP_MJ_CLOSE不是在创建句柄的进程的上下文中调用的。如果你想要进程进行相关的清理操作,你需要通过操纵IRP_MJ_CLEANUP来替代。
如你所见,当一个用户态程序调用了这些函数,它会调用你驱动中相应的例程。你也许想知道为什么用户态的“文件”并不意味着真正的“文件”。的确是这样的,
这些API可以和任何将自己展现给用户态的设备通讯,而不是只访问文件。在这篇文章的结尾,我们将会写一个和我们的驱动通讯的用户态程序,而这个程序就是通过CreateFile,WriteFile,
CloseHandle来通讯的。这是多么简单的啊!至于USE_WRITE_FUNCTION,它是一个常数,稍后我会解释的。

下一步很容易,它是驱动的卸载函数。
pDriverObject->DriverUnload = Example_Unload;

在技术上你可以忽略这个函数,但是如果你想要动态的卸载你的驱动程序,那么它一定要被列出。如果你的驱动中没有这个函数,那么系统不会允许你卸载这个驱动。
之后的代码实际上是DEVICE_OBJECT,而不是DRIVER_OBJECT。由于他们都是以“D”开头,同时又都是以“_OBJECT”结尾的,所以可能会有点难以区分,所以我们很容易混淆他们。
pDeviceObject->Flags |= IO_TYPE;
pDeviceObject->Flags &= (~DO_DEVICE_INITIALIZING);
我们仅仅是设置了标志。“IO_TYPE”其实是一个用来定义我们所想要做的I/O类型的常数(我在example.h中定义了它)。我在操作用户态写请求的部分会进行解释。
“DO_DEVICE_INITIALIZING”用来告诉I/O管理器设备正在被初始化以及不要向驱动发送任何的I/O请求。对于在“DriverEntry”的上下文中创建的设备,当
“DriverEntry”完成的时候不需要清除这个标志,然而,如果你在“DriverEntry”之外的任何例程中设定了这个标志,那么你必须为任何你使用IoCreateDevice创建的设备通过手动清除这个标志,
事实上这个标志是由IoCreateDevice设定的,我们在这儿清除这个标志其实只是为了玩笑而已——因为我们并不需要这么做。
我们的驱动的最后一部分是使用我们在之前定义的Unicode字符串。“/Device/Example”和“/DosDevices/Example”。
IoCreateSymbolicLink(&usDosDeviceName, &usDriverName);
IoCreateSymbolicLink只做了这些事情,它在对象管理器中创建了一个“符号链接”。你可以下载我的工具“QuickView”,或者是去www.sysinternals.com下载“WinObj”来查看对象管理器。一个符号链接仅仅是把一个“DOS设备名”映射为“NT设备名”而已。
在这个例子中,“Example”是我们的DOS设备名,“/Device/Example”是我们的NT设备名。
我们接下来要看的代码段是卸载例程。这个例程是够动态的卸载驱动所需要的。这个部分比较短小而且没有什么可解释的。

VOID Example_Unload(PDRIVER_OBJECT DriverObject)
{

UNICODE_STRING usDosDeviceName;

DbgPrint("Example_Unload Called /r/n");

RtlInitUnicodeString(&usDosDeviceName, L"//DosDevices//Example");
IoDeleteSymbolicLink(&usDosDeviceName);

IoDeleteDevice(DriverObject->DeviceObject);
}
你可以在你的卸载进程中做任何你喜欢的事情。这个卸载例程很简单,它只是删除了我们之前创建的符号链接和被我们命名为/Device/Example的设备。

如果你使用过WriteFile和ReadFile,你应当知道调用函数的时候你只是传入一个指向缓冲区来实现写入数据或是从设备中读取数据。这些参数就如我们之前解释的那样,在IRP中被传入驱动。
尽管输入输出管理器在IRP传递到驱动之前有三种方法来排列数据,但是其实还是有更多的故事的。这也意味着数据的排列方式决定了驱动程序的读写函数需要如何分析传入的数据。

这三种方法是“直接输入输出”,“被缓冲的输入输出”,“两者皆非”。
#ifdef __USE_DIRECT__
#define IO_TYPE DO_DIRECT_IO
#define USE_WRITE_FUNCTION Example_WriteDirectIO
#endif

#ifdef __USE_BUFFERED__
#define IO_TYPE DO_BUFFERED_IO
#define USE_WRITE_FUNCTION Example_WriteBufferedIO
#endif

#ifndef IO_TYPE
#define IO_TYPE 0
#define USE_WRITE_FUNCTION Example_WriteNeither
#endif

这段代码意味着,如果你在头文件中定义了“__USE_DIRECT__”,那么IO_TYPE将是DO_DIRECT_IO并且USE_WRITE_FUNCTION指向Example_WriteDirectIO。
如果你在头文件中定义了“__USE_BUFFERED__”,那么IO_TYPE将是DO_BUFFERED_IO并且USE_WRITE_FUNCTION指向Example_WriteBufferedIO。
如果你在头文件中既没有定义__USE_DIRECT__也没有定义__USE_BUFFERED__,那么IO_TYPE将被定义为0(两者皆非)并且写入函数指向Example_WriteNeither。

我们将会讲解每一种类型的输入输出。
Direct I/O:

NTSTATUS Example_WriteDirectIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
PIO_STACK_LOCATION pIoStackIrp = NULL;
PCHAR pWriteDataBuffer;

DbgPrint("Example_WriteDirectIO Called /r/n");

/*
* Each time the IRP is passed down
* the driver stack a new stack location is added
* specifying certain parameters for the IRP to the driver.
*/

pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);

if(pIoStackIrp)
{
pWriteDataBuffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);

if(pWriteDataBuffer)
{
/*
* We need to verify that the string
* is NULL terminated. Bad things can happen
* if we access memory not valid while in the Kernel.
*/

if(Example_IsStringTerminated(pWriteDataBuffer, pIoStackIrp->Parameters.Write.Length))
{
DbgPrint(pWriteDataBuffer);
}
}
}

return NtStatus;
}

我们所做的第一件事是调用“IoGetCurrentIrpStackLocation”,这个函数只是给我们提供我们的IO_STACK_LOCATION。
在我们的例子中,惟一一个我们需要从它之中获取的参数是提供给驱动的缓冲区长度,它位于Parameters.Write.Length。
缓冲输入输出所工作的方法是提供给你一个“MdlAddress”,它是“内存描述表”。这个表是一个对用户态的内存地
址以及如何将其映射到物理地址的描述。我们可以调用
MmGetSystemAddressForMdlSafe这个函数,这个操作可以给我们一个系统虚拟地址,
我们可以使用这个虚拟地址来读内存。
所以,这只是简单的映射被用户态使用的物理页面到系统内存中。我们可以使用它的返回值来访问被用户态所传入的缓冲区。
这种方法通常用于较大的换用区因为他并不需要内存来复制。用户态的缓冲区在IRP请求完成之前是被锁定的,这是使用直接输入输出的不利方面。这是唯一的不好之处,也是为什么它通常对较大缓冲区而言更有用的原因。
Buffered I/O :
NTSTATUS Example_WriteBufferedIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
PIO_STACK_LOCATION pIoStackIrp = NULL;
PCHAR pWriteDataBuffer;

DbgPrint("Example_WriteBufferedIO Called /r/n");

/*
* Each time the IRP is passed down
* the driver stack a new stack location is added
* specifying certain parameters for the IRP to the driver.
*/

pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);

if(pIoStackIrp)
{
pWriteDataBuffer = (PCHAR)Irp->AssociatedIrp.SystemBuffer;

if(pWriteDataBuffer)
{
/*
* We need to verify that the string
* is NULL terminated. Bad things can happen
* if we access memory not valid while in the Kernel.
*/

if(Example_IsStringTerminated(pWriteDataBuffer,
pIoStackIrp->Parameters.Write.Length))
{
DbgPrint(pWriteDataBuffer);
}
}
}

return NtStatus;
}

这种方法的好处是传送到驱动程序中的数据可以在任何上下文中,比如另外一个位于其他进程中的线程的上下文中被访问。另外一个原因是映射内存到未分页的页面中,以便于驱动也能够在被提升的IRQL等级中读取。

使用Buffered I/O的不好之处在于它分配了未分页的内存并且进行了复制。这总是凌驾于驱动驱动每次的读写之上的。
这也是它最好被用于较小的缓冲区的原因之一。整个用户态的页不需要再像直接访问输入输出那样被锁定,这是它的好的方面。另外一个将它用于较大缓冲区的问题
是,由于他分配了未分页内存,它也许会需要在连续的未分页内存中分配较大的一个块。

Neither Buffered nor Direct:
NTSTATUS Example_WriteNeither(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
PIO_STACK_LOCATION pIoStackIrp = NULL;
PCHAR pWriteDataBuffer;

DbgPrint("Example_WriteNeither Called /r/n");

/*
* Each time the IRP is passed down
* the driver stack a new stack location is added
* specifying certain parameters for the IRP to the driver.
*/

pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);

if(pIoStackIrp)
{
/*
* We need this in an exception handler or else we could trap.
*/

__try {

ProbeForRead(Irp->UserBuffer,
pIoStackIrp->Parameters.Write.Length,
TYPE_ALIGNMENT(char));
pWriteDataBuffer = Irp->UserBuffer;

if(pWriteDataBuffer)
{
/*
* We need to verify that the string
* is NULL terminated. Bad things can happen
* if we access memory not valid while in the Kernel.
*/

if(Example_IsStringTerminated(pWriteDataBuffer,
pIoStackIrp->Parameters.Write.Length))
{
DbgPrint(pWriteDataBuffer);
}
}

} __except( EXCEPTION_EXECUTE_HANDLER ) {

NtStatus = GetExceptionCode();
}

}

return NtStatus;
}
在这个方法中,驱动直接访问用户态内存。输入输出管理器不会复制数据,但是也不会锁定用户态内存中的页。它只不过将用户态缓冲区的地址交给了驱动。
好处是没有数据被复制,没有内存被分配,并且也没有页被锁定到内存中。不好之处在于你必须在调用线程的上下文中处理,
否则你将无法正确的访问到用户态内存。另外一个不好之处是进程自身有可能会在另一个线程中尝试去修改页的访问权限,释放内存,等等。
这就是为什么你通常想要使用“ProbeForRead”和“ProbeForWrite”函数并且给所有的代码都写上异常处理。没有什么可以担保在任何时候页都是可用的,
你可以简单的在你尝试读写之前,尝试确认他们是可用的。这个缓冲区位于Irp->UserBuffer。

驱动开发之一 --- 创建一个简单的设备驱动(续) 【译文】
2008-04-20 22:39
接上:
动态加载和卸载驱动,代码如下:
int _cdecl main(void)
{
HANDLE hSCManager;
HANDLE hService;
SERVICE_STATUS ss;

hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);

printf("Load Driver/n");

if(hSCManager)
{
printf("Create Service/n");

hService = CreateService(hSCManager, "Example",
"Example Driver",
SERVICE_START | DELETE | SERVICE_STOP,
SERVICE_KERNEL_DRIVER,
SERVICE_DEMAND_START,
SERVICE_ERROR_IGNORE,
"C://example.sys",
NULL, NULL, NULL, NULL, NULL);

if(!hService)
{
hService = OpenService(hSCManager, "Example",
SERVICE_START | DELETE | SERVICE_STOP);
}

if(hService)
{
printf("Start Service/n");

StartService(hService, 0, NULL);
printf("Press Enter to close service/r/n");
getchar();
ControlService(hService, SERVICE_CONTROL_STOP, &ss);

DeleteService(hService);

CloseServiceHandle(hService);

}

CloseServiceHandle(hSCManager);
}

return 0;
}
与设备驱动通讯,代码如下:
int _cdecl main(void)
{
HANDLE hFile;
DWORD dwReturn;

hFile = CreateFile(".//Example",
GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, 0, NULL);

if(hFile)
{
WriteFile(hFile, "Hello from user mode!",
sizeof("Hello from user mode!"), &dwReturn, NULL);
CloseHandle(hFile);
}

return 0;
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值