Driver.h头文件中包含了开发NT式驱动所需要的NTDDK.h,此外还定义了几个标志来指明函数和变量分配在分页内存还是非分页内存中。Windows驱动程序的入口函数是DriverEntry函数。WDM式的驱动程序要导入的头文件是WDM.h。
说明:
1)采用C++编程,所以需要用extern “C”,因为我们导入的是C的函数的符号表。
2)在驱动中用到的变量或函数都需要指定分配在分页或非分页内存中,分页内存在物理内存不够的情况下可能会被交换出去,对于一些需要高IRQL的例程绝对不能被交换出页面,因此它们必须被定义为非分页内存。
3)DriverEntry需要放在INIT标志的内存中。
#pragma INITCODE
extern "C" NTSTATUS DriverEntry (
IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath )
{
NTSTATUS status;
KdPrint(("Enter DriverEntry\n"));
//注册其他驱动调用函数入口
pDriverObject->DriverUnload = HelloDDKUnload;
pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_READ] = HelloDDKDispatchRoutine;
//创建驱动设备对象
status = CreateDevice(pDriverObject);
KdPrint(("DriverEntry end\n"));
return status;
}
1) 犹如控制台程序需要main、Win32 程序需要WinMain、DLL 程序需要DllMain 一样,驱动程序也有自己的入口点,即DriverEntry。DriverEntry 需要被加载到INIT 内存区域中,这样当驱动被卸载后它可以退出内存。
2) DriverEntry 是由内核中的I/O 管理器负责调用的,它有两个参数DriverObject 和RegistryPath(当然形参的名字可以自己改变)。其中DriverObject 是由I/O管理器传递进来的驱动对象,RegistryPath 则指向此驱动负责的注册表。
3)我们可以看到DriverEntry 首先是定义了一些变量,然后调用IoCreateDevice 创建设备对象,紧接着调用IoCreateSymbolicLink 创建符号链接。HelloDDKDispatchRoutine等是驱动程序在向Windows 的I/O 管理器注册一些回调函数。上面代码的含义是:当驱动程序将被卸载时自动调用HelloDDKUnload例程;当驱动程序接收到 IRP_MJ_CREATE 时自动调用HelloDDKDispatchRoutine。
4)KdPrint是宏,用来输出。类似于MFC中的TRACE。
5)#pragma INITCODE来指明此函数加载到INIT内存函数中。
6)在驱动对象DriverObject 中,有个函数指针数组MajorFunction,它里面的每一个元素都记录着一个函数的地址对应着相应的IRP,我们可以通过简单地设置这个数组将IRP 与相应的派遣函数关联起来
#pragma INITCODE
NTSTATUS CreateDevice (IN PDRIVER_OBJECT pDriverObject)
{
NTSTATUS status;
PDEVICE_OBJECT pDevObj;
PDEVICE_EXTENSION pDevExt;
//创建设备名称
UNICODE_STRING devName;
RtlInitUnicodeString(&devName,L"\\Device\\MyDDKDevice");
//创建设备
status = IoCreateDevice( pDriverObject,
sizeof(DEVICE_EXTENSION),
&(UNICODE_STRING)devName,
FILE_DEVICE_UNKNOWN,
0, TRUE,
&pDevObj );
if (!NT_SUCCESS(status))
return status;
pDevObj->Flags |= DO_BUFFERED_IO;
pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
pDevExt->pDevice = pDevObj;
pDevExt->ustrDeviceName = devName;
//创建符号链接
UNICODE_STRING symLinkName;
RtlInitUnicodeString(&symLinkName,L"\\??\\HelloDDK");
pDevExt->ustrSymLinkName = symLinkName;
status = IoCreateSymbolicLink( &symLinkName,&devName );
if (!NT_SUCCESS(status))
{
IoDeleteDevice( pDevObj );
return status;
}
return STATUS_SUCCESS;
}
RtlInitUnicodeString
IoCreateDevice
NTSTATUS IoCreateDevice
(
IN PDRIVER_OBJECT DriverObject,
IN ULONG DeviceExtensionSize,
IN PUNICODE_STRING DeviceNameOPTIONAL,
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,
IN BOOLEAN Exclusive,
OUT PDEVICE_OBJECT *DeviceObject
);
DeviceObject:一个指向DEVICE_OBJECT结构体指针的指针,这是一个指针的指针,指向的指针用来接收DEVICE_OBJECT结构体的指针
1) 前面我们创建的设备对象虽然有个参数指定了设备名称,但是这个设备名称只能在内核态可见,也就说ring3 的应用层程序是看不见它的,因此驱动程序需要向ring3 公布一个符号链接,这个链接指向真正的设备名称,而ring3 的应用程序可以通过该符号链接找到驱动程序进行通信。实际上我们经常所说的C 盘、D 盘就是一个符号链接,它们在内核中的真正设备对象是“\Device\HarddiskVolume1”和“\Device \HarddiskVolume2”。在内核模式下,符号链接是以“\??\”( 或“\DosDevices\”)开头的,如C 盘就是“\??\C:”,而在用户模式下,则是以“\.\”开头的,如C 盘就是“\.\C:”。
#pragma PAGEDCODE
VOID HelloDDKUnload (IN PDRIVER_OBJECT pDriverObject)
{
PDEVICE_OBJECT pNextObj;
KdPrint(("Enter DriverUnload\n"));
pNextObj = pDriverObject->DeviceObject;//由驱动对象得到设备对象
while (pNextObj != NULL)
{
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
pNextObj->DeviceExtension;
//删除符号链接
UNICODE_STRING pLinkName = pDevExt->ustrSymLinkName;
IoDeleteSymbolicLink(&pLinkName);
//删除设备对象
pNextObj = pNextObj->NextDevice;
IoDeleteDevice( pDevExt->pDevice );
}
}
卸载驱动例程是我们在DriverEntry 中自己定义的,当驱动被卸载时I/O管理器负责调用该例程,它主要做一些扫尾处理的工作。
KdPrint:由于驱动程序工作于内核态,不像控制台的程序一样可以使用printf 输出一些信息,也不像Win32 程序可以通过MessageBox 来弹出一个对话框,它要想输出一些信息,就需要调用DbgPrint 函数,不过这个函数输出的信息我们无法直接看到,需要使用一些专门的工具,比如DbgView (KmdManager)等。
有些内容我们只想在调试版输出,在发行版忽略,因此DDK 中定义了一个宏KdPrint,它在发行版不被编译,只在调试版才会运行。KdPrint是这样定义的:
#define KdPrint(_x_) DbgPrint _x_ //在使用时最外层要有两个连续的括号。
#pragma PAGEDCODE
NTSTATUS HelloDDKDispatchRoutine(IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp)
{
KdPrint(("Enter HelloDDKDispatchRoutine\n"));
NTSTATUS status = STATUS_SUCCESS;
// 完成IRP
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = 0; // bytes xfered
IoCompleteRequest( pIrp, IO_NO_INCREMENT );
KdPrint(("Leave HelloDDKDispatchRoutine\n"));
return status;
}
派遣例程是处理IRP的。
用DriverStudio中的工具:DriverMonitor。
#pragma INITCODE
extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath)
{
KdPrint(("Enter DriverEntry\n"));
pDriverObject->DriverExtension->AddDevice = HelloWDMAddDevice; //相比NT,此回调函数的作用是创建设备对象并由PNP管理器调用。
...
}
#pragma PAGEDCODE
NTSTATUS HelloWDMAddDevice(IN PDRIVER_OBJECT DriverObject,
IN PDEVICE_OBJECT PhysicalDeviceObject)
{
PAGED_CODE();
KdPrint(("Enter HelloWDMAddDevice\n"));
...
}
同NT
用EzDriverInstall安装或控制面版中添加硬件。
实际上,常见的Windows 驱动程序是可以分成两类的:一类是不支持即插即用功能的NT 式驱动程序,另一类是支持即插即用的WDM 式驱动程序。NT 式驱动的安装是基于服务的,可以通过修改注册表进行,也可以直接通过服务函数如CreateService 进行安装;但WDM 式驱动不同,它安装的时候需要通过编写一个inf 文件进行控制。除此之外,它们所使用的头文件也不大相同,例如NT 式驱动往往需要导入一个名为“ntddk.h”的头文件,而WDM 式驱动需要的却是“wdm.h”头文件。我们在学习的过程中所编写的大多属于NT 式驱动,除非我们需要自己的设备支持即插即用,才需要考虑编写WDM 式驱动程序。