
简介NDIS 6.0小端口驱动开发长期缺乏可参考的完整实例DDK自带示例仅E100BEX一个这套针对Realtek 8111/8168/8169/8110等PCI千兆以太网卡编写的miniport驱动源码覆盖了驱动框架、硬件访问与数据路径等核心部分为需要编写NDIS 6.0驱动的开发者提供了难得范本。压缩包共69个文件除主要源码外还附带PM电源管理、LSO巨帧支持等3个zip附加模块以及用于辅助说明的htm、js、css与图片文件整体体积仅615KB小巧紧凑便于快速下载和按需查阅。目前已有1240人学习其价值受到不少开发者认可。资源从适配器初始化、发送接收队列管理到中断处理均有完整实现并可结合附加模块对比不同功能特性的集成方式对于正在开发或维护NDIS 6.0小端口驱动的工程技术人员这套代码既能帮助理解底层运转机制也能显著缩短底层驱动开发中的调研和排错时间。1. 把 NDIS 小端口驱动拆开从一个看不见的以太网卡驱动说起网卡驱动在 Windows 里不是随便写个 WDM 或 KMDF 驱动就完事它必须经过 NDISNetwork Driver Interface Specification这个中间层来和协议栈通信。刚接触这块的人最容易懵的是明明照着示例代码写完了设备也枚举出来了但网络连接死活起不来网上邻居里那个“未识别的网络”消不掉。这事我当初调试了快一周最后才发现是 OID 查询的返回状态没处理好NDIS 直接判定 miniport 初始化失败。NDIS 小端口驱动Miniport Driver本质上是一个“被 NDIS 管理的适配器驱动”。它自己不直接操作 TCP/IP 协议栈而是通过一组 NDIS 定义的回调函数把硬件的收发能力、链路状态、电源管理等细节暴露给系统。你需要关心的核心问题只有一个如何用一套标准的回调模型去驱动一块具体的以太网硬件。这个资源适合谁说清——不是在写 PCIe 网卡固件的嵌入式工程师也不是只调应用层 Socket 的开发而是那些需要让一块自定义网卡或虚拟网卡在 Windows 设备管理器里以“网络适配器”身份工作的人。2. NDIS 的层次与核心理念为什么必须走 miniport 这一层2.1 NDIS 驱动栈不是所有驱动都能叫 miniportWindows 网络驱动从下往上分布着物理网卡、小端口驱动、NDIS 库、协议驱动TCP/IP 栈以及上面的各种过滤驱动如 WFP、LWF。NDIS 库本身不处理任何数据包它只是个调度中心负责把上层的发送请求转给小端口把小端口收到、完成的接收包再递交回上层。这里最常见的误用是有人以为自己可以写一个直接接管网卡硬件、再自己实现一套类似 Socket 的接口的驱动。这么说吧那你会失去 TCP/IP 协议栈、DHCP、无线上网、防火墙过滤等所有系统自带能力。走 NDIS miniport 的好处在于协议栈和驱动之间是解耦的你做的是“硬件适配”不用重写网络语义。NDIS 库帮你管理了大部分琐碎的同步与并发问题比如数据路径上的锁、预取、队列。调用了NdisMRegisterMiniportDriver之后PNP 管理器、电源管理器会通过 NDIS 间接引导你的驱动配套的 inf 也有一套成熟的写法。打个不严谨的比喻NDIS 是江湖中间人miniport 是你在中间人面前演的“硬件代理人”。你不需要知道 TCP 粘包拆包但你必须知道如何把你的网卡 DMA 描述符里的数据搬到NET_BUFFER_LIST里。2.2 你逃不掉的两条主线控制路径与数据路径在开发 miniport 时我习惯把代码分成两条线来想。控制路径也叫慢路径是 PNP 事件、OID 查询/设置、暂停/重启、电源状态迁移等操作。它们的特点是频率低、但讲究正确性和状态同步。比如MiniportInitializeEx里要完成适配器初始化、寄存器映射、中断注册、广播初始链路状态MiniportQueryOid和MiniportSetOid处理上层比如IP Helper、DHCP 客户端发来的各种属性请求。数据路径是收发数据的快路径。这里只关心怎么高效地把包从硬件 DMA 引擎里取出来以及把上层的NET_BUFFER_LIST链描述成硬件能理解的 DMA 描述符数组。数据路径是不能随便调函数、不能做复杂计算的因为每一微秒都可能影响吞吐。理解这两条路径的区分是读懂任何一份 NDIS 示例代码的钥匙。如果你发现自己在MiniportSendNetBufferLists里做同步等待或者分配大量内存那说明设计已经歪了。2.3 选择全功能 miniport 还是仅控制路径的 miniportNDIS 支持两种 miniport无线的、有线的全功能驱动以及所谓的“仅控制路径”的驱动比如一些虚拟交换机扩展。对以太网卡来说你需要的是完整的数据路径支持即MiniportSendNetBufferLists并支持分片MiniportReturnNetBufferListsMiniportReceiveNetBufferLists通过NdisMIndicateReceiveNetBufferLists上报中断处理函数MiniportInterrupt和MiniportInterruptDPC如果你的硬件本身不是太复杂很多厂商会选择把全部路径写在一个驱动里但并不代表结构可以简化。你依然要把初始化、暂停、重启、OID 处理等钩子全部实现否则 NDIS 库在遇到特定状态时会直接给你返回驱动不支持——没有商量的余地。3. 搭起一个能加载的 miniportDriverEntry 与注册特征结构3.1 DriverEntry 的真实使命NDIS miniport 的 DriverEntry 比普通 WDM 驱动多一个关键动作调用NdisMRegisterMiniportDriver注册一组特征函数。它和 WDF 的WdfDriverCreate不是一个套路——你不需要在这里创建设备对象因为设备对象的创建完全由 NDIS 在 PNP 事件到来时替你完成。一个典型的 DriverEntry 骨架如下NDIS_STATUS DriverEntry( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath ) { NDIS_MINIPORT_DRIVER_CHARACTERISTICS Chars; NDIS_STATUS Status; // NDIS 要求使用该宏安全填充版本信息 NdisInitMiniportDriverCharacteristics( Chars, NDIS_OBJECT_TYPE_MINIPORT_DRIVER_CHARACTERISTICS, NDIS_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_2, 0); Chars.AdapterInitializationHandler MiniportInitializeEx; Chars.AdapterHaltHandler MiniportHaltEx; Chars.AdapterPauseHandler MiniportPauseEx; Chars.AdapterRestartHandler MiniportRestartEx; Chars.QueryOidHandler MiniportQueryOid; Chars.SetOidHandler MiniportSetOid; Chars.SendNetBufferListsHandler MiniportSendNetBufferLists; Chars.ReturnNetBufferListsHandler MiniportReturnNetBufferLists; Chars.CancelSendHandler MiniportCancelSend; Chars.InterruptHandler MiniportInterrupt; Chars.InterruptDpcHandler MiniportInterruptDPC; Status NdisMRegisterMiniportDriver( DriverObject, RegistryPath, Chars, NDIS_SIZEOF_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_2, AdapterDriverHandle); return Status; }注意Chars内部的对象头Header字段在第一处调用里已经用宏填好了所以再单独去改Header.Type/Size/Revision是多余的。很多新手是从 WDK 老示例ndislwf或e1000抄代码能看到那里多了几个InitializePutHandle之类的调用但如果你用的是 Windows 10 的 WDK直接按上面的骨架来即可。参数上NdisMRegisterMiniportDriver最后的AdapterDriverHandle是一个NDIS_HANDLE后续几乎所有全局性调用申请内存、注册中断、读寄存器都要以它作为NdisWrapperHandle传入。丢了这个句柄你在其它函数里就寸步难行。3.2 注册特征结构时最容易犯的版本错误NDIS_MINIPORT_DRIVER_CHARACTERISTICS这个结构有多个版本不同版本允许注册的回调函数数量不同。WDK 里REVISION_1到REVISION_2的主要区别在于Revision_2支持更新的回调集比如DirectOidRequest和SynchronousOidRequest后者用于异步 OID 请求处理。但注意这个结构的Size字段必须使用NDIS_SIZEOF_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_2这个宏不能自己算。如果你用的是旧点的 WDK硬编码sizeof(NDIS_MINIPORT_DRIVER_CHARACTERISTICS)通常也能编译过但它可能与 NDIS 库期望的结构大小不一致——NDIS 在运行时校验一旦失败它会拒绝加载并且Event Log里给你一个非常不明确的The driver is invalid错误。3.3 初始化适配器MiniportInitializeEx 里必须做到的六件事当一个网卡设备被 PNP 枚举出来后NDIS 会调用你注册的MiniportInitializeEx。这个函数失败设备将无法启用Windows 网络服务直接不可用。我在做虚拟网卡时总结的固定流程NDIS_STATUS MiniportInitializeEx( _In_ NDIS_HANDLE NdisAdapterHandle, _In_ NDIS_HANDLE MiniportDriverHandle, _In_ PNDIS_MINIPORT_INIT_PARAMETERS InitParams) { PADAPTER Adapter NULL; // 1. 分配适配器结构该结构的第一个字段必须是 NDIS_OBJECT_HEADER Adapter NdisAllocateMemoryWithTagPriority( NdisAdapterHandle, sizeof(ADAPTER), paNx, LowMemoryPriority); RtlZeroMemory(Adapter, sizeof(ADAPTER)); Adapter-Header.Type NDIS_OBJECT_TYPE_ADAPTER; Adapter-Header.Revision NDIS_ADAPTER_REVISION_1; Adapter-Header.Size NDIS_SIZEOF_ADAPTER_REVISION_1; NdisMSetMiniportAttributes( NdisAdapterHandle, Adapter-Header); // 2. 读取注册表中的参数中断亲和性、环形容量等 // 3. 映射 BAR 寄存器获取物理地址 // 4. 分配 DMA通过 NdisMRegisterDmaChannel 或直接使用硬件固有的片段 // 5. 注册中断NdisMRegisterInterruptEx // 6. 初始化本地状态后设置“媒体连接”的初始状态再返回 NDIS_STATUS_SUCCESS Adapter-MediaState MediaConnectStateConnected; *((PNDIS_STATUS)None) NDIS_STATUS_SUCCESS; // 伪代码示意实际是通过 NdisM... 调用上报 return NDIS_STATUS_SUCCESS; }在初始化里最隐蔽的一个坑是你需要调用NdisMSetMiniportAttributes并传入一个NDIS_MINIPORT_ADAPTER_ATTRIBUTES结构里面至少要有MediaTypeNdisMedium802_3是绝大多数以太网适配器的选择和PhysicalMediumType。如果你漏设MediaType为NdisMedium802_3系统可能把该设备识别成某种未知介质类型的网络设备DHCP 从上层就把你“拒绝服务”了。另一个坑是初始化期间不能直接向协议栈发送数据包也不能主动调用NdisMIndicateReceiveNetBufferLists——那必须等MiniportRestartEx之后。我看到过有人试图在初始化里探测对端链接状态导致 NDIS 库抛异常。初始化阶段就应该把精力放在“让设备可被操纵”上而不是抢跑数据路径。4. 把包发出去、把包收回来数据路径的关键实现4.1 发送路径从 NET_BUFFER_LIST 到 DMA 描述符数据发送是验证一个 miniport 是否靠谱的第一道考试。当 TCP/IP 栈要发包时NDIS 会调用你的MiniportSendNetBufferLists。你要做的是遍历传入的NET_BUFFER_LIST链再遍历每个NET_BUFFER的NET_BUFFER_LIST上挂着的各段内存。VOID MiniportSendNetBufferLists( _In_ NDIS_HANDLE MiniportAdapterContext, _In_ PNET_BUFFER_LIST NetBufferLists, _In_ ULONG PortNumber, _In_ ULONG SendFlags) { PADAPTER Adapter (PADAPTER)MiniportAdapterContext; PNET_BUFFER_LIST CurrentNbl; BOOLEAN fSendComplete TRUE; CurrentNbl NetBufferLists; while (CurrentNbl ! NULL) { PNET_BUFFER CurrentNb NET_BUFFER_LIST_FIRST_NB(CurrentNbl); // 把 NET_BUFFER 中数据地址映射给硬件 // 大多数网卡支持散列分散收集但我们一般先检查段数 ULONG nbCount NET_BUFFER_LIST_QUERY_NBL_NB(CurrentNbl); if (nbCount 32) { // 超过硬件DMA描述符上限走分片逻辑或直接丢弃并上报发送失败 } // 这里必须记住 NBL 的上下文字段用来在 完成 时把它还回去 NET_BUFFER_LIST_SET_CONTEXT_STRUCT( CurrentNbl, Adapter-SendContext[CurrentIndex], ADAPTER_SEND_CTEXT); CurrentNbl NET_BUFFER_LIST_NEXT_NBL(CurrentNbl); } // 将描述符列表写入硬件 TX 队列然后写 Doorbell 寄存器触发发送 // 如果该驱动支持“即时完成”可以在此处循环完成 // 但注意不要在 NDIS 的数据路径里做阻塞等待 —— 哪怕你用 KeDelay 也不行 NdisMSendNetBufferListsComplete( Adapter-AdapterHandle, NetBufferLists, NDIS_STATUS_SUCCESS); }这个逻辑里最重要的一个环节是NBL 的所有权在你手上完成发送后必须调用NdisMSendNetBufferListsComplete把它还给 NDIS。如果你忘记调用协议栈会认为该包还在途中最终导致发送队列内存泄漏系统内存池一点点被耗尽直到蓝屏。参数上SendFlags是NDIS_SEND_FLAGS_DISPATCH_LEVEL或NDIS_SEND_FLAGS_CHECK_FOR_LOOPBACK等标志位。如果你在中断 DPC 里处理发送完成不再对原 NBL 做额外操作一般直接原样传给完成函数即可。但如果是切到线程上下文里完成需要自行处理DISPATCH_LEVEL的限制不能把 IRQL 随意降级而不加保护。4.2 接收路径中断进来之后的三段式处理接收路径上网卡硬件通过 DMA 把包写进 RAM然后触发中断。你要做到的事情是从中断服务例程ISR快速判断这是自己设备的包、清中断、把处理交到 DPC。在 DPC 里从 DMA 描述符环里取包构造NET_BUFFER_LIST再通过NdisMIndicateReceiveNetBufferLists告诉协议栈。BOOLEAN MiniportInterrupt( _In_ NDIS_HANDLE MiniportAdapterContext, _In_ PVOID InterruptContext, _In_ PVOID MessageId) { PADAPTER Adapter (PADAPTER)MiniportAdapterContext; ULONG statusReg READ_REGISTER_ULONG( Adapter-RegBase REG_STATUS); if (!(statusReg INT_STATUS_RX_DONE)) { return FALSE; // 不是本设备的中断必须返回FALSE } // 立刻关闭中断/或写入Mask防重入 WRITE_REGISTER_ULONG(Adapter-RegBase REG_INT_MASK, 0); // 在ISR里绝不可以分配内存、调用NdisMIndicateReceiveNetBufferLists // 只需记录有事件让DPC去跑 NdisMQueueDpc(Adapter-DpcHandle, NULL, 0, 0); return TRUE; }DPC 里的操作VOID MiniportInterruptDPC( _In_ NDIS_HANDLE MiniportAdapterContext, _In_ PVOID InterruptContext, _In_ PVOID MessageId, _In_ PVOID DpcContext) { PADAPTER Adapter (PADAPTER)MiniportAdapterContext; // 1. 遍历DMA描述符环找到已经由硬件标记为owned by software的包 // 2. 对收到的每个包把缓冲区的物理地址对应的系统虚拟地址填入 NBL // 3. 记录 DataLength并更新 DMA 完成计数 // 4. 如果 RSS 开启根据哈希值计算 CPU调用带 CPU 编号的NdisMIndicateReceiveNetBufferLists // 普通版本第一个参数传 AdapterHandle // 5. 重新开启中断 }这里最常说的一句“血泪经验”是不要在 ISR 里做任何重量级操作。我不止一次看到有刚转来做驱动的同事在MiniportInterrupt里直接调用NdisMIndicateReceiveNetBufferLists结果 IRQL 不匹配系统立即蓝屏。如果厂商硬件没有 MSI-X 多队列单队列驱动更是要确保 DPC 处理速度能跟上中断频率否则中断风暴会吃掉整机 CPU。接收路径的绕不开的坑是 NBL 池的预分配。你必须在初始化时调用NdisAllocateNetBufferPool预留好一组 NBL 和NET_BUFFER并且让 DMA 环的缓冲区物理地址全部提前映射好。接收完成时不是“分配内存再把数据拷贝过去”而是直接把硬件缓冲区“上交”给协议栈再从池中取出一个新的缓冲区放回 DMA 环。这个交接如果不顺吞吐就崩。5. NDIS miniport 避坑九个前人踩出来的坑5.1 现象驱动安装了设备管理器中适配器显示“无法启动代码 10”原因MiniportInitializeEx返回非NDIS_STATUS_SUCCESS。但很多人不知道的是系统事件日志里往往不直接记录失败的具体 OID 或寄存器错误它只显示一个笼统的状态码。第一直觉应该去看SetupAPI.dev.log以及驱动里的DbgPrint缓冲。解决我在MiniportInitializeEx的每个可能失败的分支前都会调用NdisWriteEventLogEntry把失败原因记录为事件日志的EventLogDriverFailure。这个方法很老但有效至少比一句“代码 10”强太多。再者就是检查伴生的INF文件里Characteristics0x80这一类标志是否和驱动的 Release 类型匹配。5.2 现象装了驱动后任务栏网络图标始终是感叹号ipconfig显示媒体已断开原因初始化时没有设定初始的MediaConnectState或者MiniportRestartEx之后没有上报链路状态。NDIS 默认认为适配器是断开状态直到你调用NdisMIndicateStatusEx报NDIS_STATUS_LINK_STATE。解决在MiniportRestartEx成功且硬件链路已建立后显式调用NdisMIndicateStatusEx并在它的StatusBuffer里填一个NDIS_LINK_STATE结构。注意MediaConnectStateConnected只是字段值携带这个状态的 OID 请求还要能正确响应——也就是说MiniportQueryOid里对于OID_GEN_MEDIA_CONNECT_STATUS必须回MediaConnectStateConnected二者缺一不可。5.3 现象系统休眠唤醒后网卡断网需禁用再启用才恢复原因电源状态迁移处理不完整。NDIS 会在休眠时调用MiniportDevicePnPEvent并携带NDIS_DEVICE_PNP_EVENT_POWER_PROFILE_CHANGED等事件。如果驱动没有在 wake 后重新初始化 DMA 环或者没有重写某些被电源切断的寄存器就会丢包。解决在MiniportShutdownEx和MiniportDevicePnPEvent里做好状态保存在MiniportRestartEx里强制对 MAC 和 PHY 做一次完全复位soft reset并重新推进 DMA 描述符环的读写指针。别只依靠硬件寄存器的“非易失性”很多网卡从休眠醒来后 DMA 地址映射都没了必须重建。5.4 现象吞吐量只有几 MB/sCPU 占用却高到 30%原因多数情况是数据路径频繁操作 NBL 上下文和锁。比如你在发送路径里用了NdisAcquireSpinLock保护一个全程无并发问题的计数器就是严重的性能自杀另一个常见原因是接收中断没有合并interrupt moderation网络流量一大每秒几十万次中断CPU 全耗在中断到 DPC 的路上了。解决发送完成、接收指示都尽量走批处理。NDIS 提供了NDIS_RECEIVE_FLAGS_DISPATCH_LEVEL等标志位但更重要的是设计上把“循环处理多个包”作为最小处理单元而不是一次中断只收一个包。中断节流可以在硬件寄存器里设置阈值比如包数量达到 4 个或时间超过 50 微秒再触发一次中断。5.5 现象NdisMRegisterInterruptEx返回NDIS_STATUS_INVALID_PARAMETER原因NDIS_MINIPORT_INTERRUPT_CHARACTERISTICS结构里的InterruptType没填对或者MessageInfo在 NDR 中断下没有正确填充。特别是 MSI 配置错误时NDIS 库里表现为这个通用失败码。解决先确定硬件支持的是传统引脚中断还是 MSI/MSI-X。对传统的NDIS_INTERRUPT_TYPE_LEVEL_SENSITIVEMessageInfo必须清零且MessageId设为 0对 MSI-X 要用NdisMGetBusData或查询 PCIe 配置空间拿到中断消息号。我调试过的多数失败都发生在GetMessageInfo时没有检查PCI_CAPABILITY_MSI_X的下一个指针位置。5.6 现象包收发总耗一半另一半被丢原因这不是随机丢包很可能是 DMA 同步问题。网卡的 DMA 引擎把数据写到内存时使用了 CPU 缓存标记为不可缓存或未做MmMapIoSpace映射一致性设置导致 CPU 读到的数据是旧的。另一种是环形容量的深度问题——驱动 DMA 环只有 32 个描述符而硬件发送队列却可以接受 128 个写请求。解决先在MmMapIoSpace之后用Misc工具!pci 2 d或用 Windbg 的!devobj确认寄存器访问没问题。再查描述符自身是否为硬件要求的格式——比如是否设置结束位和主机端标记。最后在发送完成回路里加描述符计数确认每个完成的描述符都被释放后增加 RingSize 到 256 或 512。5.7 现象OID_GEN_RECEIVE_BLOCK_SIZE返回 0TCP 开始反复重传原因有些测试工具或上层服务通过 OID 查询驱动支持的最大接收包大小。如果驱动里没有实现这个 OID 或者返回值为 0协议栈会认为无法承载正常 MTU 流量于是放弃发送大包甚至把窗口协商成很小。解决在MiniportQueryOid里正确响应OID_GEN_MAXIMUM_FRAME_SIZE、OID_GEN_RECEIVE_BLOCK_SIZE、OID_GEN_TRANSMIT_BLOCK_SIZE等基础 OID。这个坑容易发生在“功能上能跑但 sys 老版本结构体填错”的情况下务必回头核对 NDIS 结构体版本宏。5.8 现象重启后 Inf 安装不上去在设备管理器里是黄色的“网络适配器未找到驱动”原因多半是co-installer和驱动版本签名问题。Windows 10/11 强制签名未签名的 NetCfg 安装会失败而且失败发生在复制驱动文件之后、启动服务之前导致设备信息残留。解决使用 WDK 自带的inf2cat和signtool做测试签名并且确认 INF 里DriverVer日期不是过去的、CatalogFile指向的.cat文件真实存在。安装前记得清理pnputil /delete-driver oemXX.inf再做新的安装。5.9 现象NdisMIndicateReceiveNetBufferLists导致 0xD1 蓝屏原因报告接收包后NDIS 会尝试访问NET_BUFFER_LIST的上下文区域如果你在初始化时没有正确分配上下文大小或是在接收 DPC 里擅自释放了一块正在被协议栈引用的内存就会触发DRIVER_IRQL_NOT_LESS_OR_EQUAL之类的问题。解决初始化时在NdisAllocateNetBufferPool的NBL_FLAGS中传入NDIS_NBL_FLAGS_CONTEXT对应对齐上下文空间接收时不要手动NdisFreeNetBufferList交给完成回调MiniportReturnNetBufferLists来做回收。铁律数据路径上NBL 的生命周期由 NDIS 规则决定不是你决定的。6. 验证你的 miniport用 Windbg 和 WPT 让隐藏状态现形开发完驱动只是第一步能不能在真实环境下稳定跑起来才是关键。我自己的流程是这样装好驱动、Network 图标正常之后先不管吞吐先打开 Windbg 双机调试在MiniportInitializeEx下断点然后用!ndiskd查看 NDIS 对这个小端口的内部状态。kd !ndiskd.miniport这个扩展命令会把所有已加载的 miniport 实例列出来包含适配器的设备名、句柄、状态。接着用kd !ndiskd.miniport handle -irp能够看到所有挂起的 OID 请求和 PNP IRP 队列确认是否存在“队列堆积”。如果大量 OID 请求一直挂起未完成真相就是你某条回调路径上真的阻塞了。性能验证方面用 WPTWindows Performance Toolkit里的WPA打开netsh trace或者xperf -on NDIS的 ETW 日志。重点看两个指标DPC里的NDIS中断处理耗时以及MiniportSendNetBufferLists的扇出是否繁忙。对了NT 内核里有个ndis.sys的性能计数器组用perfmon里的Networking类别也能看到小端口单队列的封装包数量。如果要验证我前面说的功耗——无论有没有流量先检查MiniportPauseEx和MiniportRestartEx是否能被系统正常调用。用!ndiskd.netadapter查看它当前的PnPEvents如果没有收到NDIS_DEVICE_PNP_EVENT_SURPRISE_REMOVED就有希望。有一条我保留到现在的“翻车后”习惯在MiniportSendNetBufferLists里对每个NBL的NET_BUFFER做一次KdPrint打印出它携带的源 MAC、目的 MAC、EtherType 和数据长度把输出重定向到 Windbg 缓冲区。第一次跑发包测试时你看到 EtherType 0x0806ARP能通过才说明你的这个 miniport 真正活过来了。从那以后我每次调一个新的小端口都会先把这个三步走流程强制走一遍!ndiskd.miniport验证状态、WPT 抓收发路径耗时、再抓 ARP 和 DHCP 的包确认协议栈默认通道可用三个都过了才敢往上层调 TCP 吞吐。这规矩看着繁琐但一次次把“为什么网卡连不上网”这种问题从玄学变成可定位的具体寄存器值。希望这个拆解对你有用——你手头那份源码或者驱动包值不值得下看它是否具备上面这些回调、这些 OID、这些状态处理基本就有了判断。本文还有配套的精品资源点击获取