在配置好想对应的开发环境后,我们就可以开发驱动程序了。注:下面的主要以NT式驱动为例,部分涉及到WDM驱动的差别会有特别说明。
在Console控制台下,我们的有一个入口函数main;在Windows图形界面平台下,有另外一个入口函数Winmain。我们只要在这入口函数里面调用其他相关的函数,程序就会按照我们的意愿跑起来了。在我们用IDE开发的时候,也许你不会发现这些细微之处是如何配置出来的,一般来说我们也不用理会,因为在新建工程的时候,IDE已经帮我们把编译器(Compiler)以及连接器(Linker)的相关参数设置好,在正式编程的时候,我们只要按照规定的框架编程就行了。
同样,在驱动程序也有一个入口函数DriverEntry,这并不是一定的,但这是微软默认的、推荐使用的。在我们配置开发环境的时候我们有机会指定入口函数,这是链接器的参数/entry:"DriverEntry"。
入口函数的声明
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject,PUNICODE_STRING pRegistryPath)
DriverEntry主要是对驱动程序进行初始化工作,它由系统进程(System)创建,系统启动的时候System系统进程就被创建了。
驱动加载的时候,系统进程将会创建新的线程,然后调用执行体组件中的对象管理器,创建一个驱动对象(DRIVER_OBJECT)。另外,系统进程还得调用执行体组件中的配置管理程序,查询此驱动程序在注册表中对应项。系统进程在调用驱动程序的DriverEntry的时候就会将这两个值传到pDriverObject和pRegistryPath。
接下来,我们介绍下上面出现的几个数据结构:
typedef LONG NTSTATUS
在驱动开发中,我们应习惯于用NTSTATUS返回信息,NTSTATUS各个位有不同的含义,我们可以也应该用宏NT_SUCCESS来判断是否返回成功。
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
NTSTAUS的编码意义:
其中
Ser是Serviity的缩写,代表严重程度。
00:成功 01:信息 10:警告 11:错误
C是Customer的缩写,代表自定义的位。
Facility:设备位
Code:设备的状态代码。
根据这定义编码,还有补码的概念,那么只要是错误的时候,最高位就是1,NTSTATUS的值就是负数,所以可以大于零来判断,但无论如何都希望读者用NT_SUCCESS宏来判断是否成功,因为这可能在以后会有所改动,即使这么多年来都一直沿用着。
同样的,微软也为我们定义了其他几个判断宏:
#define NT_INFORMATION(Status) ((((ULONG)(Status)) >> 30) == 1)
#define NT_WARNING(Status) ((((ULONG)(Status)) >> 30) == 2)
#define NT_ERROR(Status) ((((ULONG)(Status)) >> 30) == 3)
有了之前的介绍,这三个相信不说大家也能领会了。但最常用的还是NT_SUCCESS。
我们继续说其他的两个数据结构,先说PUNICODE_STRING吧,P代表这是一个指针类型,指向一个UNICODE_STRING结构。
宽字符串结构体(UNICODE_STRING)
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
其中,
Ø Length:Unicode字符串当前的字符长度。注意不是字节数,每个Unicode字符占用两个字节。
Ø MaximumLength:该Unicode字符串的最大容纳长度。
Ø Buffer:Unicode字符串的缓冲地址。
UNICODE_STRING是Windows驱动开发里面经常用到的一个结构,用Length来标记字符串的长度而不再用\0来表示结束。可以用RtlInitUnicodeString来对其初始化,但这里的pRegistryPath是直接由创建驱动程序的线程传进来的参数,如果在接下来仍需要用到该值,最好是使用RtlCopyUnicodeString函数将其值另外保存下来,因为这个字符串并不是长期存在的,DriverEntry函数返回的时候可能就会被销毁了。
PDRIVER_OBJECT,P代表这是一个指针类型,指向一个驱动对象(DRIVER_OBJECT),每个驱动程序都有一个驱动对象。这是一个半透明的数据结构,微软没有公开它的完全定义,只是有提到几个成员,但我们依旧可以通过WinDbg看到它的定义,只是不同的系统可能会存在不同的结构。不过我另外在WDK的头文件WDM.h里面发现了它的定义:
驱动对象(DRIVER_OBJECT)
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 + 1];
} DRIVER_OBJECT;
这里提下几个比较重要的字段,
Ø DeviceObject:指向由此驱动创建的设备对象。每个驱动程序都会有一个或多个的设备对象。其中,每个设备对象都会有一个指针指向下一个设备对象,这在我们介绍设备对象的时候再继续说。
Ø DriverName:驱动的名字,该字符串一般为\Driver\[驱动程序名称]。
Ø HardwareDatabase:记录设备的硬件数据库键名。该字符串一般为"\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\[服务名]"。
Ø FastIoDispatch:指向快速I/O函数入口,是文件驱动中用到的排遣函数。
Ø DriverStartIo:记录StartIo例程的函数地址,用于串行化操作。
Ø DriverUnload:指定驱动卸载时所用的回调函数地址。
Ø MajorFunction:这是一个函数指针数组,每个指针指向的是一个函数,该函数就是处理相应IRP的排遣函数,数组的索引值与IRP_MJ_XXX相对应。
我们已经了解了DriverEntry函数头的那个数据结构了,但这还不够,在DriverEntry里,我们主要是对驱动程序进行初始化,这就涉及到其他的一些数据结构了,下面我们继续逐一地介绍。
设备对象(DEVICE_OBJECT)
typedef struct _DEVICE_OBJECT {
CSHORT Type;
USHORT Size;
LONG ReferenceCount;
struct _DRIVER_OBJECT *DriverObject;
struct _DEVICE_OBJECT *NextDevice;
struct _DEVICE_OBJECT *AttachedDevice;
struct _IRP *CurrentIrp;
PIO_TIMER Timer;
ULONG Flags;
ULONG Characteristics;
__volatile PVPB Vpb;
PVOID DeviceExtension;
DEVICE_TYPE DeviceType;
CCHAR StackSize;
union {
LIST_ENTRY ListEntry;
WAIT_CONTEXT_BLOCK Wcb;
} Queue;
ULONG AlignmentRequirement;
KDEVICE_QUEUE DeviceQueue;
KDPC Dpc;
ULONG ActiveThreadCount;
PSECURITY_DESCRIPTOR SecurityDescriptor;
KEVENT DeviceLock;
USHORT SectorSize;
USHORT Spare1;
struct _DEVOBJ_EXTENSION * DeviceObjectExtension;
PVOID Reserved;
} DEVICE_OBJECT, *PDEVICE_OBJECT;
这里只对几个比较重要的字段进行说明:
Ø DriverObject:指向创建此设备对象的驱动程序对象。同属于一个驱动程序的设备对象指向的是同一个驱动对象。
Ø NextObject:指向同一个驱动程序创建的下一个设备对象。同一个驱动对象可以创建若干个设备对象,每个设备对象根据NextDevice来连成一个链表,最后一个设备对象的NextDevice域为NULL。
Ø AttachedDevice:指向附加到此设备对象之上的最近设备对象。这里需要理解分层驱动程序的概念。
Ø DeviceExtension:指向设备的扩展对象。每个设备都会指定一个设备扩展对象,这个数据结构由驱动程序开发者自行定义,可以用来记录一些与设备相关的一些信息,同时应尽量避免使用全局变量,将数据存放在设备扩展里,具有很大的灵活性。
Ø CurrentIrp:在使用StartIO例程的时候,该成员指向的是当前IRP结构。
Ø Flags:指定了该设备对象的标记。下面列出了常用的几个标记:
flag值 |
含义 |
DO_BUFFERED_IO |
读写操作使用缓冲方式(系统复制缓冲区)访问用户模式数据 |
DO_EXCLUSIVE |
一次只允许一个线程打开设备句柄 |
DO_DIRECT_IO |
读写操作使用直接方式(内存描述符表)访问用户模式数据 |
DO_DEVICE_INITIALIZING |
设备对象正在初始化 |
DO_POWER_PAGABLE |
必须在PASSIVE_LEVEL级上处理IRP_MJ_PNP请求 |
Ø DeviceType:指定设备的类型。一般在开发虚拟设备时,选择FILE_DEVICE_UNKNOW。其他的请自行参考WDK文档。
Ø StackSize:在多层驱动情况下,驱动与驱动之间会形成类似堆栈的结构,称之为设备栈。IRP会依次从最高层传递到最底层。StackSize描述的就是该层数。最底层的设备层数为1。
Ø AlignmentRequirement:在进行大容量传输的时候,往往需要进行内存对齐,以保证传输速度。请使用类似FILE_XXX_ALIGNMENT的方式进行赋值。
下面给大家展示一下DriverEntry的最基本框架:
#ifdef __cplusplus
extern "C"
{
#endif
#include <NTDDK.h>
#ifdef __cplusplus
};
#endif
#define PAGECODE code_seg("PAGE")
#define LOCKEDCODE code_seg()
#define INITCODE code_seg("INIT")
#define PAGEDATA data_seg("PAGE")
#define LOCKEDDATA data_seg()
#define INITDATA data_seg("INIT")
typedef struct _DEVICE_EXTENSION
{
PDEVICE_OBJECT pDevice;
UNICODE_STRING ustrDeviceName;
UNICODE_STRING ustrSymLinkName;
} DEVICE_EXTENSION , *PDEVICE_EXTENSION;
//函数声明
NTSTATUS DispatchRoutine(__in struct _DEVICE_OBJECT *DeviceObject, __in struct _IRP *Irp);
VOID UnloadRoutine(__in struct _DRIVER_OBJECT *DriverObject);
///////////////////////
#pragma INITCODE
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING pRegistryPath)
{
NTSTATUS status;
PDEVICE_EXTENSION pDevExt;
PDEVICE_OBJECT pDevObj;
KdPrint(("\n--------------------------------------!\n"));
KdPrint(("Enter DriverEntry!\n"));
//注册相关例程
pDriverObject->DriverUnload = UnloadRoutine;
pDriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = DispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchRoutine;
//初始化相关字符串
UNICODE_STRING ustrDeviceName; //设备名
UNICODE_STRING ustrSymLinkName; //符号链接名
RtlInitUnicodeString(&ustrDeviceName,L"\\Device\\MyDDKDevice1");
RtlInitUnicodeString(&ustrSymLinkName,L"\\??\\MyDDKDriver1");
//创建设备对象
status = IoCreateDevice(pDriverObject,sizeof(DEVICE_EXTENSION),&ustrDeviceName,FILE_DEVICE_UNKNOWN,0,TRUE,&pDevObj);
if (!NT_SUCCESS(status))
{
KdPrint(("Create Device Failure!\n"));
return status;
}
pDevObj->Flags |= DO_BUFFERED_IO;
pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
pDevExt->ustrDeviceName = ustrDeviceName;
pDevExt->pDevice = pDevObj;
//创建符号链接
pDevExt->ustrSymLinkName = ustrSymLinkName;
status = IoCreateSymbolicLink(&ustrSymLinkName,&ustrDeviceName);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(pDevObj);
return status;
}
KdPrint(("Leave DriverEntry! stauts=%d",status));
return status;
}
这是用C++写的,所以必要的地方加上了extern“C”,否则会引起一些错误,这是因为C++与C在进行名称粉碎的时候处理得不一样,C++这个改进主要是为了实现一些高级功能,比如多态。虽然加上extern “C”会有点麻烦,但可以用上C++那些功能,个人觉得也有所值。如果用C,直接忽略上面的extern “C”。
NTDDK.h是NT式驱动需要加载的头文件,如果是WDM式驱动,那么加载的是WDM.h
#define INITCODE code_seg("INIT")定义一个宏,#prama INITCODE还原后就是#pramacode_seg(“INIT”),表示接下来的代码加载到INIT内存区域中,成功加载后,可以退出内存。对于DriverEntry这种一次性的函数而言,这是最适合的选择,可以节省内存。函数结束后需要显式地切换回来,如:#prama LOCKEDCODE。
同样,PAGECODE表示分页内存,作用是将此部分代码放入分页内存中运行,在里面的代码切换进程上下文时可能会被换回分页文件。LOCKEDCODE表示默认内存,也就是非分页内存,里面的代码常驻内存。IRQL处于DISPATCH_LEVEL或者以上的等级,必须处于非分页内存里面。
同理,对于数据段也有同样的机制,于是有了PAGEDATA、LOCKEDDATA、INITDATA。
KdPrint是一个宏,在调试版本里面(具备DBG宏定义),有
#define KdPrint(_x_) DbgPrint _x_
而在正式版本里面,KdPrint被定义为空。所以可以用来作为调试输出。但注意Kdprint后面是两层括号,用法与C语言运行库的printf差不多。
pDriverObject->DriverUnload = UnloadRoutine;将卸载例程函数告诉驱动对象,驱动对象在前面已经有定义,这里不做深入讨论。
pDriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchRoutine;注册排遣例程。Windows是消息驱动,而驱动程序是IRP驱动的,I/O管理器将发送到驱动的“消息”封装在IRP里面,驱动程序也将结果告诉IRP。类似windows的消息机制,对于不同的“消息”,驱动程序需要注册不同的处理例程来区别对待,当然也可以放在同一个例程里面,然后用switch语句来区别对待,但当处理过程比较长的时候,会比较凌乱。
IRP_MJ_CREATE是当RING3应用程序在使用CreateFile函数建立与驱动程序的通信通道时所激活的。IRP_MJ_READ是ReadFile,IRP_MJ_WRITE是WriteFile,而IRP_MJ_CLOSE是CloseHandle关闭文件句柄的时候产生的。
小知识:对于WDM式驱动,仍需要注册AddDevice例程,pDriverObject->DriverExtension->AddDevice = WDMAddDeviceRoutine,设备对象的初始化将在AddDevice里面进行而不是DriverEntry。另外还需要注册IRP_MJ_PNP排遣函数。
前面有讲到UNICODE_STRING结构,那么这里就可以很好的了解初始化的这两个结构了,忘记的看回前面的,这里只列出RtlInitUnicodeString函数定义。
VOID RtlInitUnicodeString(PUNICODE_STRING DestinationString,PCWSTR SourceString);
IoCreateDevice是注册设备对象,一个驱动必须对应这一个或多个设备对象。
NTSTATUS
IoCreateDevice(
IN PDRIVER_OBJECT DriverObject,
IN ULONG DeviceExtensionSize,
IN PUNICODE_STRING DeviceName OPTIONAL,
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,
IN BOOLEAN Exclusive,
OUT PDEVICE_OBJECT *DeviceObject
);
Ø DriverObject:驱动对象的指针,这里用的是入口函数传进来的驱动对象。每个驱动有若干个设备对象,每个设备对象只有一个驱动函数。
Ø DeviceExtensionSize:自定义的设备扩展的大小。
Ø DeviceName:设备对象名称,前面有用RtlInitUnicodeString对其进行过初始化。设备对象是暴露在内核层面上的名称,对于RING3层桌面程序是不可见的。格式需为:\Device\[设备名]。如果不指定设备名,I/O管理器将会自动分配一个数字作为设备名,如:\Device\00000001
Ø DeviceType:设备类型,这里用FILE_DEVICE_UNKNOWN。
Ø DeviceCharacteristics:设备对象的特征。
Ø Exclusive:设置设备对象是否为专用的。也即是否允许有第二个驱动、程序访问这个设备对象。
Ø DeviceObject:I/O管理器将会负责创建这个设备对象,可以用这个参数来接收该对象的地址。
创建了设备对象,在程序临近结束之际需要用IoDeleteDevice删除设备对象。
pDevObj->Flags |= DO_BUFFERED_IO;是设置访问标志为缓冲模式。
pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;获取设备扩展。
pDevExt->ustrDeviceName = ustrDeviceName;将相关信息保存在设备扩展里面。
设备对象名称只暴露在内核层面,想要在RING3用户层访问驱动程序则需要创建符号链接。符号链接是暴露在用户层面的。注册符号链接用的函数是IoCreateSymbolicLink。
NTSTATUS
IoCreateSymbolicLink(
IN PUNICODE_STRING SymbolicLinkName,
IN PUNICODE_STRING DeviceName
);
Ø SymbolicLinkName:符号链接的字符串,前面有对其初始化。符号链接名需要以\??\开头,或者\DosDevice\(未证实)。而在用户模式下需要以\\.\开头才能找到对应的符号链接。
Ø DeviceName:设备名的字符串。
注意:创建了符号链接,在程序临近结束之际需要用IoDeleteSymbolicLink删除符号链接。并且先删除符号链接在删除设备对象。
在上面的DriverEntry函数里面,已经完成了基本的初始化工作,接下来,我们看一下卸载回调例程。
回调例程的声明为:
VOID UnloadRoutine(__in struct _DRIVER_OBJECT *DriverObject);
除了函数名字外,请不要改动其他参数。
在回调函数里面,我们主要进行一些清理工作。
#pragma PAGECODE
VOID UnloadRoutine(__in struct _DRIVER_OBJECT *pDriverObject)
{
PDEVICE_OBJECT pNextObj;
PDEVICE_EXTENSION pDevExt;
pNextObj = pDriverObject->DeviceObject;
KdPrint(("Enter unload routine!\n"));
while(pNextObj != NULL)
{
pDevExt = (PDEVICE_EXTENSION)pNextObj->DeviceExtension;
//删除符号链接
IoDeleteSymbolicLink(&pDevExt->ustrSymLinkName);
pNextObj = pNextObj->NextDevice;
//删除设备
IoDeleteDevice(pDevExt->pDevice);
}
KdPrint(("Leave unload routine!\n"));
KdPrint(("--------------------------------------!\n"));
}
基本在上面都已经有所描述了。这里还强调一点,一个驱动程序可以有一个或者多个设备对象,在驱动程序完全卸载之前需要删除对应的符号链接、设备对象。所以这里用到了while循环来完成这项工作,不明白的回去上面继续熟悉下设备对象的结构。
这个驱动程序没有做什么工作,所以在排遣函数里面我们只是单纯的设置状态为成功,操作的字节为0,设置IRP状态为完成,就返回了。
#pragma PAGECODE
NTSTATUS DispatchRoutine(__in struct _DEVICE_OBJECT *pDeviceObject,
__in struct _IRP *pIrp)
{
KdPrint(("Enter dispatch routine!\n"));
NTSTATUS status = STATUS_SUCCESS;
//完成IRP
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp,IO_NO_INCREMENT);
KdPrint(("Leave dispatch routine!\n"));
return status;
}
pIrp->IoStatus.Status = status;设置IO状态。
pIrp->IoStatus.Information = 0;设置实际操作的字节数。用户层函数ReadFile、WriteFile的第四个参数lpNumberOfBytesRead用于接收实际操作的字节数,这个结果就是这样产生的。
IoCompleteRequest设置完成IRP的处理,否则会继续往下层传递。
整一个框架都已基本介绍完毕了,下面贴上完整的代码吧。
#ifdef __cplusplus
extern "C"
{
#endif
#include <NTDDK.h>
#ifdef __cplusplus
};
#endif
#define PAGECODE code_seg("PAGE")
#define LOCKEDCODE code_seg()
#define INITCODE code_seg("INIT")
#define PAGEDATA data_seg("PAGE")
#define LOCKEDDATA data_seg()
#define INITDATA data_seg("INIT")
#define arrarysize(arr) (sizeof(arr)/sizeof(arr)[0])
typedef struct _DEVICE_EXTENSION
{
PDEVICE_OBJECT pDevice;
UNICODE_STRING ustrDeviceName;
UNICODE_STRING ustrSymLinkName;
} DEVICE_EXTENSION , *PDEVICE_EXTENSION;
//函数声明
NTSTATUS DispatchRoutine(__in struct _DEVICE_OBJECT *DeviceObject, __in struct _IRP *Irp);
VOID UnloadRoutine(__in struct _DRIVER_OBJECT *DriverObject);
///////////////////////
#pragma INITCODE
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING pRegistryPath)
{
NTSTATUS status;
PDEVICE_EXTENSION pDevExt;
PDEVICE_OBJECT pDevObj;
KdPrint(("\n--------------------------------------!\n"));
KdPrint(("pRegistryPath value:%ws",pRegistryPath));
KdPrint(("Enter DriverEntry!\n"));
//注册相关例程
pDriverObject->DriverUnload = UnloadRoutine;
pDriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = DispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchRoutine;
//初始化相关字符串
UNICODE_STRING ustrDeviceName; //设备名
UNICODE_STRING ustrSymLinkName; //符号链接名
RtlInitUnicodeString(&ustrDeviceName,L"\\Device\\MyDDKDevice1");
RtlInitUnicodeString(&ustrSymLinkName,L"\\??\\MyDDKDriver1");
//创建设备对象
status = IoCreateDevice(pDriverObject,sizeof(DEVICE_EXTENSION),&ustrDeviceName,FILE_DEVICE_UNKNOWN,0,TRUE,&pDevObj);
if (!NT_SUCCESS(status))
{
KdPrint(("Create Device Failure!\n"));
return status;
}
pDevObj->Flags |= DO_BUFFERED_IO;
pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
pDevExt->ustrDeviceName = ustrDeviceName;
pDevExt->pDevice = pDevObj;
//创建符号链接
pDevExt->ustrSymLinkName = ustrSymLinkName;
status = IoCreateSymbolicLink(&ustrSymLinkName,&ustrDeviceName);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(pDevObj);
return status;
}
KdPrint(("Leave DriverEntry! stauts=%d",status));
return status;
}
#pragma PAGECODE
NTSTATUS DispatchRoutine(__in struct _DEVICE_OBJECT *pDeviceObject,
__in struct _IRP *pIrp)
{
KdPrint(("Enter dispatch routine!\n"));
NTSTATUS status = STATUS_SUCCESS;
//完成IRP
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp,IO_NO_INCREMENT);
KdPrint(("Leave dispatch routine!\n"));
return status;
}
#pragma PAGECODE
VOID UnloadRoutine(__in struct _DRIVER_OBJECT *pDriverObject)
{
PDEVICE_OBJECT pNextObj;
PDEVICE_EXTENSION pDevExt;
pNextObj = pDriverObject->DeviceObject;
KdPrint(("Enter unload routine!\n"));
while(pNextObj != NULL)
{
pDevExt = (PDEVICE_EXTENSION)pNextObj->DeviceExtension;
//删除符号链接
IoDeleteSymbolicLink(&pDevExt->ustrSymLinkName);
pNextObj = pNextObj->NextDevice;
//删除设备
IoDeleteDevice(pDevExt->pDevice);
}
KdPrint(("Leave unload routine!\n"));
KdPrint(("--------------------------------------!\n"));
}
来源:oschina
链接:https://my.oschina.net/u/163910/blog/198615