iniwf

风是温柔的,雨是伤心的,云是快乐的,月是多情的,爱是迷失的,恋是醉人的,情是难忘的,天是长久的,地是永恒的

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

转自http://hi.baidu.com/combojiang/blog/item/28c384df1b81d716495403c5.html

在这里,我们将描述如何创建一个简单的设备驱动程序,动态的加载和卸载,以及在用户模式与之通讯。

理论:
在开始之前,我们需要了解几个概念:
我们知道一个程序经过编译器和连接器,最终生成一个一定格式二进制文件,这个格式可以被操作系统识别。在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的入口函数不是main或winmain或DriverEntry,而是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进行编译,以下是makefile和sources的内容

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, 0sizeof( RTL_HEAP_DEFINITION ));   
heapParams.Length 
= sizeof( RTL_HEAP_DEFINITION );   
Heap 
= RtlCreateHeap( 200x1000000x10000&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 
270x1B // Timer used for profiling. 
CLOCK1_LEVEL 280x1C // Interval clock 1 level - not used on x86 
CLOCK2_LEVEL 280x1C // Interval clock 2 level 
SYNCH_LEVEL 280x1C // Synchronization level 
IPI_LEVEL 290x1D // Interprocessor interrupt level 
POWER_LEVEL 300x1E // Power failure level 
HIGH_LEVEL 310x1F // 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                                 请求的文件对象



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


       上层驱动调用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);
}



文章较长,分成三部分,见下篇。

posted on 2009-03-23 21:52 iniwf 阅读(772) 评论(0)  编辑 收藏 引用 所属分类: 驱动


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


导航

统计

常用链接

留言簿(2)

随笔分类

随笔档案

收藏夹

IT技术

积分与排名

最新评论

阅读排行榜

评论排行榜