概要:开发一个NDIS驱动是一项相对复杂的工作,这一方面是由于核心驱动本身有更多的限制和要求,有更多的“游戏规则”要求开发者理解和掌握,NDIS 的复杂性把难度更是提高了,本文以PassThru为例,加上自己的理解,讲述了NDIS驱动的处理过程和在PassThru的基础上进行扩展的基本方法,本文并不是一个入门读物,所以没有提及任何核心驱动开发的相关知识,本文主要讲述的是NDIS中间层对数据包处理的流程。在阅读过程中,关于相关 API的用法,或其它信息,请参看DDK文档。
一 NDIS驱动程序分类.
NDIS(Network Driver Interface Specification)是Windows网络驱动程序接口标准,NDIS驱动程序分为三类:
1. NDIS Miniport NIC Driver: 底层的微端口NIC驱动,这就是网络设备的物理的驱动程序了。
2. NDIS Protocol Driver: 高层的协议驱动,用来实现某个具体的协议栈,如TCP/IP协议栈,
并向上导出TDI接口。
3. NDIS Intermediate Driver: 中间层驱动,位于Miniport Driver和Protocol Driver之间。
二 NDIS驱动结构简介.
其中,最上层是一个NDIS Protocol Driver,它向上提供一个Transport Driver Interface(TDI),向下通过NDIS接口与下面的NDIS中间层的上边界交互,NDIS中间层的下边界通过NDIS接口与下层的NDIS Miniport Driver交互。最后,由NDIS Miniport Driver利用NDIS接口与物理网络设备NetCard交互。NetCard是由不同的网卡设备产商提供的,而NDIS接口库是由Microsoft 开发好的,为什么NDIS Miniport Driver不是直接与物理网卡交互,而是通过NDIS接口与下物理网卡交互呢?(我想很多人都会和我当初一样,有这个疑问)。
事实上,这是由于Windows系统为了提高可移植性,而设计出一个硬件抽象层(HAL),硬件抽象层在内部处理不同的硬件之间的差异,并且暴露出一个统一的接口给核心驱动开发者。例如:在Intel构架的系统中,内存和外部设备的端口采用分别编址,如果要从某个外部设备的端口上读写数据的话,可能要通过专用指令IN/OUT读写,而在Alpha构架的系统上,采用的是统一编址的方式,所以对外部设备的IO端口进行读写的话还是通过访问内存的指令,HAL提供一组服务支持函数,如果要访问外部设备上的端口数据可以使用READ_PORT_UCHAR/WRITE_PORT_UCHAR等等,核心驱动开发者不用去考虑不同硬件构架的之间的差异。在NDIS Miniport Driver中,NetCard驱动的程序,正是这样通过NDIS接口提供的一组类似功能的函数,与物理的网络设备进行交互。其中,最上层是一个NDIS Protocol Driver,它向上提供一个Transport Driver Interface(TDI),向下通过NDIS接口与下面的NDIS中间层的上边界交互,NDIS中间层的下边界通过NDIS接口与下层的NDIS Miniport Driver交互。最后,由NDIS Miniport Driver利用NDIS接口与物理网络设备NetCard交互。
三 NDIS驱动程序的数据处理流程
3.1 三种NDIS驱动程序的关系。
通常一个NDIS Protocol Driver 的上边沿导出TDI接口,并在其下边沿向NDIS注册一组Protocolxxx操作例程;一个NDIS Miniport Driver则在其下边沿通过NDIS接口操作物理网络设备,并在其上边沿向NDIS注册一组Miniportxxx操作例程。当一个中间层介入的时候,必需遵守这个规则,因此,中间层驱动对上层来说,扮演一个Miniport Driver的角色,它在上边沿向NDIS注册一组Miniportxxx函数;对于下层Miniport Driver来说,中间层驱动扮演一个Protocol Driver的角色,因此它在下边沿向NDIS注册一组Protocolxxx函数。Miniport Driver通过调用NdisMRegisterMiniport向NDIS注册一组MiniportXxx函数。其中原型如下:
NDIS_STATUS NdisMRegisterMiniport(
IN NDIS_HANDLE NdisWrapperHandle,
IN PNDIS_MINIPORT_CHARACTERISTICS MiniportCharacteristics,
IN UINT CharacteristicsLength
);
其中,NdisWrapperHandls是之前通过调用NdisMInitializeWrapper取得的句柄MiniportCharacteristics包含一组MiniportXxx函数指针。
Protocol Driver 通过调用 NdisRegisterProtocol向NDIS注册一组ProtocolXxx函数。其中原型如下:
VOID NdisRegisterProtocol(
OUT PNDIS_STATUS Status,
OUT PNDIS_HANDLE NdisProtocolHandle,
IN PNDIS_PROTOCOL_CHARACTERISTICS ProtocolCharacteristics,
IN UINT CharacteristicsLength
);
其中,ProtocolCharacteristics包含一组ProtocolXxx函数。由于NDIS Intermediate Driver的双重性,它需要调用 NdisIMRegisterLayeredMiniport 向NDIS注册,并向上层导出一组MiniportXxx函数,之后,调用NdisRegisterProtocol向NIDS注册,并向下层导出一组 ProtocolXxx函数。当一个NDIS中间层介入后,如图二所示。
3.2 NDIS数据发送流程:
当上层协议驱动要发数据时,调用NdisSend/NdisSendPackets请求NDIS发送数据包,NDIS将会调用紧接其下的中间层驱动的MiniportSend/MiniportSendPackets,中间层驱动MiniportSend/MiniportSendPackets 有机会在这里对包进行必要的操作,然后,中间层驱动再次调用NdisSend/NdisSendPackets请NDIS发送数据包,NDIS将调用其下层的Miniport Driver的MiniportSend/MiniportSendPackets,底层MiniportSend /MiniportSendPackets通过NDIS接口控制物理网络设备,将数据发送出去。在上层请求发送数据包时,上层分配了相关的资源(如内存),希望在下层完成发送动作后,能够及时的收回相关的资源,所以,当上层调用NdisSend/NdisSendPackets返回 NDIS_STATUS_PENDING以外的任何值时,上层就可以释放资源了,如果得到返回的结果是NDIS_STATUS_PENDING话,说明下层还没有完成发送请求,以后,等下层最终完成发送请求时,下层调用NdisMSendComplete请求NDIS通知上层可以释放资源了,于是NDIS 调用上层注册的ProtocolSendComplete函数,上层在这它的ProtocolSendComplete中释放了资源后,再次调用 NdisMSendComplete请求NDIS通知更上层。
3.3 NDIS数据接收流程:
当底层网络设备有数据到来的时候,将触发中断,相应的中断处理程序接管中断后,将可能调用Miniport Driver所注册的中断处理例程(ISR),Miniport Driver通常在这里把网卡上的数据考贝到Miniport Driver缓冲区队列中去,出于效率的考虑,Miniport Driver这时可能不会立即通知上层处理新的数据,因为很可能,马上还有随后的新的数据到来,当接收到的包的数量达到一定程度的时候,Miniport Driver会调用NdisMIndicateReceivePacket指示新的NDIS新数据的到来,这时候,NdisMIndicateReceivePacket的调用将导致NDIS调用位于Miniport上层的中间层向NDIS注册的下边沿 ProtocolReceivePacket函数。中间层驱动程序的ProtocolReceivePacket可以对收到的数据包进行相应的处理,之后,可以选择再次调用NdisMIndicateReceivePacket请求NDIS通知更上层数据包的到来,这时,NDIS调用更上层注册的 ProtocolReceive函数,上层的ProtocolReceive对数据包进行必要的处理后,继续调用 NdisMIndicateReceivePacket请求NDIS,通知更上层,最终数据包传到协议驱动中,由相关的协议栈进行处理。
有时候,在这种级连的上传过程中,并不是那么的顺利,例如,由于某种原因,如:网络设备的驱动程序的可用缓冲区数量减少到某个指定的数量时,网络设备的驱动程序调用NdisMxxxIndicateReceivePacket请求NDIS通知上层数据的到来,这时NDIS将调用上层注册的 ProtocolReceive,上层在ProtocolReceive中进行必要的处理后,进一步调用 NdisMxxxIndicateReceivePacket使得NDIS调用上层的协议驱动注册的ProtocolReceive。在一些不太理想的情况下,一次中断,从网络设备中接收到的数据对某个协议来说并不是一个完整的协议数据包。(一般情况下,其余的数据bit已经在链路途中了,并随后立即就会到达网络设备,这有可能就发生在一个CPU在处理网络设备中断的同时,网络设备的板载存储器上就已经收到了其余的数据了,甚至DMA控制器可能已经把这些数据传到了系统的主存储器上了),这时,上层的ProtocolReceive都无法进行正常的处理(一般对某个包进行处理,都要以相关的协议为依据,进行分析)。这时,在上传到某一层时,可以调用NdisTransferData请求NDIS把随后的信息传上来,这时,NDIS又将在向上传递的途中回过头来向下调用下面的MiniportTransferData,下层重复调用NdisTransferData把这个请求传送到底程的Miniport Driver。如果在上层调用NdisTransferData时不是返回NDIS_STATUS_PENDING,则上层可以继续它的处理,而如果返回 NDIS_STATUS_PENDING,底程在最终完成请求时,调用NdisMTransferDataComplete请求NDIS通知上层传送完成,这将导致上层注册的ProtocolTransferDataComplete被调用,上层调用NdisMTransferDataComplete 请求NDIS通知更上层。由于硬件技术的发展,网络设备板载存储的增加,系统主存储器的增加,以及网络传输能力的改善,NDIS那么迂回的通过 MdisMxxxIndicateReceivePacket,ProtocolReceive,MiniportTransferData这一条坎坷的路径进行数据处理的情况似乎越来越少见了。
接收过程是由下层传递上去的,同样,底层的Miniport分配了一些资源,如用于存储这个数据包内容的内存,我们希望这些资源最终能极时的被归还,以供以后使用。一个包在从下到上的传递过程时,如果某一层的ProtocolReceive/ProtocolReceivePacket有兴趣对这个包进行处理的话,则需要检查这个包的OOB信息段是不是携带NDIS_STATUS_RESOURCES,如果是的话,说明其下层资源紧缺,希望上层在处理的时候,自己考贝一份副本,以供自己使用,因为下层希望自己能够尽快收回这个包的资源,在这里,上层以后可以用自己的那份拷贝指示上层数据包的到来,这样的话,以后,中间层希望上层处理完后,能够收回所有权。另一方面,底层的Miniport并不是每一次都会在OOB信息段中设置 NDIS_STATUS_RESOURCES标志的,这在很多情况下是不必要的。当上层协议驱动完成处理时,可以调用NdisReturnPackets 通知NDIS,相应包已经处理完成,可以安全的释放相关资源了,于是NDIS将调用其下层注册的MiniportReturnPackets,下层在这里释放与这个包相关的资源,并继续调用NdisReturnPackets,请求NDIS把这个通知传给更下层。
这样一来,发数据由上层发起请求往下传,而接收数据从下层往上传,有没有可能一次接收的过层是由上面发起往下传的呢?这是没有必要的,为什么?我的应用程序不就主动调用WSARecv或WSARecvFrom等函数主动向下传递的一次收数据的请求的吗?事实上是这样的,你的接收请求经过 SPI,TDI,到了最终的协议驱动时,如果协议驱动中,指定的套接字上(最重要的是端口和目的IP了)有相应的数据的能满足你的这一次的读请求的话,就完成你的请求,如果不能的话,则阻塞了。当下层有数据来的时候,数据传递到协议驱动时,协议驱动会检查包头的信息并查看当前不是有应用程序打开过相应的端口,以及和对应的目标建立过连接,如果有的话,就把数据存到协议栈中的缓冲区中去,如果对应套接字上有阻塞的接收请求的话,就判断是不是能完成它的请求了,如果能了,就完成他,如果不能,继续等以后下层传上来再重复这个过程。否则就抛弃了,另外,如果,一个应用程序建立一个套接字,并与一台机器建立连接,对方发送了数据,协议栈会把数据保存起来,也许连接超时后,协议栈的缓冲区中还有应用程序没有接收的数据的话,也被抛弃了。所以,协议驱动从来都没有必要主动往下传递一个接收数据的请求。这一点,对于有一点网络程序调试经验的人来说,似乎可以直接找到一个的证据,在调试器的调用WSARecv前下一个断点,用Sniffer抓包,可能数据已经到来了,然后,再回到调试器中执行WSARecv,可以看到收到的就是先前Sniffer下来的数据了。
(注:用一个工作在NDIS协议层或是NDIS中间层的Sniffer来观查。)
3.4 NDIS 的数据包结构。
发送NDIS Protocol Driver分配相关的包资源,请求NDIS向下传递发送动作,接收时,NDIS Miniport Driver分配相关的包资源请求NDIS向上传递接收动作,中间层要对包进行处理,首先通过包描述符查询出这个包中的所有Buffer描述符,然后,从每一个Buffer描述符中取得相应的数据。一个包描述符中包含了一个或多个Buffer描述符,每一个Buffer描述符中包含了这个Buffer中数据的缓冲区首地址及其大小等信息,另外,一个包描述符中还包括了一些保留给开发者自己使用的Reserved字段,其于的一些字段,并没有被公开。另外 NDIS提供了一些宏和一些函数对这相应的描述符进行操作。为什么要把这个结构弄得这么复杂呢?这是由于面对分层的协议处理的时候,避免过多的数据考贝,比如对于TCP/IP协议来说,上层传递下来的数据,在经过TCP,IP层时,把TCP,IP协议头部那个Buffer分别加入这个Packet来就可以了,如果,要它们在一个Buffer中,则需要在经过TCP层时,TCP层建立一个新的Buffer,把头部放到这个Buffer中来,并把数据考贝到这个Buffer中来;到了IP层还要继续这一动作......
四 基于PassThru框架的中间层驱动程序的扩展实现
Microsoft在DDK中附带PassThru提供了一个的中间层驱动框架,使得开发者能够相对容易的在这个基础实现自己的NDIS中间层驱动扩展。我们将在PassThru的基础上实现一个基本的数据包操作的扩展。对于发送出去的数据包处理,只要在PassThru中的 MiniportSend和MiniportSendPackets中加入必要的操作代码,而对于接收的数据包时,则需要在 ProtocolReceive和ProtocolReceviePackets中加入必要的操作代码,在这里,我将以windows 2003 DDK的PassThru为例,进行扩展。
4.1 发送处理