NDIS小端口驱动开发:Windows网卡驱动的源码实现与调试

📅 发布时间:2026/10/20 1:47:58
NDIS小端口驱动开发:Windows网卡驱动的源码实现与调试
简介针对Realtek 8111/8168/8169/8110系列PCI千兆以太网控制器这份NDIS 6.0小端口驱动实例源代码为驱动开发者提供了难得的参考范本。相比DDK中仅有的E100BEX示例该实现更贴近主流网卡硬件完整覆盖驱动注册与初始化、包收发、中断处理、电源管理等核心环节压缩包内特别提供基础版本、LSO-jumbo支持版本、PM支持增强版本三种工程便于对照理解大包发送卸载与电源管理功能的扩展方式。资源包共69个文件大小约615KB包括3个zip源码工程、7个htm说明文档以及若干png、gif、js、css等网页辅助文件htm文档可用于离线查看开发说明图片和脚本则保留了原网页的图文排版与交互效果。目前已有1240人学习下载适合正在学习Windows内核驱动开发、或需要移植网卡驱动的工程师作为入门参考。通过这套代码可以快速建立NDIS 6.0 miniport驱动的整体框架并为后续调试与二次开发节省大量时间。1. 它是什么NDIS 小端口驱动与以太网卡驱动的「最小可运行原型」想写 Windows 网卡驱动的人通常先被中间层驱动、协议驱动的各种概念绕晕。这份资源只留一条主线NDIS 小端口驱动miniport driver它是网络驱动体系中最底层的一环直接对应物理网卡管理发送、接收、中断和电源状态。资源是一套用 C 语言写成的以太网卡小端口驱动源码包含 DriverEntry 到数据处理再到 OID 查询的完整回调集合编译产物是 .sys 内核模块。适合两类人想快速落地虚拟网卡或网络抓包工具的开发者以及刚入门 WDK、想搞懂 NDIS 数据路径的新手。把它的代码读完、跑通你对 Windows 网络栈的理解会比看十篇文档都具体。2. 架构定位小端口驱动在 Windows 网络栈里的「底层战场」2.1 三类驱动模型小端口为什么最难NDIS 是 Windows 为网络驱动提供的统一接口规范把网络驱动分成三个层次。最上层是协议驱动比如 TCP/IP 协议栈它负责把 socket 请求翻译成网络包中间是过滤驱动或中间层驱动可以对上下传递的数据做修改、统计、重定向最下层就是小端口驱动直接操作网卡硬件。协议驱动见到的是 NDIS 抽象出来的虚拟网卡小端口驱动见到的才是真实的总线、寄存器、中断和 DMA 描述符。小端口驱动是三个层次里难度最扎实的一个。它的代码几乎全是被动回调你写完一个函数不知道系统何时调用它调用的时候硬件处于什么状态也由外部决定。更要命的是数据路径上的接口大多是异步的发包时你只是把数据交出去实际写入硬件的时机在 DPC 或专用线程里收包时网卡随时可能中断你必须在极短的时间内把数据取走并通过 NDIS 提交给上层。同步写法在小端口驱动里基本不存在这是它和普通内核驱动最大的区别。小端口驱动还有一个容易被忽视的特点它拥有完整的网卡语义。设备枚举进来后NDIS 会为它创建网卡对象支持接口枚举、PnP、电源管理这些由 NDIS 统一接管但驱动本身要让出控制权。你写的 MpInitialize 会拿到一个 MiniportAdapterContext后续所有回调都通过它访问你的私有数据所以上下文结构体的设计质量直接决定整个驱动的可维护性。2.2 一张函数表撑起整个驱动小端口驱动的能力边界全部由一张特征结构定义NDIS_MINIPORT_DRIVER_CHARACTERISTICS。DriverEntry 里你只做一件事——填这张表然后交给 NdisMRegisterMiniportDriver。之后系统加载网卡时NDIS 会按需回调你注册的函数。所以判断一个驱动代码是否完整直接看这张表填了哪些函数。头几项是驱动版本号和 NDIS 版本对应关系用来告诉系统你要运行在哪个 NDIS 版本上。InitializeHandler 是入口系统为网卡创建设备对象后会先调它SendNetBufferListsHandler 是数据发送入口ReturnNetBufferListsHandler 是接收数据归还入口OidRequestHandler 处理上层下发的 OID 查询与设置。还有 Halt、Reset、Shutdown、CheckForHang分别负责停用、重置、系统关机和超时检测。这张表里最关键的设计思想是所有函数全部由 NDIS 调度。面向对象里叫接口内核里叫回调。你不需要自己跑主循环只需要在回调里保证两件事——及时返回、不泄露资源。比如 MpInitialize 失败NDIS 会根据错误码决定是否回滚设备对象MpSendNetBufferLists 没正确处理 NBLNDIS 会一直等你归还。理解这张表的人拿到任何小端口驱动源码都能在三分钟内画出它的控制流。2.3 数据在路上走的是 NET_BUFFER 链表NDIS 的数据包不是裸 buffer它被封装成三级结构NET_BUFFER_LISTNBL是一个数据包的集合NET_BUFFERNB代表一个具体数据包MDL 指向数据所在的内存。发送路径上协议驱动把一组 NBL 传下来小端口驱动逐个取 NB再通过 MDL 拿到物理地址写入硬件的 DMA 描述符。接收路径则相反小端口驱动把从硬件取到的内存包装成 NB塞进 NBL 后通过 NdisMIndicateReceiveNetBufferLists 上报。记住一个规则谁交给你 NBL谁就要负责归还。发送时上层把 NBL 借给你你用完后通过 NdisMReturnNetBufferLists 还回去接收时你分配 NBL 交给上层上层用完再通过你的 ReturnNetBufferListsHandler 还给你你还回去之后才能把它的内存释放或复用。这个归属关系搞错系统会蓝屏之后避坑章节还会提到。数据结构本身并不复杂难的是引用计数与状态转换。一个 NBL 可能同时被多个驱动引用比如过滤驱动挂起了它、协议驱动还在等待只有所有引用都释放后内存才能回收。小端口驱动不需要完全模拟这些引用关系但要严格遵守“返回后不再触碰”的纪律。2.4 数据路径的四个阶段把发送和接收连起来看小端口驱动的数据路径一共经历四个阶段上层下发、驱动拷贝、硬件收发、状态回收。上层下发发生在协议线程驱动拷贝发生在发送回调硬件收发发生在中断上下文状态回收发生在完成回调或 DPC。多队列网卡还会把不同队列分配在不同 CPU 上处理这四个阶段会并行出现。这也是小端口驱动调试难度高的真正原因它不是线性流程而是并发流程。你断点在发送回调里看到的数据可能只是整个链路的一小段要确认丢包发生在哪一层需要把上面四个阶段分别打日志再比对硬件统计寄存器和 NDIS 的 OID 计数。后续第 4 章的双机调试、第 5 章的丢包排查都是围绕这四个阶段展开的。2.5 常用 NdisM 接口速查开发小端口驱动一半时间是跟 NdisM 开头的接口打交道。把最常用的按功能整理如下接口用途NdisMRegisterMiniportDriver在 DriverEntry 里注册驱动特征NdisMMapIoSpace把 PCI BAR 映射为虚拟地址NdisMRegisterInterruptEx注册中断对象NdisMIndicateReceiveNetBufferLists把收到的 NBL 上报给 NDISNdisMReturnNetBufferLists归还发送用的 NBLNdisMIndicateStatus通知上层适配器状态变化比如链路断开NdisMAllocateSharedMemory分配 DMA 共享内存用于环形队列这张表值得贴在显示器旁边。Debug 的时候第一件事就是确认调用链上的 NdisM 接口有没有少调、调序错。比如网卡链路状态没上报大多数时候就是忘了 NdisMIndicateStatus接收环数据出不来先查 NdisMIndicateReceiveNetBufferLists 的条件是否正确。3. 核心实现拆解一个以太网小端口驱动的代码脉络3.1 DriverEntry注册一张完整的回调表下面是小端口驱动的入口代码也是整个项目的起点#include ndis.h NDIS_HANDLE g_MPHandle NULL; /* 全局注册句柄 */ /* 卸载例程注销小端口驱动 */ VOID MpDriverUnload(_In_ PDRIVER_OBJECT DriverObject) { if (g_MPHandle ! NULL) { NdisMDeregisterMiniportDriver(g_MPHandle); g_MPHandle NULL; } } /* 真正的驱动入口 */ NTSTATUS DriverEntry( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { NDIS_MINIPORT_DRIVER_CHARACTERISTICS chars; NDIS_STATUS status; NdisZeroMemory(chars, sizeof(chars)); chars.Header.Type NDIS_OBJECT_TYPE_MINIPORT_DRIVER_CHARACTERISTICS; chars.Header.Revision NDIS_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_2; chars.Header.Size NDIS_SIZEOF_MINIPORT_DRIVER_CHARACTERISTICS_REVISION_2; chars.MajorVersion 6; chars.MinorVersion 83; /* NDIS 6.83 */ /* 生命周期回调 */ chars.InitializeHandler MpInitialize; chars.HaltHandler MpHalt; chars.ResetHandler MpReset; chars.ShutdownHandler MpShutdown; chars.CheckForHangHandler MpCheckForHang; /* 数据路径回调 */ chars.SendNetBufferListsHandler MpSendNetBufferLists; chars.ReturnNetBufferListsHandler MpReturnNetBufferLists; chars.CancelSendHandler MpCancelSend; /* OID 请求回调 */ chars.OidRequestHandler MpOidRequest; chars.OidRequestCompleteHandler MpOidRequestComplete; status NdisMRegisterMiniportDriver( DriverObject, RegistryPath, chars, sizeof(chars), g_MPHandle); if (status ! NDIS_STATUS_SUCCESS) { return status; } DriverObject-DriverUnload MpDriverUnload; return NDIS_STATUS_SUCCESS; }这段代码里NdisZeroMemory 把特征结构清零是防止未初始化字段造成玄学问题。重点在 header 的类型、版本和大小三件套NDIS 会用它做对象校验少填或填错会直接拒绝注册。MajorVersion 和 MinorVersion 决定了驱动运行在哪个 NDIS 版本我写的是 6.83对应较新的 Windows 10 及以上系统如果你要兼容老系统改成 6.1 会更有把握。注册成功后驱动暂时静默。真正干活的时刻要等系统枚举到 PCI 网卡并调用 MpInitialize。3.2 MpInitialize从硬件到内存的一次握手MpInitialize 是每个适配器实例的初始化回调。系统检测到网卡后会调用它你在这里分配私有上下文、映射硬件寄存器、注册中断、初始化发送/接收环形队列。NDIS_STATUS MpInitialize( _In_ NDIS_HANDLE MiniportAdapterHandle, _In_ NDIS_HANDLE MiniportDriverContext, _In_ PNDIS_MINIPORT_INIT_PARAMETERS InitParams) { PMP_ADAPTER adapter; NDIS_STATUS status; /* 1. 分配适配器上下文所有回调都会用到它 */ adapter NdisAllocateMemoryWithTagPriority( MiniportAdapterHandle, sizeof(MP_ADAPTER), APMp, NormalPoolPriority); if (adapter NULL) { return NDIS_STATUS_RESOURCES; } NdisZeroMemory(adapter, sizeof(MP_ADAPTER)); adapter-MiniportAdapterHandle MiniportAdapterHandle; /* 2. 映射 PCI BAR 寄存器区域省略具体地址读取 */ // 常见做法读 PCI 配置空间的 BAR0再用 NdisMMapIoSpace // 把物理地址映射成虚拟地址保存到 adapter-RegBase。 /* 3. 初始化 DMA 描述符环形队列分配共享内存 */ /* 4. 注册中断优先 MSI-X失败回退传统 INTx */ status NdisMRegisterInterruptEx( adapter-InterruptHandle, adapter-InterruptInfo); if (status ! NDIS_STATUS_SUCCESS) { NdisFreeMemory(adapter, 0, 0); return status; } /* 5. 把适配器上下文和 NDIS 绑定 */ NdisMSetMiniportAttributes( MiniportAdapterHandle, adapter-NdisMiniportAdapterAttributes); NdisMSetMiniportContext(MiniportAdapterHandle, adapter); return NDIS_STATUS_SUCCESS; }几个参数理解一下MiniportAdapterHandle 是 NDIS 给这个网卡实例的句柄后面几乎所有 NdisM* 调用都要带上它InitParams 里带了网卡的中断资源、电源参数等一般在处理即插即用时要读取。分配内存用了 NdisAllocateMemoryWithTagPriority 而不是 ExAllocatePool2是因为 NDIS 包装过的内存分配会带上适配器维度的统计和优先级控制驱动里统一用它维护。注册中断是关键一步。PCIe 网卡可以申请传统中断线也可以申请 MSI/MSI-X 中断。常见做法是优先尝试 MSI-X失败再回退传统 INTx因为 MSI-X 可以把不同队列的中断绑定到不同 CPU多队列网卡性能差距非常明显。3.3 发送路径拿 NBL 写描述符网卡真正发数据发生在 MpSendNetBufferLists 里。上层协议把 NBL 传进来你要把 NET_BUFFER 里的数据地址和长度取出来写入 DMA 描述符然后触发硬件发送。VOID MpSendNetBufferLists( _In_ NDIS_HANDLE MiniportAdapterContext, _In_ PNET_BUFFER_LIST NetBufferLists, _In_ NDIS_PORT_NUMBER PortNumber, _In_ ULONG SendFlags) { PMP_ADAPTER adapter (PMP_ADAPTER)MiniportAdapterContext; PNET_BUFFER_LIST nbl; PNET_BUFFER nb; PMDL mdl; PVOID dataVa; ULONG dataLen; for (nbl NetBufferLists; nbl ! NULL; nbl NET_BUFFER_LIST_NEXT_NBL(nbl)) { nb NET_BUFFER_LIST_FIRST_NB(nbl); while (nb ! NULL) { mdl NET_BUFFER_CURRENT_MDL(nb); dataVa MmGetMdlVirtualAddress(mdl); dataLen NET_BUFFER_DATA_LENGTH(nb); /* 把物理地址填入发送描述符触发硬件发送 */ adapter-TxRing[adapter-TxTail].PhysAddr MmGetMdlPhysicalAddress(mdl); adapter-TxRing[adapter-TxTail].Length dataLen; adapter-TxRing[adapter-TxTail].Va dataVa; MpWriteRegister(adapter, REG_TX_TAIL, adapter-TxTail); adapter-TxTail (adapter-TxTail 1) % TX_RING_SIZE; nb NET_BUFFER_NEXT_NB(nb); } /* 数据已复制到描述符等硬件完成后再归还更严谨 */ NdisMReturnNetBufferLists( adapter-MiniportAdapterHandle, nbl, 0); } }这段代码把“发送”简化成了同步写描述符逻辑很清楚从 NET_BUFFER 里取 MDL通过 MmGetMdlPhysicalAddress 拿物理地址填进环形描述符然后写 tail 寄存器告诉硬件“队列里有新包”。NET_BUFFER_CURRENT_MDL 表示当前待处理的数据位置配合 NET_BUFFER_DATA_LENGTH 可以拿到剩余数据长度。NET_BUFFER_LIST_NEXT_NBL 用来遍历链表。真实驱动中发送通常不会同步完成因为硬件处理速度远慢于上层递交速度。工程做法是先把 NBL 挂到自己的待发送队列做完发送后由中断或超时回调里归还 NBL。这样要求你实现自己的 Tx 完成队列和引用计数复杂度会高一个量级。建议先跑通同步版本再渐进改成异步。3.4 接收路径中断里抢数据接收路径是被动等硬件中断。中断服务例程中先把数据从 DMA 环形队列里搬出来置 DPC 再进来完成 NBL 上报这是常见的组合。/* 简化版 DPC从接收队列取数据并上报 */ VOID MpRxDpc( _In_ NDIS_HANDLE MiniportAdapterContext, _In_ PVOID Context) { PMP_ADAPTER adapter (MP_ADAPTER *)MiniportAdapterContext; PNET_BUFFER_LIST nbl; PNET_BUFFER nb; PVOID buf; ULONG len; while (MpRingPop(adapter-RxRing, buf, len)) { nbl NdisAllocateNetBufferAndNetBufferList( adapter-RxNblPool, 0, 0, NULL, 0, 0); if (nbl NULL) { break; /* 池空了丢弃当前包让硬件继续收 */ } nb NET_BUFFER_LIST_FIRST_NB(nbl); NET_BUFFER_FIRST_MDL(nb) NdisAllocateMdl(adapter-MiniportAdapterHandle, NULL, buf, len); NET_BUFFER_DATA_LENGTH(nb) len; NdisMIndicateReceiveNetBufferLists( adapter-MiniportAdapterHandle, nbl, 1, 0); } }这里的思路是硬件收到帧后DMA 把数据放到预先分配好的接收缓冲里然后触发中断。DPC 中从环形队列弹出一块缓冲包装成 NBL 上报给 NDISNDIS 再把数据交到协议驱动栈。NdisAllocateNetBufferAndNetBufferList 从我们预先创建的空闲池里分配 NBLNB避免了频繁分配内核内存。接收方向同样存在“内存归属”问题上报出去的 NBL协议栈用完后会通过 MpReturnNetBufferLists 还给我们我们在那里把缓冲重新塞回接收环形队列或释放。这一来一回就是 NDIS 数据路径的完整闭环。3.5 OID 请求系统怎么“问”网卡系统装好网卡后会通过 OID 查询网卡能力比如速率、MAC 地址、链路状态。小端口驱动要在 MpOidRequest 里按 OID 分类处理。NDIS_STATUS MpOidRequest( _In_ NDIS_HANDLE MiniportAdapterContext, _In_ PNDIS_OID_REQUEST Request) { PMP_ADAPTER adapter (PMP_ADAPTER)MiniportAdapterContext; switch (Request-RequestType) { case NdisRequestQueryInformation: switch (Request-DATA.QUERY_INFORMATION.Oid) { case OID_GEN_CURRENT_PACKET_FILTER: Request-DATA.QUERY_INFORMATION.InformationBuffer adapter-PacketFilter; Request-DATA.QUERY_INFORMATION.BytesWritten sizeof(adapter-PacketFilter); return NDIS_STATUS_SUCCESS; case OID_GEN_LINK_SPEED: *(ULONG *)Request-DATA.QUERY_INFORMATION.InformationBuffer adapter-LinkSpeed; /* 单位:100bps */ Request-DATA.QUERY_INFORMATION.BytesWritten sizeof(ULONG); return NDIS_STATUS_SUCCESS; } break; case NdisRequestSetInformation: if (Request-DATA.SET_INFORMATION.Oid OID_GEN_CURRENT_PACKET_FILTER) { adapter-PacketFilter *(ULONG *)Request-DATA.SET_INFORMATION.InformationBuffer; return NDIS_STATUS_SUCCESS; } break; } return NDIS_STATUS_NOT_SUPPORTED; }OID 请求分查询和设置两类。查询返回当前值设置更新内部状态。OID_GEN_LINK_SPEED 的单位是 100bps也就是说千兆网卡要填 10000不是 1000。这个坑不少人踩过写错会直接导致系统显示的速率不对。处理 OID 时如果遇到不认识的 OID统一返回 NDIS_STATUS_NOT_SUPPORTED 即可不会让系统蓝屏只是功能降级。4. 跑起来从源码到能发包的完整链路4.1 编译环境VS 与 WDK 的版本匹配小端口驱动用 C 语言写但编译环境不是随便装个编译器就行它的工程依赖 Windows Driver KitWDK。常见组合是 Visual Studio 2022 对应版本的 WDK。安装 WDK 后VS 里会出现“驱动程序”项目模板源码工程就基于该模板创建。若你拿到的资源是 .sln 或 .vcxproj直接打开编译即可。如果编译报找不到 ndis.h 或 ntifs.h几乎可以断定是 WDK 没装或 VS 版本和 WDK 不匹配。装好对应版本后在项目属性页确认“Windows 驱动程序”平台配置再编译一次就过了。还有一点值得注意小端口驱动是内核态必须选择 x64 或适合当前系统的目标架构Win32 配置在 64 位系统里跑不起来。4.2 开启测试签名让系统接受你的 .sys内核驱动安装需要驱动签名开发环境里不必申请正式证书直接用测试签名模式即可前提是本地策略允许。# 以管理员身份打开 cmd bcdedit /set testsigning on shutdown /r /t 0重启后桌面右下角出现“测试模式”水印驱动签名验证就被放宽了。这个命令会自动处理证书链几乎所有本地调试都靠它。注意生产环境不要开测试签名水印和安全风险都不值得。如果重启后水印没出现检查 bcdedit 是否真的以管理员权限执行了。测试签名模式开启后.sys 文件还需要一个配套的测试签名证书WDK 安装时一般会自动配置驱动项目编译时会自动调用 inf2cat 和 signtool 做测试签名。4.3 用 INF 把驱动装到设备管理器小端口驱动需要一个 INF 文件描述设备 ID、驱动文件、服务名称的对应关系。核心节如下[Version] Signature $WINDOWS NT$ Class Net ClassGuid {4d36e972-e325-11ce-bfc1-08002be10318} Provider %ProviderName% DriverVer 10/01/2024,1.0.0.0 [Manufacturer] %ProviderName% DeviceList, NTamd64 [DeviceList.NTamd64] %DeviceDesc% XXMP_Device, PCI\VEN_1234DEV_5678 [XXMP_Device.NT] CopyFiles Drv_Dir [Drv_Dir] xxmp.sys [XXMP_Device.NT.Services] AddService xxmp, 0x00000002, Drv_Service [Drv_Service] DisplayName XX NDIS Miniport ServiceType 1 StartType 3 ErrorControl 1 ServiceBinary %12%\xxmp.sysINF 里的 ClassGuid 必须是 Net 对应的那个固定值写错会导致设备无法出现在“网络适配器”类别下。DeviceList 节的 PCI 格式是 VEN_硬件厂商IDDEV_设备ID如果资源里有真实硬件要按实际 PCI ID 改否则设备管理器识别不到。手动安装时在设备管理器里右键网卡设备选择“更新驱动程序—浏览我的电脑—让我从计算机上的驱动程序列表中选择”指向 INF 即可。安装成功后设备管理器里会出现一个新的以太网适配器。若之前驱动编译失败INF 和 .sys 的命名要保持一致ServiceBinary 路径写错系统会报“指定的服务未安装”。4.4 双机调试观察内核日志和断点驱动一旦蓝屏信息量有限最好从最初就跑双机调试。常见做法是宿主机上用调试器连接虚拟机WinDbg 里设置内核调试参数虚拟机开机前配置 bcdedit 的 debug 设置bcdedit /dbgsettings serial debugport:2 baudrate:115200 bcdedit /set debug on shutdown /r /t 0然后宿主机用调试器连接串口加载驱动后就能下断点。断点打在 MpSendNetBufferLists发一次 ping 就能确认发送路径是否正确进入。调试日志用 DbgPrint 输出用 !ndiskd 扩展查看 NDIS 内部状态。NDIS 扩展是调试 NDIS 驱动的利器。!ndiskd.miniport 可以列出所有注册的小端口实例!ndiskd.nbl 可以查看 NBL 的归属和引用计数。排查数据路径问题比单纯看寄存器高效得多。跑通一次双机调试后面所有踩坑都能在五分钟内定位到具体回调。4.5 验证用 ping 和 OID 计数确认驱动在工作驱动装好、系统识别网卡后第一件事不是抓包而是做基础连通性验证。给网卡配一个静态 IPping 网关或另一台机器。能通说明发送、接收、中断、DMA 描述符都基本正常。不通则按数据路径逐层排查。# 查看网卡速率和状态 ipconfig /all # 看网卡收发统计 netstat -e # ping 对端附带 64 字节包观察丢包和延迟 ping 192.168.1.1 -n 100更细的验证用 OID 计数。在 MpOidRequest 里支持 OID_GEN_RCV_OK、OID_GEN_XMIT_OK系统性能监视器里就能看到收发统计。如果 ping 通了但 netstat 计数不变问题出在 OID 实现没生效如果 ping 不通但 DbgPrint 显示回调被调用问题出在数据路径的中断或 DMA 部分。5. 避坑集合签名、蓝屏、丢包与版本错位的血泪排查5.1 驱动装不上事件日志报“无法验证数字签名”现象设备管理器里驱动带黄色感叹号查看事件日志显示签名错误。原因系统没开测试签名或 INF 里没有 NTamd64 架构节。64 位系统加载未签名驱动直接被拒这是最常见的装不上原因。解决确认第一步的 bcdedit /set testsigning on 已生效。若生效仍失败检查 INF 是否包含 NTamd64 节点以及 .sys 文件是否在 CopyFiles 列表中。还有一个隐蔽点编译时如果选择了 Debug 配置WDK 会自动做测试签名但 Manual 编译出来的 .sys 可能没签名需要手工 signtool 签名。5.2 装上了但设备状态是“代码 10”现象设备管理器显示“该设备无法启动代码 10”右下角网络图标是红叉。原因初始化回调返回失败或者 INF 服务配置的 StartType 不对。很多时候是硬件资源映射失败导致 MpInitialize 提前返回但驱动已注册成功设备也就挂在那里。解决在 MpInitialize 里逐段打 DbgPrint判断是内存分配失败、中断注册失败还是寄存器映射失败。如果中断资源没被分配检查 InitParams 里传递的资源列表是否被正确解析。代码 10 是万能错误码核心思路是先通过双机调试看驱动到底在哪一步返回非成功状态再针对性修。5.3 一装驱动就蓝屏而且每次都蓝在 ndis.sys 内部现象系统加载驱动后立刻蓝屏崩溃堆栈里有 ndis.sys 的符号。原因最可能的是发送路径上 NBL 归属权混乱比如提前把上层借给你的 NBL 释放了NDIS 后面再访问就撞了非法内存。这是小端口驱动新手翻车率最高的问题。解决记住规则——发送的 NBL 由 NDIS 交给你用完后通过 NdisMReturnNetBufferLists 归还接收的 NBL 由你交给 NDIS归还后你要回收。不要自行释放任何 NdisReturnNetBufferLists 之外的资源。蓝屏后用 !analyze -v 看一下 faulting module信息能对上十之八九。5.4 驱动加载成功但网卡链路状态一直是“网络电缆被拔出”现象能观察到你自己的初始化日志设备管理器显示正常但系统不认为网卡在线。原因小端口驱动没有上报链路状态。网卡连接状态是 NDIS 通过状态指示机制通知的初始化后如果没有调用 NdisMIndicateStatus 通知 NDIS_LINK_STATE上层默认认为无连接。解决初始化完成后检查真实网卡是否有对端连接。如果确认线连好了调用状态指示通知 NDIS 当前链路为千兆全双工推荐加一个轮询线程检测链路线缆插拔。链路状态指示不是可选动作而是小端口驱动的义务漏了它整个网卡在系统眼里就是“死”的。5.5 双向通信都通但大流量时丢包严重现象常规 ping 没问题传大文件时吞吐掉得很惨抓包看大量重传。原因接收环形队列太小DPC 里来不及取走所有包新包覆盖了旧包。也可能发送队列满后直接丢弃了 NBL而不是做流控。解决调大接收环形队列的深度从 256 调到 1024观察 DMA 内存是否充足。发送路径上如果描述符满不要直接丢包正确的做法是把 NBL 挂到待发送队列等中断完成后再补发送同时返回 NDIS_STATUS_PAUSED 通知上层降速。丢包问题的排查顺序是先看硬件寄存器丢包计数再看驱动 DPC 处理耗时最后看上层 NBL 归还情况。6. 二次改造把一个网卡驱动改成“虚拟网卡 统计仪表盘”拿到小端口驱动后最常见的改造方向不是改硬件而是把这块骨架改造成虚拟网卡或网络抓包统计工具。改造的核心思路很简单只要不碰真实硬件发送、接收、OID 三个回调就是全部入口。以发送路径为例我想统计每个发送包的长度分布只需在 MpSendNetBufferLists 里加一段采样逻辑/* 在 MpSendNetBufferLists 循环内插入 */ for (nbl NetBufferLists; nbl ! NULL; nbl NET_BUFFER_LIST_NEXT_NBL(nbl)) { nb NET_BUFFER_LIST_FIRST_NB(nbl); while (nb ! NULL) { dataLen NET_BUFFER_DATA_LENGTH(nb); /* 统计桶按 64B、128B、512B、1KB、1KB 分组 */ MpIncrStat(adapter, MpGetBucket(dataLen)); nb NET_BUFFER_NEXT_NB(nb); } }这样就是一个最简单的流量统计小端口驱动。过滤掉硬件寄存器操作之后它甚至可以跑在裸机环境里当“空网卡”专门为上层协议栈提供数据通道。在此基础上想变成抓包工具就在接收回调里把 NBL 的数据复制一份用共享内存或事件通知交给你自己的内核缓冲区再开一个应用层进程读取。注意点只有两个一是别在中断上下文里做耗时操作复制和排队放到 DPC 或工作者线程二是千万保持 NBL 归属权正确这是这类驱动最核心的纪律。我自己的习惯是每次改完小端口驱动都会用 !ndiskd.nbl 先确认一遍 NBL 的引用计数再跑一轮大流量压测用双机调试确认发送、接收、归还三个环节没有偏差。这套流程走过一次后面再做虚拟网卡、流量镜像、过滤驱动都会顺手很多。希望帮到你。本文还有配套的精品资源点击获取