📄 网络驱动教程.txt
字号:
对于这个设备,应用程序可以打开、读、写、发出控制命令、关闭。
--------------示例3-----------------
// 打开
HANDLE Handle=CreateFile("\\.\MyNdisDevice",
GENERIC_WRITE|GENERIC_READ,
FILE_SHARE_WRITE|FILE_SHARE_READ,
NULL,OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,NULL);
if(Handle==INVALID_HANDLE_VALUE)
{}// 失败了
else
{}// 成功了
// 写
if(WriteFile(Handle,buf,len,&dlen,NULL))
{}// 成功了
else
{}//失败了
其他的函数调用涉及CloseHandle(),DeviceIoControl(),ReadFile(),使用参数请阅读MSDN,与读写文件并无不同之处。主要要注意的是CreateFile的参数,建议您直接用上边的示例。我不太清楚每个参数的准确含义,但是我用上边的参数总是可以成功。
我不知道有没有更好的办法,我见过的驱动主动通知应用程序的办法是应用程序用一个线程来读驱动。驱动把要通知应用程序的东西让应用程序读出。这需要一个无尽循环的循环来读这个驱动。当无数据可读的时候可以阻塞。当驱动想通知应用(如丢出一笔日志)的时候,写一些东西让应用程序的ReadFile返回即可。
为了处理io请求我们现在来写那个IoDispatch.这个函数在WDM驱动中一般称为分发例程。这个函数在Passive Level运行,因此非常安全,几乎可以调用绝大部分的系统内核服务。
请结合下边的例子,可以看到最简单的是IRP_MJ_CREATE和IRP_MJ_CLOSE调用。如果返回STATUS_SUCCESS则表示成功,STATUS_UNSUCCESSFULE则表示失败。最简单的方法是只让一个进程打开自己,你可以设想一下应该怎样实现,并作为一个小练习。
这里设备采用缓冲模式,这是最为简单的一种方式,其他方式我们不讨论。为了设置为缓冲模式,我们回到示例代码5.1,在注册设备之后,加上这句:
m_pDeviceObject->Flags |= DO_BUFFERED_IO;
ReadFile的处理主要是输出数据。用户提供输出缓冲及其长度。你写入数据,并说明你写入的长度。在IrpStack->Parameters.Read.Length中得到输出缓冲长度。数据写入Irp->AssociatedIrp.SystemBuffer中。实际输出数据长度请写到Irp->IoStatus.Information中即可。
WriteFile的处理与ReadFile类似。不用的是Irp->AssociatedIrp.SystemBuffer成了输入缓冲,而长度在IrpStack->Parameters.Write.Length中。
DeviceIoControl的情况稍微复杂,一般先要得到一个功能码,用户程序一般要输入数据(在输入缓冲中),同时要获得输出(请你写入输出缓冲中),并指明了这些缓冲区的长度。你还必须指明你输出数据的真实长度。
功能码在IrpStack->Parameters.DeviceIoControl.IoControlCode;
缓冲模式,输入缓冲长度为IrpStack->Parameters.DeviceIoControl.InputBufferLength;
缓冲模式,输出缓冲长度为IrpStack->Parameters.DeviceIoControl.OutputBufferLength;
共用缓冲区为Irp->AssociatedIrp.SystemBuffer;
实际输出数据长度请写到Irp->IoStatus.Information中。
用下边的方法返回失败最为完整:注意指定了为参数错误。你也可以指定其他错误。请查阅ddk帮助。
Irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return STATUS_INVALID_PARAMETER;
用下边的方法返回成功:
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return STATUS_SUCCESS;
//------------示例4--------------
NTSTATUS IoDispatch(IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
BOOLEAN ret = FALSE;
KIrp I(Irp);
PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
switch(I.MajorFunction())
{
case IRP_MJ_CREATE:
// 如果有进程调用了CreateFile
ret = TRUE;
break;
case IRP_MJ_CLOSE:
// 如果有进程调用了CloseFile
ret = TRUE;
break;
case IRP_MJ_CLEANUP:
ret = TRUE;
break;
case IRP_MJ_READ:
// 有进程调用了ReadFile()
ret = FALSE;
break;
case IRP_MJ_WRITE:
// 如果调用了WriteFile()
ret = FALSE;
break;
case IRP_MJ_DEVICE_CONTROL:
ret = FALSE;
break;
default:
ret = FALSE;
};
if(ret)
{
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
else
{
Irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return STATUS_INVALID_PARAMETER;
}
}
好,希望我已经说明白了驱动与应用之间如何交互。如果你还不是很理解,请留意下一章的实际例子。我们将做一个简单的中间层驱动,您可以通过应用程序来配置这个驱动的一些行为(如允许所有的包通过,或是禁止所有包通过)。
第六课 一个实际的中间层驱动
第O课中我们已经建立了一个框架,现在在浏览几个重要的函数。最重要的是两个OnReceive和一个OnSend.
先看框架生成的第一个OnRecevie.注意我的工程名为My.
//---------------示例6.1--------------------
NDIS_STATUS MyAdapter::OnReceive
(const KNdisPacket& Original, KNdisPacket& Repackaged)
{
TRACE("MyAdapter::OnReceive() %u bytesn", Original.QueryTotalLength());
HEADER* Content = (HEADER*) Original.QueryFirstBuffer();
Repackaged.CloneUp(Original);
return NDIS_STATUS_SUCCESS;
}
这个函数似乎比较好理解。当计算机接受到网络数据包的时候,我们的OnReceive被调用。Original是就是数据包。请结合第一课中的内容。KNdisPacket是一个网络数据包,但是其实际内容放在KNdisBuffer中。一个KNdisPacket拥有一个KNdisBuffer链。
KNdisBuffer可以直接转成任何地址来使用,也可以用KNdisBuffer::Address()来得到地址。
那么如上边的示例,Original.QueryFirstBuffer()得到数据包头。
注意,第一个缓冲区的长度至少为14个字节(其实我并不这么肯定,但是我每次都恰好至少得到了14个字节),但是不要指望第一个KNdisBuffer就直接帮你搞定后边的ip头,tcp头数据。一个链式的结构非常的不好处理,所以我建议费点力气把头的部分复制到一个连续缓冲区中。一般都处理到tcp头即可,也不过14+20+20才54个字节。拷贝起来是不费多少资源的。
假设有一个KNdisPacket Packet,你可以用下边的例子把前54个字节的数据拷贝到MyAheadBuf中。
//----------------示例6.2---------------------
UINT nBufCnt = Packet.QueryBufferCount();
KNdisBuffer Buf = Packet.QueryFirstBuffer();
if (!Buf.IsValid())
return;
unsigned char MyAheadBuf[14+20+20];
unsigned long MyAheadBufLen = 0,WantLen = 14+20+20;
// 拷贝足够的长度
while(WantLen > MyAheadBufLen)
{
if(WantLen - MyAheadBufLen > Buf.Length())
{
memcpy(&MyAheadBuf[MyAheadBufLen],Buf.Address(),Buf.Length());
MyAheadBufLen += Buf.Length();
Buf = Buf.GetNext();
if(!Buf.IsValid())
break;
}
else
{
memcpy(&MyAheadBuf[MyAheadBufLen],Buf.Address(),WantLen-MyAheadBufLen);
MyAheadBufLen = WantLen;
}
}
不幸的是并不是所有的计算机上都只调用上一个OnRecevie.另一个可能被调用的OnReceive函数如下。这可能和下层的微端口驱动的特性有关。下边这个函数HeaderBuffer是以太网包头,长度一般为14.LookAheadBuffer是以太网包头之后的部分,如果这是个ip包,那就是ip头了。我们能否驱动到我们关心的ip头和tcp头?这需要LookAheadBufferLength至少为40.我认为LookAheadBufferLength长度至少为40.因为我似乎还没有发现过少于40的情况。
如果不是,请发邮件给我MFC_Tan_Wen@163.com,非常感谢。
这导致我们可以直接用LookAheadBuffer来得到足够的信息做大多数的工作。但是并不总是这样的。比如我要对整个数据包加密,我有必要得到整个数据包。但是LookAheadBuffer可能小于PacketSize.这种情况,你必须调用TransferData()函数,并在MyAdpater::OnTransferComplete()中处理这个数据包。但是OnTransferComplete()这种情况恰好和上一种OnReceive()的情况类似,所以这里不再详述了。
//------------------示例6.3---------------------------
NDIS_STATUS MyAdapter::OnReceive(
IN OUT KNdisPartialPacket& PacketToAccept,
IN PVOID HeaderBuffer, IN UINT HeaderBufferSize,
IN PVOID LookAheadBuffer, IN UINT LookaheadBufferSize,
IN UINT PacketSize)
{
TRACE("MyAdapter::OnReceive() Partial %u/%u%/%u bytesn",
HeaderBufferSize, LookaheadBufferSize, PacketSize);
UNREFERENCED_PARAMETER(PacketToAccept);
return NDIS_STATUS_SUCCESS;
}
OnSend()在有数据包发出的情况下被调用,处理方法应该与第一种OnReceive的方法相同。
如果我想阻止数据包的接收或者发送,我直接return NNDIS_STATUS_NOT_ACCEPTED即可。并且我不向上或者向下复制数据包。
请定义一个全局的变量BOOLEAN Allow.并假定Allow如果为TRUE,我则不干涉所有数据包的发送接受。而如果为FALSE,我丢弃接收到的所有数据包,并阻止所有的发送包。
回到示例6.1,内容应该改为
TRACE("MyAdapter::OnReceive() %u bytesn", Original.QueryTotalLength());
if(Allow)
{
HEADER* Content = (HEADER*) Original.QueryFirstBuffer();
Repackaged.CloneUp(Original);
return NDIS_STATUS_SUCCESS;
}
else
return NDIS_STATUS_NOT_ACCEPTED;
其他的两个函数请自己修改。
现在的问题是我必须用应用程序来设置这个变量。以便控制我的网络是否连通。这样的一个防火墙只有两个选项,全开或者全关,当然这是一个无实际用处的防火墙。
回到上一章节的叙述,我们在这个驱动中加入注册一个WDM设备。但是仅仅做了上边的事情无法通过编译。DriverNetworks的帮助指出,要注册WDM设备必须在向导中选中NDIS WDM选项。但是实际上只有微端口驱动才有这个选项。因此必须用我下边所说的方法:
首先包含头文件<kndisvdw.h>。然后编译预定义宏定义增加这几个:NTVERSION='WDM',NDIS_WDM=1.此时编译ok,但是连接未必能搞定。
为此建议建立一个微端口驱动,选中wdm,编译过程中会跳出来要你编译很多lib,这时全部编译之即可。
回头来,您的中间层驱动已经成功的加入了WDM设备。
现在考虑如何用WDM设备来控制开与断。我设计一个DeviceIoCtrl来做这个事情:
定义:
#define IOCTL_SET_NET_OPEN_OR_CLOSE CTL_CODE(
FILE_DEVICE_UNKNOWN,
0x802,
METHOD_BUFFERED,
FILE_ANY_ACCESS)
这个指令带一个字节的参数保存在输入缓冲中。如果为0则全断,反之全开。
然后修改IoDispatch函数,加入以下的处理:
...
ULONG ControlCode = IrpStack->Parameters.DeviceIoControl.IoControlCode;
ULONG InputLength = IrpStack->Parameters.DeviceIoControl.InputBufferLength;
ULONG OutputLength = IrpStack->Parameters.DeviceIoControl.OutputBufferLength;
switch(ControlCode)
{
case IOCTL_SET_NET_OPEN_OR_CLOSE:
{
// 主要的任务是首先检查长度是否足够
if( 1 > OutputLength)
{
ret = FALSE;
break;;
}
// 得到输入参数并设置自己全开全闭
UCHAR cAllow = *((UCHAR *)Irp->AssociatedIrp.SystemBuffer);
Allow = (cAllow==0)?FALSE:TRUE;
DWW_RULE_S *pDst = (DWW_RULE_S *)Buffer;
Irp->IoStatus.Information
ret = TRUE;
break;
}
...
}
至于应用程序如何调用DeviceIoControl,请参照上一课的例子。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -