资讯详情

NDIS小端口驱动开发实战:从Miniport注册到NBL收发路径详解

📅 2026/10/15 20:55:53 | 华诺云谱 👁 阅读
NDIS小端口驱动开发实战:从Miniport注册到NBL收发路径详解
简介这是一份面向Windows内核网络驱动开发者的NDIS 6.0小端口驱动实例源码针对Realtek 8111/8168/8169/8110等PCI千兆以太网控制器编写。由于DDK中可参考的miniport示例仅有E100BEX学习NDIS 6.0网卡驱动的资料相当稀缺本资源正好填补这一空白适合具备一定驱动基础、希望深入理解NDIS小端口驱动架构的中高级开发者。压缩包共69个文件约615KB以gif、png图片和js脚本、htm页面为主另含css样式、jpg图片及zip压缩包主要构成配套说明页面与演示素材便于对照阅读驱动实现思路。资源还涉及LSO、jumbo frame、电源管理等进阶特性的支持版本可帮助读者理解千兆网卡驱动的完整功能扩展路径。目前已有1240人学习下载适合作为NDIS 6.0 miniport驱动入门与进阶的参考实例。1. 网卡驱动为什么绕不开 NDIS从一块以太网卡说起很多人第一次写网卡驱动都会有个错觉以为只要把数据从硬件 FIFO 里搬进内存再交给协议栈就完事了。真动手才发现一块以太网卡要能被系统识别、能收发帧、能被抓包工具看到、能参与绑定与卸载中间隔着一整套框架。Windows 上这套框架就是 NDIS而以太网卡最典型的落点就是 NDIS 小端口驱动miniport driver。反直觉的地方在于小端口驱动并不是「驱动一块网卡」那么简单它其实是把自己注册进 NDIS 这个中间层让 NDIS 替你管理协议绑定、电源状态、即插即用、统计计数和收发路径。你写的代码只是 NDIS 的一个「被回调方」。搞懂这层关系才能理解为什么网卡驱动里到处是 MiniportXxx 回调、为什么初始化要分好几个阶段、为什么收包路径会牵扯到 NBL 和 NBL 链。这份资源围绕的正是这条主线从驱动入口到收发数据路径把 NDIS 小端口驱动的底层机制讲透。2. 驱动对象与 Miniport 注册为什么入口不是 DriverEntry 就完事2.1 DriverEntry 里到底注册了什么小端口驱动的入口仍然是DriverEntry但它做的事情和普通 WDM 驱动不一样。它不直接创建设备对象而是填充一个NDIS_MINIPORT_DRIVER_CHARACTERISTICS结构然后调用NdisMRegisterMiniportDriver把自己交给 NDIS。这个结构里塞满了回调函数指针NDIS 后续在合适的时机回调它们。NDIS_STATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { NDIS_MINIPORT_DRIVER_CHARACTERISTICS chars {0}; NDIS_HANDLE wrapperHandle; chars.Header.Type NDIS_OBJECT_TYPE_MINIPORT_DRIVER_CHARACTERISTICS; chars.Header.Size sizeof(NDIS_MINIPORT_DRIVER_CHARACTERISTICS); chars.Header.Revision NDIS_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_2; chars.MajorNdisVersion 6; chars.MinorNdisVersion 80; // 关键回调初始化、暂停、重启、收发、OID 查询 chars.InitializeHandlerEx MiniportInitializeEx; chars.HaltHandlerEx MiniportHaltEx; chars.PauseHandler MiniportPause; chars.RestartHandler MiniportRestart; chars.SendNetBufferListsHandler MiniportSendNetBufferLists; chars.ReturnNetBufferListsHandler MiniportReturnNetBufferLists; chars.OidRequestHandler MiniportOidRequest; return NdisMRegisterMiniportDriver(DriverObject, RegistryPath, NULL, chars, wrapperHandle); }这里几个参数值得说清楚。MajorNdisVersion和MinorNdisVersion决定了 NDIS 用哪套语义回调你6.80 对应的是 NBLNET_BUFFER_LIST收发模型而不是老的 NDIS_PACKET。InitializeHandlerEx带 Ex 后缀意味着它支持扩展的即插即用语义能拿到更完整的资源信息。SendNetBufferListsHandler一次能收到一串 NBL而不是单个包这是 NDIS 6.x 之后吞吐提升的关键设计。常见的实现思路是DriverEntry只做注册不做任何硬件访问。因为此时网卡可能还没上电甚至还没被枚举出来。真正的硬件初始化要等到MiniportInitializeEx被调用。2.2 为什么初始化要分阶段MiniportInitializeEx被调用时NDIS 已经完成了设备枚举把硬件资源内存映射、中断向量通过NDIS_RESOURCE_LIST传进来。驱动在这里要做的事包括读取配置、映射寄存器、申请 DMA 资源、注册中断、创建收发队列、最后调用NdisMSetMiniportAttributes声明自己的能力和 MAC 地址。NDIS_STATUS MiniportInitializeEx(NDIS_HANDLE MiniportAdapterHandle, NDIS_HANDLE MiniportDriverContext, PNDIS_MINIPORT_INIT_PARAMETERS InitParameters) { PMP_ADAPTER adapter allocateAdapter(); adapter-MiniportHandle MiniportAdapterHandle; // 1. 映射 BAR 空间拿到寄存器基址 status NdisMGetBusData(MiniportAdapterHandle, PCI_WHICHSPACE_MEMORY, 0, barInfo, sizeof(barInfo)); adapter-RegBase MmMapIoSpace(barInfo.BaseAddress, barInfo.Length, MmNonCached); // 2. 注册中断NDIS 会帮你处理中断共享和亲和性 status NdisMRegisterInterruptEx(MiniportAdapterHandle, adapter, interruptChars, adapter-InterruptHandle); // 3. 声明 MAC 类型、媒体状态、最大帧长 NDIS_MINIPORT_ADAPTER_GENERAL_ATTRIBUTES genAttrs {0}; genAttrs.MacType NDIS_MEDIUM_802_3; genAttrs.MaximumFrameSize 1514; genAttrs.CurrentLookaheadSize 1514; NdisMSetMiniportAttributes(MiniportAdapterHandle, genAttrs); return NDIS_STATUS_SUCCESS; }分阶段的原因在于NDIS 需要先知道你的能力才能决定怎么绑定协议、怎么分配缓冲。如果你在NdisMSetMiniportAttributes之前就试图收包NDIS 会直接拒绝。另一个细节是NdisMRegisterInterruptEx把中断处理也纳入了 NDIS 管理驱动不需要自己写 ISR 的框架代码只需要提供MiniportInterrupt和MiniportInterruptDpc两个回调。3. 收发数据路径NBL 链是怎么把帧送进协议栈的3.1 发送路径从协议栈到硬件发送路径的入口是MiniportSendNetBufferLists。协议栈把要发的数据封装成 NBL 链每个 NBL 上挂一个 NET_BUFFERNET_BUFFER 里才是真正的 MDL 数据。驱动要做的是遍历这条链把数据通过 DMA 映射到硬件可读的物理地址然后写发送描述符最后敲寄存器通知网卡。VOID MiniportSendNetBufferLists(NDIS_HANDLE MiniportAdapterHandle, PNET_BUFFER_LIST NetBufferLists, NDIS_PORT_NUMBER PortNumber, ULONG SendFlags) { PMP_ADAPTER adapter getAdapter(MiniportAdapterHandle); PNET_BUFFER_LIST nbl NetBufferLists; while (nbl ! NULL) { PNET_BUFFER nb NET_BUFFER_LIST_FIRST_NB(nbl); while (nb ! NULL) { // 把 MDL 映射成硬件可访问的地址 PHYSICAL_ADDRESS pa NdisGetPhysicalAddress(nb); writeTxDescriptor(adapter, pa, NET_BUFFER_DATA_LENGTH(nb)); nb NET_BUFFER_NEXT_NB(nb); } nbl NET_BUFFER_LIST_NEXT_NBL(nbl); } // 敲寄存器触发发送 WRITE_REGISTER_ULONG(adapter-RegBase TX_TRIGGER, 1); }这里有个容易翻车的点NBL 的所有权。驱动在发送完成后必须调用NdisMSendNetBufferListsComplete把 NBL 还给协议栈否则协议栈的发送队列会耗尽表现为网络卡死但硬件没报错。发送完成通常发生在中断 DPC 里驱动遍历已完成的描述符逐个归还对应的 NBL。3.2 接收路径中断、DPC 与 NBL 分配接收路径比发送复杂因为它涉及中断上下文和 DPC 的配合。网卡收到帧后触发中断MiniportInterrupt在中断上下文里只做最少的事屏蔽中断、记录状态然后返回 TRUE 让 NDIS 调度 DPC。真正的收包在MiniportInterruptDpc里完成。VOID MiniportInterruptDpc(PNDIS_HANDLE MiniportInterruptContext, PBOOLEAN QueueDpcWorkItem) { PMP_ADAPTER adapter (PMP_ADAPTER)MiniportInterruptContext; // 遍历接收描述符环找到已完成的帧 while (hasCompletedRxDescriptor(adapter)) { PNET_BUFFER_LIST nbl NdisAllocateNetBufferList(adapter-NblPool, 0, 0); PNET_BUFFER nb NET_BUFFER_LIST_FIRST_NB(nbl); // 把硬件缓冲里的数据拷进 NBL 的 MDL copyRxDataToNbl(adapter, nb); // 交给 NDISNDIS 再分发给绑定的协议 NdisMIndicateReceiveNetBufferLists(adapter-MiniportHandle, nbl, NDIS_DEFAULT_PORT_NUMBER, 1, NDIS_RECEIVE_FLAGS_RESOURCES); } // 重新使能中断 enableInterrupts(adapter); }NdisMIndicateReceiveNetBufferLists是收包的关键调用。它把 NBL 交给 NDISNDIS 根据绑定关系分发给上层协议。最后一个参数NDIS_RECEIVE_FLAGS_RESOURCES表示驱动在调用返回后立即回收 NBL适合驱动自己管理缓冲池的场景。如果不加这个标志NDIS 会在协议处理完后回调MiniportReturnNetBufferLists归还 NBL驱动需要在那里释放资源。3.3 OID 请求网卡配置的隐藏通道除了收发数据NDIS 还通过 OID 请求来查询和设置网卡参数。比如查询 MAC 地址、设置多播列表、调整 MTU、读取统计计数都走MiniportOidRequest。这个回调看起来不起眼但它是驱动和上层管理工具之间的唯一配置通道。NDIS_STATUS MiniportOidRequest(NDIS_HANDLE MiniportAdapterHandle, PNDIS_OID_REQUEST OidRequest) { PMP_ADAPTER adapter getAdapter(MiniportAdapterHandle); switch (OidRequest-Oid) { case OID_802_3_CURRENT_ADDRESS: // 返回当前 MAC 地址 moveMemory(OidRequest-DATA.QUERY_INFORMATION.InformationBuffer, adapter-CurrentMac, 6); OidRequest-DATA.QUERY_INFORMATION.BytesWritten 6; return NDIS_STATUS_SUCCESS; case OID_GEN_STATISTICS: // 返回收发统计 fillStatistics(adapter, OidRequest-DATA.QUERY_INFORMATION.InformationBuffer); return NDIS_STATUS_SUCCESS; default: return NDIS_STATUS_NOT_SUPPORTED; } }OID 请求可以是同步完成的也可以是异步的。如果驱动需要访问硬件才能拿到结果通常返回NDIS_STATUS_PENDING然后在 DPC 里调用NdisMOidRequestComplete完成。同步返回NDIS_STATUS_SUCCESS只适合那些不需要硬件交互的查询。4. 避坑与排查小端口驱动最容易翻车的几个点4.1 初始化失败但没有任何日志现象驱动加载后设备管理器显示黄色感叹号但系统日志里只有一句「设备启动失败」没有更具体的信息。原因MiniportInitializeEx返回了非NDIS_STATUS_SUCCESS但驱动没有在返回前记录具体是哪一步失败。NDIS 只会把最终状态码上报不会替你保留中间过程。解决在MiniportInitializeEx的每个关键步骤后加DEBUGP或DbgPrint把NdisMGetBusData、NdisMRegisterInterruptEx、NdisMSetMiniportAttributes的返回值都打出来。如果用了 WPP记得在MiniportInitializeEx入口和出口各加一个 trace 点。4.2 收包时蓝屏错误码指向 NBL 池现象驱动跑一段时间后蓝屏!analyze -v显示NdisAllocateNetBufferList返回的 NBL 被重复释放。原因在MiniportInterruptDpc里用了NDIS_RECEIVE_FLAGS_RESOURCES意味着 NBL 在NdisMIndicateReceiveNetBufferLists返回后就可以回收。但驱动可能在协议还没处理完时就释放了 NBL或者释放了两次。解决如果驱动自己管理 NBL 池确保每个 NBL 只释放一次。更稳妥的做法是不加NDIS_RECEIVE_FLAGS_RESOURCES让 NDIS 通过MiniportReturnNetBufferLists归还驱动在那里统一释放。这样虽然多一次回调但所有权清晰。4.3 发送队列卡死硬件寄存器显示空闲现象网络突然不通抓包看不到任何发出的帧但网卡寄存器显示发送队列为空。原因驱动在发送完成后没有调用NdisMSendNetBufferListsComplete协议栈的待发送 NBL 计数一直不归零最终耗尽。解决检查发送完成中断的处理逻辑。每次处理完一个发送描述符必须找到对应的 NBL 并调用NdisMSendNetBufferListsComplete。如果一次中断完成了多个描述符要把它们串成链一次性归还减少调用次数。4.4 OID 查询返回成功但数据是空的现象上层工具查询 MAC 地址返回成功但缓冲区全是 0。原因MiniportOidRequest里填了InformationBuffer但忘了设置BytesWritten或者BytesWritten设成了 0。解决每个查询类 OID 都必须设置BytesWritten为实际写入的字节数。对于OID_802_3_CURRENT_ADDRESS这个值是 6对于OID_GEN_STATISTICS是sizeof(NDIS_STATISTICS_INFO)。设置错误会导致 NDIS 认为没有数据返回。4.5 中断注册成功但收不到包现象NdisMRegisterInterruptEx返回成功但网卡收到帧后没有任何反应。原因中断注册成功只代表 NDIS 接受了你的中断对象不代表硬件的中断使能位已经打开。很多网卡需要驱动显式写寄存器打开中断掩码。解决在MiniportInitializeEx的最后写网卡的中断使能寄存器打开接收和发送中断。同时检查MiniportInterrupt的返回值如果返回 FALSENDIS 会认为这个中断不是你的不会调度 DPC。5. 从 NBL 到 DMA设计取舍背后的性能账小端口驱动的设计里最值得琢磨的是 NBL 和 DMA 的配合方式。NDIS 6.x 引入 NBL 链本质上是为了减少每次收发的函数调用次数。一个 NBL 可以挂多个 NET_BUFFER一个 NET_BUFFER 可以挂多个 MDL这样一次中断就能提交一批帧而不是一帧一次调用。这个设计在万兆网卡上尤其明显如果每帧都走一次NdisMIndicateReceiveNetBufferListsCPU 光在函数调用和锁竞争上就耗光了。但批量提交也带来了新的取舍。驱动需要自己管理 NBL 池的大小池太小会在高负载时分配失败池太大又浪费非分页内存。常见的做法是按队列深度动态调整初始分配 64 个 NBL如果连续多次分配失败就翻倍扩容但设一个上限比如 1024。这个策略在多数场景下够用但在突发流量下仍可能丢包因为扩容本身需要时间。另一个取舍是 DMA 映射的时机。发送路径上驱动可以在MiniportSendNetBufferLists里立即映射 MDL也可以延迟到写描述符时再映射。立即映射的优点是逻辑简单缺点是如果硬件队列满映射了却发不出去还得回滚。延迟映射的优点是只在真正发送时才占用 DMA 资源缺点是代码复杂度高需要处理映射失败的情况。我一般会这么理解如果网卡支持分散聚集scatter-gather延迟映射更划算如果不支持立即映射反而少一次拷贝。验证这些取舍是否合理最直接的方法是看统计计数。NDIS 提供了OID_GEN_STATISTICS驱动可以在这里暴露ifOutDiscards、ifInDiscards、ifOutErrors等计数。如果ifOutDiscards持续增长说明发送队列深度不够或者 NBL 池太小如果ifInDiscards增长说明接收路径处理不过来可能需要增大 DPC 的批量处理数量。这些计数不是摆设它们是调优的唯一依据。从那以后我每次写小端口驱动都会先把统计计数接好再跑一遍压力测试看丢包到底发生在哪一层。这个习惯帮我省掉了大量靠猜来定位问题的时间。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑