驱动入门
从DriverEntry()说起
做过C语言开发的都知道程序是从main()函数开始执行。在进行Windows驱动程序开发的时候没有main()函数作为函数入口,取而代之的是DriverEntry().
DriverEntry()的原型如下:
extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
前面的extern “C”大概的意思就是调用C编译器对函数进行编译,实现C++和C的混合编程。
DriverEntry()函数中的第一个参数为:PDRIVER_OBJECT DriverObject,代表一个驱动对象,每个驱动程序都有一个惟一的驱动对象。通过这个函数来初始化。
typedef struct _DRIVER_OBJECT
{
CSHORT Type;
CSHORT Size;
PDEVICE_OBJECT DeviceObject;
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName;
PUNICODE_STRING HardwareDataBase;
PFAST_IO_DISPATCH FastIoDispatch;
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo;
PDRIVER_UNLOAD DriverUnload;
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION];
} DRIVER_OBJECT;
根据MSDN的描述DRIVER_OBJECT是一个半透明的结构,上面并没有完全列出所有元素。其中有几个域需要进行进一步的说明。
首先是: PDEVICE_OBJECT DeviceObject;
typedef struct _DEVICE_OBJECT {
…
struct _DRIVER_OBJECT * DriverObject;//指向驱动程序中的驱动对象
struct _DEVICE_OBJECT * NextDevice;//指向下一个设备对象
struct _DEVICE_OBJECT * AttachedDevice;//如果有高层驱动附加到该
//驱动的话,AttachedDevice指向那个更高层的驱动
struct _IRP * CurrentIrp;//指向当前的IRP结构
PVOID DeviceExtension;//指向设备扩展对象,这是开发人员针对具体设备定义的结构体
DEVICE_TYPE DeviceType;
CCHAR StackSize;
union {
LIST_ENTRY ListEntry;
WAIT_CONTEXT_BLOCK Wcb;
} Queue;
ULONG AlignmentRequirement;
KDEVICE_QUEUE DeviceQueue;
KDPC Dpc;
…
} DEVICE_OBJECT, *PDEVICE_OBJECT;
DEVICE_OBJECT结构被操作系统用来表示一个设备对象,这个设备对象可以代表逻辑的、虚拟的或者是物理设备。每一个设备对象都会有一个指针指向下一个设备对象(如果有的话),形成一个设备链。设备链的第一个设备时在DRIVER_OBJECT结构中指明的。
接着是:PDRIVER_UNLOAD DriverUnload;
这个函数用来指明驱动的卸载函数,在该函数中可以将各种资源释放。
最后是: PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION];
指向驱动程序的DispatchXXX函数指针的数组。每个驱动程序至少要设置一个DispatchXXX函数指针在这个数组里来处理这个驱动程序IRP请求包。需要设定一些默认的分发函数(DIspatchXXX)来处理一些默认的IRP包。
Driver_Entry()函数的第二个参数一般不用管。
在Driver_Entry()函数中主要是将各种函数和驱动对象关联起来。
一般可以这样定义:
extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,IN PUNICODE_STRING pRegistryPath)
{
KdPrint(("Enter DriverEntry\n"));
pDriverObject->DriverExtension->AddDevice = xxxAddDevice;
pDriverObject->MajorFunction[IRP_MJ_PNP] = xxxPnp;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] =xxxControl;
pDriverObject->MajorFunction[IRP_MJ_CREATE] =xxxCreate;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] =xxxClose;
pDriverObject->MajorFunction[IRP_MJ_READ] =xxxDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = xxxDispatchRoutine;
pDriverObject->DriverUnload = xxxUnload;
KdPrint(("Leave DriverEntry\n"));
return STATUS_SUCCESS;
}
现在来挨个介绍一下。首先是DRIVER_OBJECT对象的扩展部分DriverExtension的AddDevice域。当即插即用管理器检测到一个由此驱动程序负责的设备时,即调用此例程来增加一个设备。在xxxAddDevvice “函数”中一般通过调用IoCreateDecive来创建一个DEVICE_OBJECT,并将该对象加入到设备栈中。然后是驱动程序DRIVER_OBJECT对象的MajorFunction,I/O管理器中发送的I/O请求最终都会有这一组函数来处理。这里就列出这些函数的一般定义方式。
NTSTATUS xxxPnp(IN PDEVICE_OBJECT fdo, IN PIRP Irp)
对即插即用IRP进行处理,注意参数列表。
NTSTATUS xxxControl(PDEVICE_OBJECT fdo, PIRP pIrp)
IO设备控制操作,实现数据的传输,主要上层程序需要的操作都由该函数来完成。需要根据IRP传来的不同的“操作码”采取不同的操作。
NTSTATUS xxxCreate(IN PDEVICE_OBJECT fdo,IN PIRP Irp)
对create IRP进行处理
NTSTATUS xxxClose(IN PDEVICE_OBJECT fdo,IN PIRP Irp)
对 close IRP 进行处理
NTSTATUS xxxDispatchRoutine(IN PDEVICE_OBJECT fdo,IN PIRP Irp)
对缺省的IRP进行处理
Driver_Entry()函数中最后一个函数:pDriverObject->DriverUnload = xxxUnload;
用于与初始化例程(Routine)相对应,释放初始化设备时申请的资源。
现在来探讨一下比较重要的xxxAddDevice 例程。
NTSTATUS xxxAddDevice(IN PDRIVER_OBJECT DriverObject,IN PDEVICE_OBJECT PhysicalDeviceObject)
该函数用来创建设和添加新设备对象。其中DriverObject是由I/O管理器传来的驱动对象,也就是是Driver_Entry()函数中的那个驱动程序对象。PhysicalDeviceObject 代表设备堆栈底部的物理设备对象(由总线驱动创建,其实就是操作系统创建,一般被称为PDO)。
xxxAddDevice函数的基本职责是创建一个设备对象并把它连接到以PDO为底的设备堆栈中。可以通过以下步骤完成:
- 调用IoCreateDevice创建设备对象,并建立一个私有的设备扩展对象。
- 寄存一个或多个设备接口,以便应用程序能知道设备的存在。另外,还可以给出设备名并创建符号连接。
- 调用IoAttachDeviceToDeviceStack函数把新设备对象放到堆栈上。
- 初始化设备扩展和设备对象的Flag成员。
通过IoCreateDevice()函数我们可以这样创建设备对象:
NTSTATUS status;
PDEVICE_OBJECT fdo;
//创建设备名称,注意使用Unicode宽字符
UNICODE_STRING devName;
RtlInitUnicodeString(&devName,L"\\Device\\xxxDevice");
status = IoCreateDevice(
DriverObject,
sizeof(DEVICE_EXTENSION),
&(UNICODE_STRING)devName,//指定设备名
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&fdo);
各个参数说明:
第一个参数(DriverObject) 就是AddDevice的第一个参数。该参数用于在驱动程序和新设备对象之间建立连接,这样I/O管理器就可以向设备发送指定的IRP。
第二个参数是设备扩展结构的大小。I/O管理器自动分配这个内存,并把设备对象中的DeviceExtension指针指向这块内存。
第三个参数表示设备名称
第四个参数(FILE_DEVICE_UNKNOWN)这个值可以被设备硬件键或类键中的超越值所替代,如果这两个键都含有该参数的超越值,那么硬件键中的超越值具有更高的优先权。对于属于某个已存在类的设备,必须在这些地方指定正确的值,因为驱动程序与外围系统的交互需要依靠这个值。另外,设备对象的默认安全设置也依靠这个设备类型值。
第五个参数这里设置为0.
第六个参数(FALSE) 指出设备是否是排斥的。通常,对于排斥设备,I/O管理器仅允许打开该设备的一个句柄。这个值同样也能被注册表中硬件键和类键中的值超越,如果两个超越值都存在,硬件键中的超越值具有更高的优先权。
第七个参数(&fdo) 是存放设备对象指针的地址,IoCreateDevice函数使用该变量保存刚创建设备对象的地址。
当IoCreateDevice返回后,我们可以建立一个私有设备扩展对象,私有设备对象DEVICE_EXTENSION是驱动程序开发者自己定义的。
可以这样来完成:
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;
pdx->fdo = fdo;
可以定义为这样:
typedef struct _DEVICE_EXTENSION
{
PDEVICE_OBJECT fdo; //功能层设备对象
PDEVICE_OBJECT NextStackDevice; //底层设备对象
UNICODE_STRING interfaceName; //设备接口
UNICODE_STRING devName; //设备名
PKINTERRUPT InterruptObject; // address of interrupt object
BOOLEAN mappedport; //如果为真需要做IO端口映射
PVOID MemBar0; //内存基地址0
ULONG nMem0; //基地址BAR0占用字节数
ULONG DmaChannel; //DMA通道
PDMA_ADAPTER DmaAdapter; //DMA Adapter 对象
PALLOCATE_COMMON_BUFFER allocateCommonBuffer; //分配连续的物理内存
//DMA函数
PFREE_COMMON_BUFFER freeCommonBuffer; //释放连续的物理内存DMA函数
PPUT_DMA_ADAPTER putDmaAdapter; //释放DMA Adapter对象
PHYSICAL_ADDRESS RegsPhyBase; //寄存器物理地址首地址
PVOID RegsBase; //寄存器虚拟地址首地址
PHBA_REGS pHBARegs;
ULONG DmaPort; //设备DMA物理端口
…
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
注:该设备扩展的定义针对了PCI驱动
设备扩展主要用来维护设备状态信息、存储驱动程序使用的内核对象或系统资源(如自旋锁)、保存驱动程序需要的数据等。由于大多数的总线驱动、功能驱动和过滤器驱动都要工作在任意线程上下文,即任意线程都可能成为当前线程,所以,设备扩展是保存设备状态信息和数据的主要空间。