转自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, 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 请求的文件对象
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);
}
文章较长,分成三部分,见下篇。