WDM,即Windows Driver Model,是Windows环境下开发驱动程序的有力工具。
给出一个WDM一书中描述windows 2000系统结构的一个截图:
驱动程序是一个分层的结构,一个硬件设备并不是只由一个驱动程序来管理,在它相关联的物理设备驱动程序之上,还有很多过滤驱动程序。与这些过滤驱动程序相关联的,就是这个物理设备对象的过滤器设备对象。那么,一个用户模式的请求,必须通过上层的过滤器设备对象,一层一层的往下传,最终才能到达物理设备对象。这有点像TCP/IP分层结构模型,一个应用层的数据包必须通过传输层、网络层这样一层一层的往下传,最终才能达到物理层并传递到网络中。而设计这样的分层模型的目的,我想应该是为了方便扩展,比如如果想对某个设备加入新的管理操作,那么不需要修改其已有的物理设备驱动程序和过滤器驱动程序,而只需要加入新的过滤器设备对象以及相应的驱动程序,在这里加入新的操作就行了。
下面,还是用一个图来表示这种分层的结构模型:
这个图左边的PDO、FDO、FiDO等,就是指设备对象。而IRP——IO请求包,就是上一层设备对象向下一层设备对象发送的请求,也就是它们之间交互的信息。
另外,需要指出的一点是,在很多内核模式编程中,驱动程序并不一定要与某一个实际存在的物理设备相关联,它可以仅创建一个虚拟的设备对象,而这个设备对象不与任何实际的物理设备相关联。因为在很多情况下,用户编写驱动的目的仅仅是要让自己的代码执行在系统的内核态中。
驱动程序入口点 - DriverEntry函数(相当于c语言的main函数), 它在驱动程序被加载进内存的时候调用。
DriverEntry函数有两个参数,其中第一个参数PDRIVER_OBJECT pDriverObj是指向该驱动程序对应的驱动程序对象的指针。
从前面设定的驱动程序对象中的函数指针可以看到,主要有两个函数:卸载函数DriverUnload和派遣函数DriverDispatch。DriverUnload函数应该很容易理解,它是在驱动程序被卸载出内存的时候调用,主要做一些释放内存之类的工作。而在我的这个程序中,所有的IRP都是在同一个函数里面进行处理的,这就是派遣函数DriverDispatch(实际上很多WDM程序都是这样做的)。下面就分别介绍一下这两个函数。
最后一步,如果需要完成该请求,那么应该先设置IRP结构中的IoStatus域,然后调用函数IoCompleteRequest:
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = information;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
如果需要向下一层设备对象传递该IRP,则要先初始化往下传递的IRP对应IRP Stack(可以直接将当前的IRP Stack复制给下层IRP Stack),然后调用IoCallDriver函数往下层传递该IRP:
IoCopyCurrentIrpStackLocationToNext(pIrp);
status = IoCallDriver(pLowerDeviceObj, pIrp);
那么,怎样从用户模式的程序中调用驱动?
用户模式的程序要调用驱动,首先就要打开设备,也就是驱动程序中创建的设备对象。这可以通过调用CreateFile函数来实现。CreateFile函数本来是用于打开文件,它的第一个参数就是文件名。而这里,我们以设备名作为它的第一个参数传入,那么该函数打开的就是设备了。这里所说的设备名,实际上是驱动程序里面为设备对象建立的符号连接名。比如用户模式中给出的设备名为” \\.\MyDevice”,I/O管理器在执行名称搜索前先自动把”\\.\”转换成”\??\”,这样就成了” \??\MyDevice”,这就是驱动程序里面建立的符号连接名了。打开设备后,用户模式的程序就可以调用ReadFile、WriteFile和DeviceIoControl等函数向驱动程序发出请求了。
最后,给出我的试验程序的源码。
WDM程序编译出来的并不是我们常见的.exe,而是.sys文件,在未经设置编译环境之前,是不能直接用VC来编译的(这就是为什么会有几百个错误了)。这种类型的文件你可以在WINNT\System32\Drivers里面找到很多。其实驱动程序也是一种PE文件,它同样由DOS MZ header开头,也有完整的DOS stub和PE header,同样拥有Import table和Export table——hoho……那跟普通的PE文件有什么不一样呢?
其实.sys跟.exe文件一样,都是一种PE文件来的。不同的是,.sys文件Import的通常是NTOSKRNL.EXE,而.exe文件Import的通常是KERNEL32.DLL和USER32.DLL。
知道了这些有什么用呢?实际上,由于.sys通常不调用KERNEL32.DLL和USER32.DLL,所以你是不能在设备驱动程序里面调用任何C、C++和Win32函数的,而且也不能用C++关键字new和delete等(可以用malloc和free来代替),而必须使用大量的内核函数。
为了读者的方便,下面我列出一些常见的驱动程序可用的内核函数:
Ex… 执行支持
Hal… 硬件抽象层(仅NT/Windows 2000)
Io… I/O管理器(包括即插即用函数)
Ke… 内核
Ks… 内核流IRP管理函数
Mm… 内存管理器
Ob… 对象管理器
Po… 电源管理
Ps… 进程结构
Rtl… 运行时库
Se… 安全引用监视
Zw… 其他函数
//Driver部分:
#ifdef __cplusplus
extern "C" {
#endif
#include "ntddk.h"
#define DEVICE_NAME L"\\Device\\MyDevice"
#define LINK_NAME L"\\??\\MyDevice"
#define IOCTL_GET_INFO \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS)
void DriverUnload( PDRIVER_OBJECT pDriverObj );
NTSTATUS DriverDispatch( PDEVICE_OBJECT pDeviceObj, PIRP pIrp );
// 驱动程序加载时调用DriverEntry例程:
NTSTATUS DriverEntry( PDRIVER_OBJECT pDriverObj, PUNICODE_STRING pRegistryString )
{
DbgPrint( "DriverEntry!\n" );
NTSTATUS status;
PDEVICE_OBJECT pDeviceObj;
UNICODE_STRING deviceName;
RtlInitUnicodeString( &deviceName, DEVICE_NAME );
status = IoCreateDevice( pDriverObj, 0, &deviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, true, &pDeviceObj );
if( !NT_SUCCESS( status ) )
{
DbgPrint( "Error: Create device failed!\n" );
return status;
}
UNICODE_STRING linkName;
RtlInitUnicodeString( &linkName, LINK_NAME );
status = IoCreateSymbolicLink( &linkName, &deviceName );
if( !NT_SUCCESS( status ) )
{
DbgPrint( "Error: Create symbolic link failed!\n" );
IoDeleteDevice( pDeviceObj );
return status;
}
pDriverObj->DriverUnload = DriverUnload;
pDriverObj->MajorFunction[IRP_MJ_CREATE] =
pDriverObj->MajorFunction[IRP_MJ_CLOSE] =
pDriverObj->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverDispatch;
return STATUS_SUCCESS;
}
void DriverUnload( PDRIVER_OBJECT pDriverObj )
{
DbgPrint( "DriverUnload!\n" );
if( pDriverObj->DeviceObject != NULL )
{
UNICODE_STRING linkName;
RtlInitUnicodeString( &linkName, LINK_NAME );
IoDeleteSymbolicLink( &linkName );
IoDeleteDevice( pDriverObj->DeviceObject );
}
return;
}
NTSTATUS DriverDispatch( PDEVICE_OBJECT pDeviceObj, PIRP pIrp )
{
ULONG information = 0;
PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp );
switch( pIrpStack->MajorFunction )
{
case IRP_MJ_CREATE:
DbgPrint( "Info: Create!\n" );
break;
case IRP_MJ_CLOSE:
DbgPrint( "Info: Close!\n" );
break;
case IRP_MJ_DEVICE_CONTROL:
{
switch( pIrpStack->Parameters.DeviceIoControl.IoControlCode )
{
case IOCTL_GET_INFO:
{
RtlCopyMemory( pIrp->UserBuffer, "This is a test driver!", 23 );
information = 23;
break;
}
default:
break;
}
}
default:
break;
}
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = information;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
#ifdef __cplusplus
}
#endif
//用户模式程序部分:
#include "stdafx.h"
#include "stdio.h"
#include "windows.h"
#define DEVICE_NAME "\\\\.\\MyDevice"
#define CTL_CODE( DeviceType, Function, Method, Access ) ( \
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)
#define FILE_DEVICE_UNKNOWN 0x00000022
#define METHOD_NEITHER 3
#define FILE_ANY_ACCESS 0
#define IOCTL_GET_INFO \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS)
int main(int argc, char* argv[])
{
HANDLE hDevice = CreateFile( DEVICE_NAME, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
if( hDevice == INVALID_HANDLE_VALUE )
{
printf( "Error: Can't open the device!\n" );
return -1;
}
unsigned long numOfBytesReturned;
char info[32] = {0};
if( DeviceIoControl( hDevice, IOCTL_GET_INFO, NULL, 0, info, 32, &numOfBytesReturned,
NULL ) == true )
printf( "Information: %s \n", info );
CloseHandle( hDevice );
Sleep( 3000 );
return 0;
}
总结一下流程
1.从封包池里申请封包 NdisAllocatePacket.
2.设置IRP请求为未决状态 IoMarkIrpPending.
3.利用NDIS_PACKET的保留域来保存上面哪个IRP请求的指针
4.关联内存描符到我们申请的封包上 NdisChainBufferAtFront.
注意如果是win2000以下的系统不能直接使用pIrp->MdlAddress
5.发送数据 NdisSendPackets.
6.在ProtocolSendCompletel里取出上面保存的Irp,设置必要的数据
7. 释放NdisAllocatePacket申请的资源NdisFreePacket.
8.完成Irp请求 IoCompleteRequest
先看2个关键的列程指派
pDriverObject->MajorFunction[IRP_MJ_WRITE] = NdisProtWrite;
protocolChar.SendCompleteHandler = NdisProtSendComplete;
msdn里定义的原型对应如下:
NTSTATUS
XxxDispatchWrite(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
);
VOID ProtocolSendComplete(
IN NDIS_HANDLE ProtocolBindingContext,
IN PNDIS_PACKET Packet,
IN NDIS_STATUS Status );
对于SendCompleteHandler的描述如下:
This is a required function. ProtocolSendComplete is called for each packet transmitted with a call to NdisSend that returned NDIS_STATUS_PENDING as the status of the send operation. If an array of packets is sent, ProtocolSendComplete is called once for each packet passed to NdisSendPackets, whether or not it returned pending.
所以SendCompleteHandler必须被指派
先看xxDispatchWrite
当我们的ring 3程序调用writefile时候,xxDispatchWrite就会被调用。
然后在xxDispatchWrite里调用NdisSend或者NdisSendPackets就可以把数据发送出去了
但是,在这之前我们需要对一些变量初始化,调用别的函数当这一切做好以后,才可以发送数据。
我们先看这2个发送函数的原型:
VOID
NdisSend(
OUT PNDIS_STATUS Status,
IN NDIS_HANDLE NdisBindingHandle, //对应NdisOpenAdapter返回的句柄,它在ndisbind.c里实现了
IN PNDIS_PACKET Packet //一个PNDIS_PACKET结构的东东?
);
VOID
NdisSendPackets(
IN NDIS_HANDLE NdisBindingHandle,
IN PPNDIS_PACKET PacketArray,
IN UINT NumberOfPackets
);
通过比较,我们可以发现,这2个函数都有3个参数,不一样的ndissend有一个返回值,而ndissendpackets没有,这个结果对应了上面关于SendCompleteHandler的描述。
在XxxDispatchWrite开始
PNDIS_PACKET pNdisPacket=NULL;
调用NdisAllocatePacket从包池里分配并初始化pNdisPacket这个包描述符
NdisAllocatePacket(&Status,&pNdisPacket,pOpenContext->SendPacketPool);
其中pOpenContext->SendPacketPool是在ndisbind.c里调用NdisAllocatePacketPool返回的句柄
这里注意,msdn里有段关于NDIS_PACKET结构的描述
Any buffers allocated by lower-level NDIS drivers must be mapped by buffer descriptors that were allocated from the buffer pool with NdisAllocateBuffer. Only highest-level Microsoft? Windows? 2000 protocols can use memory descriptor lists (MDLs) set up by still higher-level drivers as substitutes for NDIS_BUFFER descriptors.
所以在send.c里,还有有段判断系统平台,然后是否调用NdisAllocateBuffer的代码
如果是属于win2k以上的系统,代码就很简单的处理为
pNdisBuffer = pIrp->MdlAddress;
上面的描述解释了为什么可以这样写.
然后需要调用NdisChainBufferAtFront函数,将缓冲区空间的描述符连接到包描述符里的缓冲空间头部
NdisChainBufferAtFront(pNdisPacket, pNdisBuffer);
当调用完成以后,在调用NdisSendPackets发送数据了。
但是因为我们调用NdisSendPackets,而这个函数没有返回值,一但调用NdisSendPackets,那么系统都会调用ProtocolSendComplete,来完成这个Irp请求.
所以,我们需要来在XxxDispatchWrite里来标识这个Irp处于未决状态。
IoMarkIrpPending(pIrp);
如果在列程里调用IoMarkIrpPending,那么列程必须返回STATUS_PENDING ,以表明这个Irp请求没有完成。系统会自动调用ProtocolSendComplete来完成剩下的工作。
我们在来看ProtocolSendComplete,这个函数的原型
VOID ProtocolSendComplete(
IN NDIS_HANDLE ProtocolBindingContext,
IN PNDIS_PACKET Packet,
IN NDIS_STATUS Status );
因为我们需要填充Irp结构里的一些变量,然后调用类似下面的代码
pIrpSp = IoGetCurrentIrpStackLocation(pIrp);
if (Status == NDIS_STATUS_SUCCESS)
{
pIrp->IoStatus.Information = pIrpSp->Parameters.Write.Length;
pIrp->IoStatus.Status = STATUS_SUCCESS;
}
else
{
pIrp->IoStatus.Information = 0;
pIrp->IoStatus.Status = STATUS_UNSUCCESSFUL;
}
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
来完成这个Irp请求
那么就必须保存pIrp这个指针,但是我们看上面的函数原型里的3个参数,并没有和PIrp有关的参数
事实上,ddk里的send.c。利用的Packet里的一个保留域来保存了着PIrp指针,我们可以看到
Packet这个参数被传递过来了
下面看NDIS_PACKET这个结构
typedef struct _NDIS_PACKET {
NDIS_PACKET_PRIVATE Private;
union {
struct {
UCHAR MiniportReserved[2*sizeof(PVOID)];
UCHAR WrapperReserved[2*sizeof(PVOID)];
};
struct {
UCHAR MiniportReservedEx[3*sizeof(PVOID)];
UCHAR WrapperReservedEx[sizeof(PVOID)];
};
struct {
UCHAR MacReserved[4*sizeof(PVOID)];
};
};
ULONG_PTR Reserved[2];
UCHAR ProtocolReserved[1];
} NDIS_PACKET, *PNDIS_PACKET, **PPNDIS_PACKET;
我们在看send.c里,是如何做的,用了一个宏
NPROT_IRP_FROM_SEND_PKT(pNdisPacket) = pIrp;
这个宏的定义
#define NPROT_IRP_FROM_SEND_PKT(_pPkt) \
(((PNPROT_SEND_PACKET_RSVD)&((_pPkt)->ProtocolReserved[0]))->pIrp)
其实就是把PNDIS_PACKET->ProtocolReserved转换成PNPROT_SEND_PACKET_RSVD的指针,然后在指向pIrp
PNPROT_SEND_PACKET_RSVD定义如下
typedef struct _NPROT_SEND_PACKET_RSVD
{
PIRP pIrp;
ULONG RefCount;
} NPROT_SEND_PACKET_RSVD, *PNPROT_SEND_PACKET_RSVD;
其实就是把pIrp存放到了pNDIS_PACKET->ProtocolReserved
看到这里,我就有点奇怪了ProtocolReserved不是字符数组吗?而且就1个字符,那么它就因该是1字节,但是pIrp是个指针
4字节,怎么能这样存喃?
其实不是的,msdn在非常隐秘的地方做了如下的说明....
NIC drivers and intermediate drivers allocate packet descriptors with four pointer's worth (4*sizeof(PVOID)) of ProtocolReserved space to be used by protocols for receive indications. On 32-bit systems, four pointer's worth of ProtocolReserved space equals 16 bytes. On 64-bit systems, four pointer's worth of ProtocolReserved space equals 32 bytes
所以ProtocolReserved更本就不是什么UCHAR类型
在看一下
VOID ProtocolSendComplete(
IN NDIS_HANDLE ProtocolBindingContext,
IN PNDIS_PACKET pNdisPacket,
IN NDIS_STATUS Status );
注意,Packet被传递进来了,而我们刚刚在XxxDispatchWrite将PENDING状态的Irp保存在Packet里了,所以也被传递进来了
下面完成它
PIRP PIrp;
PIO_STACK_LOCATION pIrpSp;
PIrp = NPROT_IRP_FROM_SEND_PKT(pNdisPacket);
NdisFreePacket(pNdisPacket);
pIrpSp = IoGetCurrentIrpStackLocation(pIrp);
if (Status == NDIS_STATUS_SUCCESS)
{
pIrp->IoStatus.Information = pIrpSp->Parameters.Write.Length;
pIrp->IoStatus.Status = STATUS_SUCCESS;
}
else
{
pIrp->IoStatus.Information = 0;
pIrp->IoStatus.Status = STATUS_UNSUCCESSFUL;
}
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
最后完成这个Irp请求,发送完成 Code