在这里,我们将描述如何创建一个简单的设备驱动程序,动态的加载和卸载,以及在用户模式与之通讯。 理论: 在开始之前,我们需要了解几个概念: 我们知道一个程序经过编译器和连接器,最终生成一个一定格式二进制文件,这个格式可以被操作系统识别。在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,而是 void Native application自己不会返回,所以需要在 NtTerminateProcess(NtCurrentProcess(), 0);
写native application其实也是很简单的:
status = ChangeHostName(); 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;
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如下:
PASSIVE_LEVEL .
APC_LEVEL
DISPATCH_LEVEL
了你可以在什么样的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提供了两组函数。
及
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给下层驱动之前设置堆栈位置的内容。
|
接上继续。
/********************************************************************** * * 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);
需要解释的是,IRP_MJ_CLOSE不是在创建句柄的进程的上下文中调用的。如果你想要进程进行相关的清理操作,你需要通过操纵IRP_MJ_CLEANUP来替代。 |
接上:
动态加载和卸载驱动,代码如下: int _cdecl main(void)与设备驱动通讯,代码如下: int _cdecl main(void) |