ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

NDIS小端口驱动开发指南:从架构认知到数据收发与调试实战

NDIS小端口驱动开发指南:从架构认知到数据收发与调试实战 简介面向希望编写NDIS 6.0小端口设备驱动的开发人员这是一份基于千兆以太网控制器的驱动实例源码。针对Realtek 8111/8168/8169/8110等PCI千兆网卡演示了从驱动入口、硬件初始化、报文收发到中断处理与电源管理的完整实现思路弥补DDK自带E100BEX示例之外的参考缺口。包内共69个文件以htm网页存档、gif/png示意图、js脚本及三个zip源码包为主整体约615KB网页部分便于查阅原版项目说明源码包则分别对应LSO/巨帧支持、电源管理支持及release版本等实现。目前已有1238人学习下载。读者可对照源码梳理小端口初始化、收发数据路径、中断处理、OID查询等核心环节借助不同特性的分支对比理解如何为基础驱动添加任务卸载与电源管理能力适合作为NDIS 6.0驱动开发的入门参考和二次开发起点。 很久以前我就想写一篇关于 NDIS 小端口驱动miniport driver的文章了因为它在 Windows 网络驱动里面是门槛最高、最劝退的一块但它又是一个以太网卡驱动绕不开的核心部分。我记得自己第一次接手这个方向拿到一份开发手册里面全是MiniportInitializeEx、MiniportSendNetBufferLists、NdisMIndicateReceiveNetBufferLists这种回调函数看名字就头晕。真正写起来之后才发现小端口驱动跟普通 PCIe 设备驱动完全是两码事它的难点不在于“怎么访问硬件寄存器”而在于每一个动作都必须踩在 NDIS 定义好的节拍上踩歪一点轻则丢包重则直接蓝屏。这篇文章我就把自己踩过的坑和总结出来的思路按实际开发顺序展开从架构认知讲到数据收发再讲调试实战希望能帮你少走几个星期的弯路。1. 先把工作边界划清楚NDIS 小端口驱动在系统里的真实位置1.1 不是你在直接管理网卡而是 NDIS 管你很多刚接触的人以为写网卡小端口驱动就是在做一个“读写寄存器库”只要把网卡的收发队列操作好就行了。这个理解错得离谱。真实情况是Windows 的 TCP/IP 协议栈并不直接跟你的驱动打交道它只跟ndis.sys打交道而你的小端口驱动本质上是在给ndis.sys打工。整条链路的层次关系大概是下面这样角色代表模块职责协议驱动TCPIP.sys处理 TCP/UDP/IP 逻辑向上给 Socket 层提供接口NDIS 库ndis.sys规范统一接口管理协议驱动与小端口驱动之间的绑定、数据分发、PnP 和电源状态小端口驱动你的 miniport 驱动直接面对网卡硬件完成数据包在“系统内存”和“网卡硬件队列”之间的搬运物理设备以太网卡真正把电信号发到网线上去简单打个比方协议驱动像一个发货方它只负责把货交到快递站点NDIS手里至于站点叫哪辆车运、走的哪条线路发货方完全不关心。小端口驱动就是那个负责把货搬到卡车上的装卸工它不需要关心货物里面装的是什么但必须保证“站点说搬哪批货你就能搬哪批货”。这个定位搞清楚了后面很多设计选择就都顺理成章了为什么你的驱动里要走各种回调因为 NDIS 只在特定时机叫你。为什么你不能自己随便开线程去操作硬件因为 NDIS 对收发时序、中断处理有严格的使用规定。1.2 NDIS 6.x 之后的接口模型回调驱动的生命周期Windows 从 Vista 开始全面使用 NDIS 6.0到今天 Windows 11 上的 NDIS 6.89/6.90小端口驱动的整体形态没有发生革命性变化。你要实现的核心回调按“生命周期”来记特别清晰DriverEntry里通过NdisMRegisterMiniportDriver注册驱动对象告诉 NDIS“我能干哪些活”设备被系统 PnP 枚举到后NDIS 调MiniportInitializeEx驱动在这里做整套初始化网卡工作期间MiniportSendNetBufferLists负责发数据MiniportReturnNetBufferLists负责还接收缓冲区MiniportOidRequest负责处理配置和查询系统要暂停网卡时调MiniportPause恢复时调MiniportRestart这两是 NDIS 6.0 之后强制要求实现的设备移除或系统关机时走MiniportShutdownEx和MiniportHalt。我建议新人在动手写代码之前先把上面这一串回调按时间顺序画一条线标出每个回调的 IRQL 要求、能做什么不能做什么。因为 NDIS 判断驱动是否规范很大程度上就是看你有没有在错误的时机、错误的优先级下调用了错误的函数。比如在MiniportInitializeEx里注册中断之前硬件资源必须全部就绪否则中断一进来就是访问空指针这在驱动早期开发里是最常见的蓝屏原因之一。2. 初始化链路拆解从 DriverEntry 到网卡报告“已就绪”2.1 DriverEntry 里那点“小事”搞错直接不加载小端口驱动的DriverEntry和普通 WDM 驱动差异很大。你基本上不需要处理什么创建设备的流程NDIS 会在背后替你做掉大量 PnP 工作。你需要做的核心动作是NDIS_MINIPORT_DRIVER_CHARACTERISTICS Chars; NDIS_STRING FriendlyName NDIS_STRING_CONST(My Ethernet Miniport); NDIS_STATUS Status; NdisZeroMemory(Chars, sizeof(Chars)); Chars.Header.Type NDIS_OBJECT_TYPE_MINIPORT_DRIVER; Chars.Header.Size NDIS_SIZEOF_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_2; Chars.Header.Revision NDIS_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_2; Chars.MiniportInitializeEx MyInitialize; Chars.MiniportHaltEx MyHalt; Chars.MiniportPause MyPause; Chars.MiniportRestart MyRestart; Chars.MiniportOidRequest MyOidRequest; Chars.MiniportSendNetBufferLists MySendNbl; Chars.MiniportReturnNetBufferLists MyReturnNbl; Status NdisMRegisterMiniportDriver( DriverObject, FriendlyName, Chars, MiniportDriverContext );这里有个真实的坑NDIS_MINIPORT_DRIVER_CHARACTERISTICS结构里的Header.Size和Header.Revision必须严格匹配你定义特性结构时所对应的版本。很多人图省事Size填小了一截或者Revision用错了宏结果驱动在NdisMRegisterMiniportDriver这边直接被拒绝报出NDIS_STATUS_BAD_VERSION或NDIS_STATUS_INVALID_PARAMETER。排查起来又非常不明显因为注册失败之前没有任何提示日志。这类问题最好在代码里直接用宏定义不要手写数字例如NDIS_SIZEOF_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_2。另外小端口驱动必须有配套的 INF 文件才能被系统正确识别并绑定网络类。INF 里通常Class NetNetCfgInstanceId这样的字段也要填对。新人在真机上装驱动经常遇到“驱动装上了但网卡始终是黄叹号”一半的原因不是代码坏了而是 INF 里指定的硬件 ID 跟设备实例不一致或者Characteristics字段没写NCF_PHYSICAL导致系统不认为它是一个物理网卡。2.2 MiniportInitializeEx整个驱动最重要的一场演出MiniportInitializeEx是网卡从“被系统发现”到“可以被协议栈使用”的中间之路我给它起外号叫“一场演出”因为它环节多、顺序严格任何一个环节出错NDIS 就认为初始化失败。它的原型长这样NDIS_STATUS MiniportInitializeEx( NDIS_HANDLE MiniportAdapterHandle, NDIS_HANDLE MiniportDriverContext, PNDIS_MINIPORT_INIT_PARAMETERS MiniportInitParameters );我在实际项目里一般把这个函数拆成五步分配并初始化适配器上下文Adapter Context。这个上下文是你自己定义的结构体用来保存 BAR 地址、互斥锁、环形缓冲区指针、统计计数。但凡跟这块网卡实例相关的状态都往里面塞后面所有回调函数拿到的第一个参数基本就是它。调用NdisMSetMiniportAttributes设置网卡能力属性。这一步必须在你做任何硬件操作之前还是之后我的经验是可以先做因为它只是把通用属性注册给 NDIS关键在于NDIS_MINIPORT_ADAPTER_GENERAL_ATTRIBUTES里的MediaType、PhysicalMediumType、MacAddress、IfType这些字段不能填错填错了上层的 TCP/IP 绑定行为会变得非常诡异。比如你把MediaType填成了NdisMediumWirelessLan系统可能根本不会把它当成一块普通以太网卡来配置 IP。映射硬件资源。PCIe 网卡一般通过NdisMMapIoSpace映射 BAR 空间。映射完第一件事是读 PCIe 配置空间或供电后的寄存器确认硬件真的在线上顺便获取或者恢复 MAC 地址。MAC 地址如果为零或全0xFFNDIS 在上层绑定时会拒绝因为这样的设备无法被协议栈识别。初始化 DMA。如果网卡支持总线主控 DMA几乎所有以太网卡都支持需要用NdisMInitializeScatterGatherDma初始化 DMA 描述符分配器。这一步的意义是让 NDIS 知道你的驱动要走散列/聚集scatter/gatherDMA后面你从系统拿到的大包可能横跨多个物理不连续的内存页你得通过 NDIS 提供的 SG 列表去构造硬件 DMA 描述符。注册中断。建议放在最后等前面 1-4 步都成功以后再用NdisMRegisterInterruptEx注册。如果中断在驱动完全准备好之前就触发而你的 ISR 里访问了尚未初始化的队列指针蓝屏瞬间就会发生。初始化完成后别忘了调用NdisMIndicateStatus通知 NDIS 链接状态比如NDIS_STATUS_LINK_STATE和当前速率。这一步漏掉同样很致命因为上层协议栈会一直以为网线没插。3. 数据通路详解Send 与 Receive 这两个“搬运工”3.1 发送路径不愿背锅的 NET_BUFFER_LIST发送回调用一句话概括就是NDIS 把一批数据包丢给你你负责把它们塞进网卡硬件队列事后告诉 NDIS“这些包我处理完了”。函数原型VOID MiniportSendNetBufferLists( NDIS_HANDLE MiniportAdapterContext, PNET_BUFFER_LIST NetBufferLists, NDIS_PORT_NUMBER PortNumber, ULONG SendFlags );关键点在于NetBufferLists是一个链表不是单个包。也就是说 NDIS 可能一次给你几十个包你要批量送进硬件 DMA 描述符这也是 NDIS 追求高性能的体现。每个NET_BUFFER_LISTNBL里挂着若干个NET_BUFFERNB每个 NB 里又有一串 MDL。MDL 描述的是这些数据实际在内存里占哪些物理页。新手最容易懵的就是这个三层层级我建议你在调试器里把!ndiskd.nbl配合!ndiskd.netbuffer用熟你会看到NET_BUFFER_LIST: 地址 NET_BUFFER: 地址 MDL 链: start/mappedlen真正发送时你遍历 NB 的 MDL通过MmGetMdlVirtualAddress之类的接口把虚地址转成物理地址组装成网卡的发送描述符再写 doorbell 通知网卡 DMA 拉数据。硬件发完之后会产生发送完成中断你在 DPC 里回收这批描述符最后调用NdisMSendNetBufferListsComplete把 NBL 还给 NDIS。这一步千万不能漏漏了就是 NBL 泄漏驱动跑一段时间系统网络就彻底卡住。如果网卡硬件暂时收不下这么多包怎么办NDIS 6.0 之后的规则是你不能在 Send 里无限阻塞等待队列空出来也不能直接“退回去不接受”。合理的策略是自己维护一个环形队列在驱动内部把 NBL 排起来然后立刻返回等硬件完成后中断再补发或放弃。如果你不排队就一股脑把超出硬件能力的包全写进 FIFO要么 FIFO 溢出丢包要么因非法操作直接蓝屏。3.2 接收路径提前备好缓冲区别做无谓的内存分配接收方向是反过来的网卡 DMA 把线路上收到的数据写进你提前准备好的接收缓冲区然后产生中断驱动在 DPC 里把缓冲区封装成 NBL 交给上层之后上层用完了再通过MiniportReturnNetBufferLists回调还给你。接收路径的性能好坏很大程度上取决于你预分配缓冲区的策略。大厂驱动基本都会在初始化阶段就分配好一个接收环形队列比如 512 个 DMA 缓冲区每个缓冲区 2KB 左右把这些缓冲区的物理地址提前填到网卡的接收描述符表里。收包时把某个缓冲区从“空闲队列”摘下来装进 NBL 指示上去同时立刻从系统申请一个新的空闲缓冲区补充到队列里。这个过程中最忌讳的就是每收一个包就现场NdisAllocateMemoryWithTag申请一次内存因为频率一高内存分配器和锁竞争会直接拖垮吞吐量。另外接收指示的时机也很有讲究。我见过不少驱动在第一个包到达时立刻调用NdisMIndicateReceiveNetBufferLists这样单包小流量还好但在高 PPS 场景下每次中断和协议栈入口的开销会被无限放大。更合理的做法是自己在 DPC 里攒一批包比如攒满 32 个或用一个ndis_packet_batch的概念批量向上指示这样一次协议调用处理多个包CPU 占用率明显下降。不过这个“攒包”时间也不能太长否则延迟暴涨游戏或视频场景立刻能感知到卡顿。这里的取舍要根据产品定位来定驱动里多留几个可调参数总没错。4. 中断、暂停、OID稳定性的三根柱子少一根都会出事4.1 中断处理ISR 里只做最小动作其他一律扔给 DPC以太网卡属于高频率中断设备如果 ISR 里干太多活系统 DPC 延迟和中断嵌套问题会疼到你怀疑人生。NDIS 的设计原则很简单ISR 里只做“判断这个中断是不是我的”然后清中断并对中断里需要及时处理的寄存器进行最小操作剩下的实际收发工作全部放到MiniportInterruptDPC里面完成。这两者的职责对比我整理如下工作项ISRDPC读取中断状态寄存器是否判断是否为当前网卡的中断是否清除中断标志/写中断应答是否搬运 DMA 数据、组装 NBL否是调用接收指示/发送完成接口否是访问可以睡眠的锁和内存分配否否注意最后一行就是很多驱动崩掉的根源中断 DPC 里绝对不能调用可睡眠的锁也不能分配容易触发调页的内存。你用NdisAcquireSpinLock这类自旋锁可以但持有时间必须极短。如果不小心在持有自旋锁的情况下去调用 NDIS 的某些可能会阻塞的函数死锁或 watchdog 蓝屏就会立刻找上门。另外现代网卡大多支持 MSI-X 多队列每个队列可以单独绑到不同 CPU 核心配合 RSSReceive Side Scaling把收包负载分散到多核。如果你要支持 RSS注册中断时要按队列数量注册多个中断向量同时要用NdisMGetRssProcessorInformation之类的接口去了解系统当前给网卡分配了哪些 CPU 核心。这一步不复杂但很琐碎特别是处理 CPU 热插拔或系统处理器组变化时容易漏。4.2 为什么 NDIS 6 之后必须实现 Pause系统真的会突然让你“闭嘴”在 NDIS 5.x 时代驱动可以不实现暂停逻辑网卡可以一直工作到关机。但 NDIS 6.0 以后MiniportPause和MiniportRestart成了强制要求。这是因为系统在协议栈重置、电源状态切换、虚拟化热迁移、绑定关系重建等场景下需要让网卡安静片刻。如果你的驱动没有在MiniportPause里把 DMA、发送队列、接收指示全部拦截下来系统可能在资源重置过程中跟驱动抢同一块内存或队列最终导致蓝屏。实现MiniportPause的难点在于它可能处于比较高的 IRQL你的实现里不能用超长等待也不能调用任何可能阻塞的函数。我自己的做法是进入MiniportPause后先设置一个AdapterPaused标志位让发送回调在入口处直接把 NBL 返回或排队然后屏蔽中断等待已经在执行中的 DPC 快速退出最后把环形队列的尾指针同步一次确保没有包还悬在硬件里。这个过程一定要设计得“非抢占式优雅”不要硬等否则驱动会超时。4.3 OID 请求别人眼里的“网卡健康状态”都由这里出MiniportOidRequest是系统查询和配置网卡时都会走到的回调常见请求像 OID_GEN_STATISTICS、OID_802_3_PERMANENT_ADDRESS、OID_GEN_LINK_SPEED 等。统计类 OID 特别考验功力因为上层网络管理工具比如性能监视器、Get-NetAdapterStatistics显示的发送/接收计数、丢包数、CRC 错误数全部来自你驱动返回的统计结构。如果这些数字跟真实硬件状态对不上运维和网管会拿着这些数据来找你麻烦。所以我在开发时会在适配器上下文里维护一组用InterlockedExchangeAdd累加的计数器包括发送成功包数、发送丢弃包数、接收成功包数、接收丢弃包数、CRC 错误数、资源不可用丢包数。然后在 OID 请求返回时把这组分装成 NDIS 要求的统计结构。这个习惯虽然简单但能让你在后续调优时省去大量“数据对不上”的扯皮。5. 实战复盘一次“驱动加载正常、ping 不通”的完整排查链路最后分享一个我印象很深的排查案例。现象是驱动在虚拟机和真机上都成功加载设备管理器里没有黄叹号网卡链路状态显示已连接DHCP 也拿到了 IP但 ping 网关就是没有响应。这种问题最折磨人因为从“设备层面”看一切正常问题出在数据通路上。我的排查顺序是先把问题缩小。在目标机上用ping -S 本机IP 网关IP确认走的是这块网卡而不是走 loopback 之类的虚拟网卡。然后抓包可以用 Wireshark 也可以在内核里看确认 ARP 请求有没有发到驱动层。如果 ARP 都没出去问题在发送路径如果 ARP 发出去了但没收到回应可能问题在接收路径也可能是对端没回。用 windbg 接上内核调试。打开目标机的内核调试后执行!ndiskd.miniport这会列出系统里所有小端口驱动实例。我的经验是先看字段里的State如果显示Paused说明网卡虽然“存在”但没被协议栈正常启用。接下来用!ndiskd.nbl NBL地址查看发送队列里的 NBL 停在哪个环节。如果发送 NBL 被驱动接收但一直没调用NdisMSendNetBufferListsComplete基本能断定你的驱动在发送路径上有逻辑没走完。在这个案例里我发现 ARP 请求已经进入了发送回调驱动也把数据写进了硬件发送描述符但网卡并没有真正把数据发出去。最后定位到问题在 DMA 描述符的地址计算驱动在构造散列/聚集描述符时把某个 MDL 的物理地址拆成高32位和低32位时高32位没有正确填入描述符结构导致 64 位地址的高位为零硬件 DMA 读到了完全错误的内存位置数据当然发不出去。这类“小包 OK、大包失败”或“发着发着就断了”的现象往往都和 DMA 地址计算、描述符对齐、缓存一致性相关。修复之后我还总结了一个预防技巧在驱动的测试版本里加一个简单的自检逻辑初始化后往硬件的环回loopback模式发一个已知数据包然后从接收路径收回来比对内容。如果环回测试通过再让系统绑定 IP。这个不起眼的操作能拦截掉一大类底层 DMA 配置错误不用每次都等上层网络故障才暴露。如果你也是刚起步做网卡驱动开发我强烈建议先在虚拟机或支持虚拟网卡的模拟环境里跑通一个最小原型再往真机上搬。虽然真机能暴露更多时序问题但反复蓝屏和重启会让你排查效率飞快下降。再就是给测试系统开测试签名模式省去每次都要签名的烦恼等 MVP 完全稳定之后再做 WHQL 签名流程。写在最后回想那次“ping 不通”的排查我在调试器里蹲了整整一下午最后发现只是 DMA 描述符高 32 位没填对。从此之后我给自己定了几条规矩任何跟物理地址相关的字段先用工具打印出来逐位核对收到异常丢包先怀疑 DMA 描述符对齐和地址拆分而不是第一时间怀疑协议栈每加一个功能都要保持“硬件环回自检”能随时跑通。NDIS 小端口驱动确实门槛高、抽象多但只要你把架构、回调时序、数据通路和调试工具这条链摸熟写起来也会越来越有章法祝各位早日点亮网卡驱动这颗技能树。本文还有配套的精品资源点击获取
返回列表