NDIS网络驱动分类
协议驱动:上层直接提供应用层socket使用的数据传输接口,下层绑定小端口驱动用于发送和接收以太网包、小端口驱动:直接针对网卡,给协议驱动提供接收和发生数据的能力
中间层驱动:以一种特殊的方式插入到协议驱动和小端口驱动之间,作为过滤驱动的最佳选择。
协议驱动
使用wdk下的ndisport工程,不提供传输层接口,只是只有ReadFile、WriteFile和DeviceIoControl进行接收和发生数据包。//若提供传输层的socket接口,也就是在DeviceIocontrol函数内设置应用层的bind、connect、accept等接口函数。
主要编写过程:
(1)在DriveEntry中填写协议特征(回调函数列表)
(2)在DriverEntry中使用NdisRegisterProtocolDriver把自己注册为协议驱动
(3)系统对每个实际存在的网卡实例调养本驱动的回调函数,决定是否要绑定网卡。
(4)发生各种事件时,调用回调函数。
(5)当应用层试图发生一个以太网包时,可以打开这个协议并发生请求。
协议驱动中的DriverEntry
(1)使用IoCreateDeviceSecure函数来生成控制设备。(2)IoCreateSymbolicLink创建控制设备符号链接
(3)初始化NDIS_PROTOCOL_CHARACTERISTICS结构体的协议驱动回调函数
(4)使用NdisRegisterProtocol函数,通知NDIS库要注册一个NDIS协议驱动。
(5)填写使用IoCreateDeviceSecure函数创建的控制设备的分发函数。
协议与网卡的绑定
设备对象之间的绑定和协议和网卡之间的绑定不同,一个协议驱动绑定了网卡意味着:(a)网卡收到的数据包会提交给这个协议
(b)协议驱动可以使用这个网卡发送数据。
(c)协议与网卡的绑定时多对多的
协议驱动的绑定与解绑定时调用以下的回调函数,
protocolChar.BindAdapterHandler = NdisProtBindAdapter;
protocolChar.UnbindAdapterHandler = NdisProtUnbindAdapter;
下面看绑定网卡的实现:
函数NdisProtBindAdapter实现网卡的绑定,使用NdisOpenAdapter函数对网卡打开,对网卡的打开就是对网卡的绑定,在这个驱动里,每当有一个网卡被绑定,则分配一个内存空间,用来保存和这次绑定相关的一些信息,以及这次绑定所需要的资源,比如锁、队列等等这就是打开上下文。
VOID
NdisProtBindAdapter(
OUT PNDIS_STATUS pStatus,
IN NDIS_HANDLE BindContext,
IN PNDIS_STRING pDeviceName,
IN PVOID SystemSpecific1,
IN PVOID SystemSpecific2
)
NdisProtBindAdapter主要工作是: (a)打开上下文BindContext的分配和初始化,初始化几个用到的数据成员。锁、读队列、写对队列、包队列等等
(b)由SystemSpecific1参数读取注册表配置,判断当前的执行环境。
(c)将这个上下文保存到全局链表,并调用ndisprotCreateBinding函数正式绑定
ndisprotCreateBinding函数是真正的进行协议驱动和网卡的绑定:
主要工作:
(a)设法防止多线程竞争
(b)分配和初始化这次绑定所需要的资源
(c)获取网卡的一些参数
最主要的绑定函数为
VOID
NdisOpenAdapter(
OUT PNDIS_STATUS Status,//状态,
OUT PNDIS_STATUS OpenErrorStatus,//错误码
OUT PNDIS_HANDLE NdisBindingHandle,//返回绑定的句柄
OUT PUINT SelectedMediumIndex,//最终选择的媒体介质在数组的索引
IN PNDIS_MEDIUM MediumArray,//媒介质数组,含有这个驱动支持的所有的媒体介质类型,
IN UINT MediumArraySize,//媒体介质数组的大小
IN NDIS_HANDLE NdisProtocolHandle,//协议句柄
IN NDIS_HANDLE ProtocolBindingContext,//上下文指针,这个指针会再次出现在绑定完成函数中,以便信息被传递到绑定的完成函数中。
IN PNDIS_STRING AdapterName,//要绑定的网卡的名字,是函数ndisprotCreateBinding的参数pBindingInfo传递的
IN UINT OpenOptions,//0
IN PSTRING AddressingInformation OPTIONAL,//NULL
);
最主要的是返回的NdisBindingHandle句柄,此句柄表征一个协议和网卡之间的绑定关系的存在,之后的许多操作,比如协议要发送数据等,都要提供这个句柄,以便知道内核数据要发送到哪个网卡上。
当返回NDIS_STATUS_PENDING的时候,一般都会设置一个事件去等待,在完成的时候会调用完成函数,在完成函数内设置事件有效即可,此处的完成函数为
protocolChar.OpenAdapterCompleteHandler = NdisProtOpenAdapterComplete;
最麻烦的是必须小心的处理绑定和解除绑定的同步问题,由于内核中的多线程环境下,网卡设备为即插即用设备。(有可能在绑定的同时,拔掉了网卡调用的解除绑定的操作)
ndisprotCreateBinding函数内绑定的竞争问题的解决,需要设置一些标志并配合自旋锁来控制。自旋锁内只负责查询和更变标志位,根据标志位判断是直接退出还是继续执行。自旋锁是很耗资源的锁,不适宜耗时长的操作,并且会提高IRQL。
分配接收和发送的包池和缓冲池 包池是预先已经分配好的“包描述符”,缓冲池是一组已经分配好的“包缓冲区描述符”。 在NDIS开发中,每一个以太网包用一个包描述
符(NDIS_PACKET)来描述,但包的实际内容并不包含在包描述符内,而是专门有一个包描述符缓冲区描述符(NDIS_BUFFER)来描述。
为什么上下文需要分配包池? 打开上下文是用来保存一个协议绑定到一个网卡的相关信息的。在协议看来,每个打开上下文就对应了一个网卡,这些网卡的包接收到之后,必须
要保存起来等待上层应用来取走。同时上层应用可能要求发送一些包,这些包需要先保存到发送缓冲区中再发送出去。因此适合在这里保存发送缓冲区和接收缓冲区的信息。具体的做法是先分配两个包池,包池的大小就是所能容纳的包的个数。这样在具体的以太网包存入的时候就不必再次分配包描述符和包缓冲区描述符了,减少的动态内存分配的消耗。
用函数NdisAllocatePacketPoolEx分配包池
用函数NdisAllocateBufferPool来分配缓冲区描述符池
通过OID请求获取网卡信息:
通过自己封装的ndisprotDoRequest函数实现,内部其实是调用的NdisRequest函数进行通信的。函数原型如下:
VOID
NdisRequest(
OUT PNDIS_STATUS Status,
IN NDIS_HANDLE NdisBindingHandle,
IN PNDIS_REQUEST NdisRequest
);
在NDIS中,所有的OID请求都用它发送。参数status表示返回的结果,
为NDIS_STATUS_SUCCESS表示成功,
为NDIS_STATUS_PENDING表示结果未决,此时设置一个事件等待,直到请求完成时在完成函数内设置事件有效,
当请求完成时会调用请求的完成函数:protocolChar.RequestCompleteHandler = NdisProtRequestComplete;
为其他则表示失败。
调用NdisRequest主要是填写NDIS_REQUEST结构:
typedef struct _NDIS_REQUEST {
UCHAR MacReserved[4*sizeof(PVOID)];
NDIS_REQUEST_TYPE RequestType;
union _DATA {
struct QUERY_INFORMATION {
NDIS_OID Oid;
PVOID InformationBuffer;
UINT InformationBufferLength;
UINT BytesWritten;
UINT BytesNeeded;
} QUERY_INFORMATION;
struct SET_INFORMATION {
NDIS_OID Oid;
PVOID InformationBuffer;
UINT InformationBufferLength;
UINT BytesRead;
UINT BytesNeeded;
} SET_INFORMATION;
} DATA;
} NDIS_REQUEST, *PNDIS_REQUEST;
协议与网卡的解除绑定
解除绑定使用内核函数NdisCloseAdapterEx函数,原型如下:NDIS_STATUS
NdisCloseAdapterEx(
IN NDIS_HANDLE NdisBindingHandle
);
返回的状态若为NDIS_STATUS_PENDING,则设置事件,等待完成后在完成函数内设置事件有效。
protocolChar.UnbindAdapterHandler = NdisProtUnbindAdapter;
protocolChar.CloseAdapterCompleteHandler = NdisProtCloseAdapterComplete;
需要注意的问题:
(a)只有已经绑定成功了才需要解除绑定,其他情况下,直接返回。
(b)若刚刚开始绑定,在还没有绑定完成的情况下解除绑定,则直接放弃这次解除绑定操作
(c)必须通过OID发送请求,使网卡停止向这个协议驱动提交新的数据包,以便释放各个资源的接收缓冲区和发送缓冲区。
大多数的处理操作,都放到的工具函数ndisprotShutdownBinding函数里面,主要工作为:
(1)处理多线程竞争问题
(2)停止接收和发送数据包
(3)处理掉未完成的请求
(4)调用NdisCloseAdapter
(5)清理掉所有打开上下文分配的资源
在用户态操作协议驱动
(1)使用CreateFile函数打开协议驱动的CDO设备,得到一个句柄(2)使用DeviceIoControl函数来与CDO设备进行控制通信
(3)使用WriteFile函数发送数据包,使用ReadFile函数来接收数据包
(4)使用CloseHandle函数来关闭设备
在内核态完成功能的实现
在内核层提供上面应用层程序的接口,本质上也就是处理不同IRP类型的的派遣函数。如下: pDriverObject->MajorFunction[IRP_MJ_CREATE] = NdisProtOpen;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = NdisProtClose;
pDriverObject->MajorFunction[IRP_MJ_READ] = NdisProtRead;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = NdisProtWrite;
pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = NdisProtCleanup;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = NdisProtIoControl;
pDriverObject->DriverUnload = NdisProtUnload;
协议驱动接收回调
和接收包有关的回调函数 protocolChar.ReceiveHandler = NdisProtReceive;
protocolChar.ReceiveCompleteHandler = NdisProtReceiveComplete;
protocolChar.TransferDataCompleteHandler = NdisProtTransferDataComplete;
protocolChar.ReceivePacketHandler = NdisProtReceivePacket;
ReceiveHandler和ReceivePacketHandler都是接收回调函数指针,当被绑定的网卡收到数据包时,windows内核函数会调用这个两个函数,至于调用哪个函数是不确定的,所以任何情况下都处理这个两个函数,避免漏掉数据包。
ReceiveCompleteHandler函数指针所指向的函数总是在ReceiveHandler被调用完成以后被调用。
ReceiveHandler函数指针所指向的函数原型如下:
NDIS_STATUS NdisProtReceive(
IN NDIS_HANDLE ProtocolBindingContext,//绑定时的上下文句柄
IN NDIS_HANDLE MacReceiveContext,//保留给下层驱动使用,调用NdisTransferData函数时使用
__in_bcount(HeaderBufferSize) IN PVOID pHeaderBuffer,//以太网包头
IN UINT HeaderBufferSize,//包头的长度,只看数据内容的前几个字节(例如TCP头)就确定是否是本协议需要处理的
__in_bcount(LookaheadBufferSize) IN PVOID pLookaheadBuffer,//前视缓冲区,
IN UINT LookaheadBufferSize,//前视缓冲区长度
IN UINT PacketSize//完整的包长度(不包含以太网包头,若PacketSize==LookaheadBufferSize表明接收的是一个完整数据包)
)
显然协议驱动在这个回调函数中并没有得到完整的数据,若要获取完整的数据,需要调用NdisTransferData。调用之后,在数据包传输完成之后,协议驱动的回调函数指针TransferDataCompleteHandler所指向的回调函数将被调用。在此函数内能获取完整的数据。
ReceivePacketHandler 函数指针所指向的函数原型如下:
INT
NdisProtReceivePacket(
IN NDIS_HANDLE ProtocolBindingContext,
IN PNDIS_PACKET pNdisPacket
)
这个函数是更高级的回调,直接传递包描述符,避免了调用NdisTransferData的麻烦。而且传递包描述符也就说明协议驱动和下层网卡驱动可以共用包缓冲区,也就避免了内存拷贝的问题,不用担心效率问题了。
NdisProtReceive函数的实现:
(a)首先根据函数传递的参数获取绑定上下文,然后判断以太网包头的长度是否正确。
(b)调用函数ndisprotAllocateReceivePacket实现分配包描述符和缓冲区描述符以及分配内存空间,ndisprotAllocateReceivePacket函数通过返回值返回了包描述符,通过参数指针返回了缓冲区描述符(包括以太网包头和数据的空间),并且使用NdisChainBufferAtFront将分配的缓冲区描述符挂接到了包描述符上(因此返回的缓冲区描述符指针便是直接指向了包描述符的缓冲区描述符空间上)。
(c)ndisprotAllocateReceivePacket返回的缓冲区描述符指针为pRcvData,然后使用NdisMoveMappedMemory函数将以太网包头的内存进行拷贝,拷贝到pRcvData指向的缓冲区描述符空间。然后根据参数PacketSize 和 LookaheadBufferSize进行检查,看前视缓冲区内是否传递进来的包是完整包。
(d)若是完整包,则使用函数NdisCopyLookaheadData在pRcvData+HeaderBufferSize(以太网包头的长度)位置,将前视缓冲区的内存拷贝到此。然后使用ndisprotQueueReceivePacket函数将包描述符插入队列即可。
(e)若不是完整包,则需要首先分配一个新的缓冲描述符(不包含以太网包头的长度),然后使用NdisUnchainBufferAtFront函数就把原有的缓冲描述符解链,是原来的缓冲描述符脱离包描述符。接着使用NPROT_RCV_PKT_TO_ORIGINAL_BUFFER宏将原有的缓冲描述符保存到包描述符的ProtocolReserved[0]内(保留在NdisProtTransferDataComplete函数内需要将此还原),然后使用NdisTransferData函数将包描述符发送给底层绑定的网卡,待完成以后会将底层绑定的网卡的数据复制,然后调用协议驱动的TransferDataCompleteHandler完成函数指针所指向的函数。
注:分配新的缓冲区描述符的原因?
用NdisTransferData传输数据的时候,不传输以太网包头。也就是说,若传入一个包描述符,用NdisTransferData进行数据传送时,这个函数会写入完整的数据包,到不包括以太网包头。写入的目标为包描述符所连接的包缓冲区描述符所指的缓冲区。
本节代码中分配的包描述符已经含有可以容纳完整数据包的缓冲区,并且使用NdisMoveMappedMemory函数将以太网包头进行拷贝到缓冲区描述符中。但如果将这个包描述符直接传递给NdisTransferData进行传输,此函数会写入不含以太网包的数据,而且从缓冲区的开头开始写,就会把以前拷贝的以太网包头的数据覆盖掉了。
TransferDataCompleteHandler函数实现:
(a)在TransferDataCompleteHandler函数内,首先通过NPROT_RCV_PKT_TO_ORIGINAL_BUFFER宏获取原始的缓冲描述符(其实就是ProtocolReserved[0]内的数据)保存在pOriginalBuffer变量,然后判断获取的原始缓冲区描述符是否为NULL
(b)为NULL则表示没有进行缓冲描述符替换,可以直接插入的队列
(c)不为NULL,则表示原来替换过缓冲描述符,在此需要将缓冲描述符还原,首先使用NdisUnchainBufferAtFront函数将包描述符上的缓冲描述符解链(保存在pPartialBuffer变量),然后使用NdisChainBufferAtBack宏将原始缓冲描述符pOriginalBuffer挂接,并使用NdisFreeBuffer将解链的缓冲描述符pPartialBuffer释放。
NdisFreeBuffer仅仅释放缓冲区描述符,并不释放缓冲区。
ReceivePacketHandler函数的实现:
此函数返回的是一个引用计数,引用计数是指接收到的包描述符被本协议驱动使用的次数。以为包如果被本驱动使用,则下层网卡不能释放这个包,只有在引用计数为0时才能释放。因为我们是重用这个包,并不是自己分配一个新的,再拷贝其数据,因此必须返回一个引用计数,一般返回1即可。
在下层驱动资源紧张时,必须释放该包时,本驱动必须分配新包并将数据拷贝到新的包描述符内。可以使用(NDIS_GET_PACKET_STATUS(pNdisPacket) == NDIS_STATUS_RESOURCES)判断是否可以为重用包。
接收数据包入队:
实现函数为ndisprotQueueReceivePacket
将收到的数据包添加的到队列,使用的链表为LIST_ENTRY。使用到的宏:
//从包指针得到链表节点指针,使用两个CONTAINING_RECORD嵌套
#define NPROT_LIST_ENTRY_TO_RCV_PKT(_pEnt) \
CONTAINING_RECORD(CONTAINING_RECORD(_pEnt, NPROT_RECV_PACKET_RSVD, Link), NDIS_PACKET, ProtocolReserved)
//从链表节点指针得到缓冲区描述符指针
#define NPROT_RCV_PKT_TO_LIST_ENTRY(_pPkt) \
(&((PNPROT_RECV_PACKET_RSVD)&((_pPkt)->ProtocolReserved[0]))->Link)
说明,将ProtocolReserved[0]数据强制转换为PNPROT_RECV_PACKET_RSVD类型,然后得到PNPROT_RECV_PACKET_RSVD这个结构内的Link指针,但是这里不是很明白,在函数内的调用是这样的pEnt = NPROT_RCV_PKT_TO_LIST_ENTRY(pRcvPacket);这样的话,pRcvPacket的类型为PNDIS_PACKET,pRcvPacket内的ProtocolReserved[0]的缓冲区为UCHAR的,此缓冲区内的数据应该为NDIS_BUFFER类型的数据(个人觉得),此处强制转化为PNPROT_RECV_PACKET_RSVD类型的数据难道没问题吗?下面是PNPROT_RECV_PACKET_RSVD类型的结构体:
typedef struct _NPROT_RECV_PACKET_RSVD
{
LIST_ENTRY Link;
PNDIS_BUFFER pOriginalBuffer; // used if we had to partial-map
} NPROT_RECV_PACKET_RSVD, *PNPROT_RECV_PACKET_RSVD;
这个问题,估计是对LIST_ENTRY没有理解透彻,先在此记录下来。
ndisprotQueueReceivePacket函数总的功能是将一个包描述符插入到队列。其中做了一些检查,例如检查网卡的电源状态(在节能状态下直接丢包的),检查队列的大小。当队列中包过多时必须删除。最后调用ndisprotServiceReads函数,这个函数用于查看是否有未完成的读请求(用户层应用程序要求读包的请求),如果有就取出数据包完成它。
ndisprotServiceReads函数实现:
函数功能:这个函数是一个服务函数,当读请求队列和接收队列不为空时,从包中取数据,拷贝到IRP,然后完成这个IRP。
(a)使用一个大循环,判断请求队列和接收队列是否为空。
while (!NPROT_IS_LIST_EMPTY(&pOpenContext->PendedReads) &&
!NPROT_IS_LIST_EMPTY(&pOpenContext->RecvPktQueue))
(b) 使用一个while循环,在读请求队列中取出一个有效的IRP,若无irp或者仅有正在被取消的IRP则break。
pIrpEntry = pOpenContext->PendedReads.Flink;
while (pIrpEntry != &pOpenContext->PendedReads)
(c)取出第一个包描述符
// 得到第一个包(最旧的),出队列。
pRcvPacketEntry = pOpenContext->RecvPktQueue.Flink;
NPROT_REMOVE_ENTRY_LIST(pRcvPacketEntry);
pOpenContext->RecvPktCount --;
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
NPROT_DEREF_OPEN(pOpenContext);
// 从节点获得包。
pRcvPacket = NPROT_LIST_ENTRY_TO_RCV_PKT(pRcvPacketEntry);
(d)一个包描述符(NDIS_PACKET)的包数据实际是存在缓冲区描述符(NDIS_BUFFER)所描述的缓冲区里面的。多个NDIS_BUFFER串联成链表,即一个NDIS_PACKET内可能有多个NDIS_BUFFER,它们串联成链表。利用NdisGetNextBuffer可以从上一个节点得到下一个节点,而IRP的缓冲区是连续的,因此需要使用一个循环来拷贝,尽量多的将数据拷贝到输出缓冲区(以数据缓冲区和包长度中小的一个为限)。
pDst = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);
NPROT_ASSERT(pDst != NULL); // since it was already mapped
BytesRemaining = MmGetMdlByteCount(pIrp->MdlAddress);
pNdisBuffer = NDIS_PACKET_FIRST_NDIS_BUFFER(pRcvPacket);
// 请注意,每个PNDIS_BUFFER都是一个PMDL,同时PNDIS_BUFFER
// 本身都是链表。用NdisGetNextBuffer可以从一个得到它的下面一个。
// 包的数据实际上是保存在一个缓冲描述符链表里的。
while (BytesRemaining && (pNdisBuffer != NULL))//拷贝满缓冲区或者拷贝完一个包描述符
{
NdisQueryBufferSafe(pNdisBuffer, &pSrc, &BytesAvailable, NormalPagePriority);
if (pSrc == NULL)
{
DEBUGP(DL_FATAL,
("ServiceReads: Open %p, QueryBuffer failed for buffer %p\n",
pOpenContext, pNdisBuffer));
break;
}
// 如果还可以继续拷贝,就继续拷贝。
if (BytesAvailable)
{
ULONG BytesToCopy = MIN(BytesAvailable, BytesRemaining);
NPROT_COPY_MEM(pDst, pSrc, BytesToCopy);
BytesRemaining -= BytesToCopy;
pDst += BytesToCopy;
}
NdisGetNextBuffer(pNdisBuffer, &pNdisBuffer);
}
(e)然后就是将IRP完成
// 拷贝好数据之后,结束IRP即可。
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = MmGetMdlByteCount(pIrp->MdlAddress) - BytesRemaining;
DEBUGP(DL_INFO, ("ServiceReads: Open %p, IRP %p completed with %d bytes\n",
pOpenContext, pIrp, pIrp->IoStatus.Information));
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
(f)由于在接收包时,有两种情况:一种是重用绑定网卡的包,另一种是分配新包并将绑定网卡的包进行拷贝。分配的新包所在的包池肯定就是接收包池(NdisGetPollFromPackets)。包是自己分配出来的不需要归还,若是重用绑定网卡的包需要归还(NdisReturnPackets归还包)。
// 如果这个包描述符不是从接收包池里分配的,那么就是从
// 网卡驱动里重用的。如果是重用的,调用NdisReturnPackets
// 归还给网卡驱动,让它释放。
if (NdisGetPoolFromPacket(pRcvPacket) != pOpenContext->RecvPacketPool)
{
NdisReturnPackets(&pRcvPacket, 1);
}
else
{
// 否则的话自己释放。
ndisprotFreeReceivePacket(pOpenContext, pRcvPacket);
}
NPROT_DEREF_OPEN(pOpenContext); // took out pended Read
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
pOpenContext->PendedReadCount--;