NDIS协议驱动开发全解析:从框架到收发NetBufferLists
发布时间:2026/10/1 22:36:23来源:尧图网络
简介Windows网络驱动接口标准NDIS下的协议驱动与外网接口驱动开发实战资源主要面向底层网络驱动开发者、系统集成商以及对NDIS架构感兴趣的学习者。压缩包共31个文件、仅69KB核心为多组C源代码cpp/h、编译生成的sys驱动文件、INF安装配置以及Visual C工程文件dsp/dsw并附有可执行程序可完整呈现协议驱动从源码编写、编译到安装运行的闭环。资源内包含第8章ProcDrv、ProtoDrv、DriverDemo、ProcApp等多个Demo覆盖ndisprot、send/recv、packet.inf等关键模块能帮助读者掌握NDIS回调函数注册、数据包封装解封装、与网卡驱动交互的常用实现。已有77人浏览学习对于从事Windows驱动开发或希望进入底层网络技术研究的人员来说是一份结构清晰、可直接对照验证的参考范例。1. 网络驱动接口标准到底是干什么的先看协议驱动在 Windows 里的位置做 Windows 网络驱动开发的人几乎都会在标题里撞见“网络驱动接口标准”这七个字。它其实就是 NDISNetwork Driver Interface Specification微软从 NT 时代就定下来的内核网络驱动框架。如果你收到的是一份名为“windows 网络驱动接口标准和协议驱动开发”的压缩包里面装的大多是 NDIS 协议驱动的示例工程、编译脚本和配套文档。真正值钱的部分不是代码本身而是这套驱动在系统网络栈中占的位置协议驱动在网卡驱动之上、传输协议之下正好是抓包、过滤、拨号适配和 WAN 接口驱动的核心位置。很多人一开始分不清“协议驱动”和“网卡驱动”。简单说网卡驱动负责把数据从物理网卡搬到内核协议驱动负责把内核里的网络数据接住、按需要加工再交给上层协议或者反过来把上层协议的数据发出去。外网接口驱动、拨号驱动、基于 NDIS 的包过滤驱动本质上都是协议驱动的变体。这篇文章会从 NDIS 的驱动模型讲起给你一个能在本机跑起来的最小协议驱动骨架再把发送、接收路径的代码、参数、坑位全部拆开。适合两拨人一拨是刚拿到练习包却装不上驱动的初学者另一拨是已经被 NDIS 回调弄得蓝屏过几次、想系统搞清边界的内核开发者。2. 网络驱动接口标准与协议驱动先搞懂你在内核网络栈的哪一层2.1 NDIS 驱动模型Miniport、Protocol、Filter 三者的边界NDIS 把网络驱动分成三类微端口驱动Miniport、协议驱动Protocol和过滤驱动Filter。这三类驱动虽然都在内核里但职责完全不同接口也完全不同。微端口驱动是硬件驱动负责管理网卡、处理中断、收发数据到物理介质协议驱动没有自己的设备对象它是 NDIS 帮它绑定到一块网卡上用 NDIS 提供的 API 跟网卡通信过滤驱动则插在两者之间做流量监控、加解密、重定向这类中间处理。这三者最直观的区别用一张表格就能看清类型谁创建实例绑定方式典型用途Miniport驱动被加载时创建设备绑定到具体 PCI/USB 网卡网卡厂商驱动负责硬件数据收发ProtocolNDIS 调用 BindAdapter 绑定网卡通过绑定句柄与网卡交互TCP/IP 协议、拨号 PPP、抓包、WAN 接口驱动Filter插入到 Miniport 与 Protocol 之间按绑定关系挂接防火墙过滤、家长控制、流量整形协议驱动最容易和过滤驱动混淆。过滤驱动能改动经过它的一切数据包但它依赖绑定的顺序和附加句柄协议驱动则更像“接单”的一方它通过注册回调从一块网卡上把数据拿下来也能把数据送上去。常见做法是抓包工具里的 NDIS 协议驱动先绑定到网卡然后把进来的数据复制一份给用户态程序同时不影响 TCP/IP 协议栈的正常接收。这样网卡发出的数据、收到的数据都能看到这是过滤驱动做不到的因为过滤驱动改完包之后还得往回调链里继续塞容易影响栈。2.2 为什么协议驱动最容易被忽略却最适合做包处理很多人学 Windows 驱动从 WDF、KMDF 开始写的是字符设备、IOCTL很少碰 NDIS。原因是 NDIS 协议驱动的生命周期由 NDIS 管理而不是由你自己创建设备对象开发体验跟普通内核驱动完全不一样。也正因如此协议驱动常被忽略。但一旦你要做的功能是“贴着网卡看数据”协议驱动反而是最干净的入口。举例来说一个外网接口驱动需要在网卡收到数据时立刻拿到原始以太网帧。如果放在传输层过滤你看到的是 IP 包如果放在网卡微端口里改硬件驱动那得找网卡厂商要接口。而协议驱动绑定网卡后收到的 NetBufferLists 就是完整的以太网帧包括以太网头部。这个位置决定了它适合做协议分析、拨号封装、MAC 层桥接以及一些需要从链路层上手的上层协议适配。不过协议驱动不适合做需要修改数据包内容的操作。NDIS 协议驱动收到数据后默认是“用完即还”你想改包再放回网络栈就得调用 NdisReturnNetBufferLists 时保留缓冲、重新注入这需要额外的内存管理很容易漏掉引用计数。真要改包老老实实用过滤驱动。协议驱动适合“旁路观察”和“独立收发”不适合插手系统协议栈内部路径。2.3 NDIS 6.x 与旧版版本选型和回调函数差异现在做新驱动一律用 NDIS 6.x别再碰 NDIS 5.x。NDIS 6.0 从 Windows Vista 开始引入把数据结构从巨型集合体换成了 NET_BUFFER_LIST 链包开销大幅降低。到了 Windows 10/11NDIS 6.80 和 6.89 是常见版本。你打开 Visual Studio 的 WDK 工程在 .rc 文件里经常能看到 NDIS 版本宏比如 0x680 表示 NDIS 6.80。版本选型无需追新能用你目标系统的最低版本最好兼容性最大。协议驱动在 NDIS 6.x 下注册的结构体是NDIS_PROTOCOL_DRIVER_CHARACTERISTICS。这个结构体里有一堆回调函数指针核心是 BindAdapterEx、UnbindAdapterEx、ReceiveNetBufferLists、SendNetBufferLists、OidRequestComplete 这几个。NDIS 6.80 相比 6.0 增加了一些新的内部接口但对协议驱动来说老回调依旧稳定不需要为版本差异付出多少代价。真正要留意的是“函数名带 Ex”的回调比如ProtocolBindAdapterEx它支持异步绑定旧版BindAdapter已经被弃用新驱动不要用旧的。选型结论很简单如果你只是学习协议驱动直接以 NDIS 6.x 为目标驱动用 C 语言写编译成 x64 内核驱动。Windows 11 26H2 上依旧兼容 NDIS 6.89只要驱动签名和内存完整性这块不出问题老代码照跑。3. 用 WDK Visual Studio 搭开发环境最小可加载协议驱动的骨架3.1 开发工具链与调试环境WDK、双机调试、目标机设置协议驱动是内核驱动开发环境不只是“装个 IDE 写代码”。我一般的做法是一台开发机装 Windows 11 Visual Studio 2022 Windows SDK Windows Driver KitWDK。WDK 装完后Visual Studio 新建项目时会出现“Windows Driver”分类选“Legacy”下的“NDIS Protocol Driver”模板或者直接用 Empty Kernel Driver 再自己加 INF。没有模板没关系手动建工程把 NDIS 的头文件和库链接进来即可。调试协议驱动光在本机跑很容易蓝屏后没法救。最好准备一台虚拟机作目标机开发机用 WinDbg 通过网络调试连接。目标机开启内核调试开发机设置符号路径srv*C:\Symbols*https://msdl.microsoft.com/download/symbols这样断点时能看 NDIS 内部结构。没有第二台机器也可以用虚拟 KD 管道的方案但网络调试更接近真实部署建议直接配网络调试。Windows 11 26H2 上目标机的调试开关在“系统信息 - 高级系统设置”里不好找了直接用管理员命令行执行bcdedit /debug on和bcdedit /dbgsettings net hostip:192.168.1.10 port:50000 key:1.2.3.4。目标机的安全设置有一步很关键如果开了内存完整性HVCI内核隔离未签名或测试签名的协议驱动会被直接拒绝加载。开发调试阶段可以在“Windows 安全中心 - 设备安全性 - 内核隔离”里暂时关掉内存完整性还要以测试模式启动命令行执行bcdedit /set testsigning on后重启。这一步不做好你后面遇到的很多“驱动加载失败”其实是安全策略拦下来的不是代码问题。3.2 最小协议驱动的 DriverEntry 与 ProtocolCharacteristics先写一个最简的协议驱动不干任何事但能加载、能注册、能卸载。基于 NDIS 6.x 的协议驱动不需要创建设备对象DriverEntry 里只需要填充NDIS_PROTOCOL_DRIVER_CHARACTERISTICS然后调用NdisRegisterProtocolDriver。注意注册名字必须是 ANSI 字符串用NDIS_STRING类型而不是UNICODE_STRING这一步错的话 NDIS 直接返回失败。#include ntddk.h #include ndis.h NDIS_HANDLE g_ProtocolHandle NULL; NDIS_PROTOCOL_BIND_ADAPTER_EX ProtocolBindAdapterEx; NDIS_PROTOCOL_UNBIND_ADAPTER_EX ProtocolUnbindAdapterEx; NDIS_PROTOCOL_SEND_NET_BUFFER_LISTS HandlerSendNbl; NDIS_PROTOCOL_RECEIVE_NET_BUFFER_LISTS HandlerReceiveNbl; NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { NTSTATUS Status; NDIS_PROTOCOL_DRIVER_CHARACTERISTICS Chars; NdisZeroMemory(Chars, sizeof(Chars)); Chars.Header.Type NDIS_OBJECT_TYPE_PROTOCOL_DRIVER; Chars.Header.Size NDIS_SIZEOF_PROTOCOL_DRIVER_CHARACTERISTICS_REVISION_2; Chars.Header.Revision NDIS_PROTOCOL_DRIVER_CHARACTERISTICS_REVISION_2; NdisInitAnsiString(Chars.Name, SampleProtocol); Chars.MajorNdisVersion 6; Chars.MinorNdisVersion 80; Chars.BindAdapterExHandler ProtocolBindAdapterEx; Chars.UnbindAdapterExHandler ProtocolUnbindAdapterEx; Chars.SendNetBufferListsHandler HandlerSendNbl; Chars.ReceiveNetBufferListsHandler HandlerReceiveNbl; Status NdisRegisterProtocolDriver(DriverObject, Chars, g_ProtocolHandle); if (Status ! STATUS_SUCCESS) { return Status; } return STATUS_SUCCESS; }这段代码最关键的地方是Chars.Header。NDIS 6.x 所有结构体几乎都带 Header里面必须填对 Type、Size、Revision。NDIS_SIZEOF_PROTOCOL_DRIVER_CHARACTERISTICS_REVISION_2这个宏会帮你算好大小但前提是你已经在头文件里选定了 NDIS 版本。在源文件顶部#include ndis.h之前可以加#define NDIS_MINIPORT_MAJOR_VERSION 6这样的宏来强制版本不过常见做法是直接在工程属性里定义或者不定义让 WDK 默认走当前平台版本。NdisInitAnsiString初始化驱动名。名字是给系统看的状态字符串不能和别的协议驱动重名否则注册失败。NdisRegisterProtocolDriver成功后会返回一个 NDIS_HANDLE保存在全局变量里卸载时调用NdisDeregisterProtocolDriver(g_ProtocolHandle)。记住DriverEntry 里返回的即使成功也不能在这里创建设备对象协议驱动的设备由 NDIS 内部管理。3.3 编译、签名与安装INF 文件里的关键项协议驱动必须打包成 .sys 和一个 INF 文件才能安装。INF 的作用不是给用户看而是告诉 NDIS 怎么挂载这个协议驱动。和网卡驱动 INF 不一样协议驱动 INF 是“安装软件组件”的形式不能写硬件 ID。一个可用的 INF 通常这么写[Version] Signature $Windows NT$ Class Protocol ClassGuid {4D36E974-E325-11CE-BFC1-08002BE10318} Provider %ProviderName% DriverVer 09/01/2024,1.0.0.0 [DestinationDirs] DefaultDestDir 12 [DefaultInstall.NTamd64] CopyFiles SampleProtocolFiles [SourceDisksNames] 1 SampleProtocol [SourceDisksFiles] SampleProtocol.sys 1,, [SampleProtocolFiles] SampleProtocol.sys,,,2 [Strings] ProviderName ExampleNDIS这个 INF 里 Class 必须是ProtocolClassGuid 是固定的 NDIS 协议类 GUID。DefaultDestDir 12表示把文件复制到系统驱动目录实际是C:\Windows\System32\drivers。CopyFiles段里逗号最后那个2是标志表示复制到 drivers 目录。文件段不能改为自定义目录协议驱动不像普通内核驱动可以随意指定加载路径。编译时在 Visual Studio 里把解决方案配置选为 x64、Release。WDK 默认生成SampleProtocol.sys。安装方式是右键 INF 选择“安装”或者使用RUNDLL32.EXE SETUPAPI.DLL,InstallHinfSection DefaultInstall 132 路径\SampleProtocol.inf。安装后重启或运行net stop ndis不现实所以通常直接重启目标机再查看C:\Windows\System32\drivers\SampleProtocol.sys是否落盘。此时打开设备管理器——协议驱动不出现在网络适配器里而是出现在“网络协议”类别下。你看到它说明注册成功了。4. 把数据面做通绑定网卡与收发 NetBufferList4.1 绑定网卡ProtocolBindAdapterEx 与绑定上下文驱动加载并注册之后NDIS 会根据注册表里的 Binding 关系把协议驱动绑定到物理网卡或虚拟网卡上。绑定的入口是ProtocolBindAdapterEx。这个回调是异步提交的你发起NdisOpenAdapterEx后NDIS 完成后会再调用ProtocolOpenAdapterCompleteEx所以函数里不能同步等待结果必须记录 PENDING 状态等后续回调把绑定对象建好。typedef struct _BIND_CONTEXT { NDIS_HANDLE NdisAdapterHandle; NDIS_HANDLE NdisBindingHandle; NDIS_STRING AdapterName; BOOLEAN IsOpenComplete; NDIS_EVENT OpenCompleteEvent; } BIND_CONTEXT, *PBIND_CONTEXT; NDIS_STATUS ProtocolBindAdapterEx( NDIS_HANDLE ProtocolBindingContext, NDIS_HANDLE AdapterContext, PNDIS_STRING AdapterName, NDIS_STRING MultiCastList[], NDIS_STRING MediaTypes[], BOOLEAN BindingOptions) { PBIND_CONTEXT BindContext; NDIS_STATUS Status; NDIS_OPEN_PARAMETERS OpenParams; BindContext NdisAllocateMemoryWithTagPriority( ProtocolBindingContext, sizeof(BIND_CONTEXT), DbNp, NormalPoolPriority); NdisZeroMemory(BindContext, sizeof(BIND_CONTEXT)); BindContext-AdapterName *AdapterName; NdisZeroMemory(OpenParams, sizeof(OpenParams)); OpenParams.Header.Type NDIS_OBJECT_TYPE_OPEN_PARAMETERS; OpenParams.Header.Size NDIS_SIZEOF_OPEN_PARAMETERS_REVISION_1; OpenParams.Header.Revision NDIS_OPEN_PARAMETERS_REVISION_1; OpenParams.AdapterName AdapterName; OpenParams.MediaType NdisMedium802_3; OpenParams.ProtocolBindingContext BindContext; Status NdisOpenAdapterEx( ProtocolBindingContext, OpenParams, BindContext-NdisBindingHandle); return Status; }ProtocolBindingContext是 NDIS 传给你的每绑定上下文句柄也就是你之前注册协议驱动时提供的上下文。在绑定上下文中保存AdapterName因为在后续的 Open Complete 回调里你要知道当前绑定的是哪个网卡。OpenParams.MediaType这里写的是NdisMedium802_3以太网。如果要做外网接口驱动比如绑定到 WAN 口的拨号链路上MediaType 要用 NdisMediumWan 或者留空让 NDIS 决定否则 NDIS 不会把一个广域网接口绑定给这个协议驱动。要注意NdisOpenAdapterEx返回 NDIS_STATUS_PENDING 是正常的不是错误。你需要在ProtocolOpenAdapterCompleteEx里设置事件或完成标志驱动注册流程才算结束。绑定上下文必须用NdisAllocateMemoryWithTagPriority分配不能用ExAllocatePoolWithTag。NDIS 的内存管理在外面包了缓存和内存优先级直接使用内核池也能用但之后做自由内存分析时容易漏掉 NDIS 的引用关系。4.2 收包路径ProtocolReceiveNetBufferLists 的处理绑定成功之后网卡收到的包会通过协议驱动的 ReceiveNetBufferLists 回调进来。这个回调在 dispatch level 执行也可能在低 IRQL取决于网卡和 NDIS 的内部调度。代码里不能做太耗时的工作否则会影响吞吐并导致丢包。常见做法是只做统计、记录然后把 NetBufferList 直接标记为已消费再返回 NDIS。NDIS_PROTOCOL_RECEIVE_NET_BUFFER_LISTS HandlerReceiveNbl( NDIS_HANDLE ProtocolBindingContext, NDIS_HANDLE NdisBindingHandle, PNET_BUFFER_LIST NetBufferLists, NDIS_PORT_NUMBER PortNumber, ULONG NumberOfNetBufferLists, ULONG ReceiveFlags) { PNET_BUFFER_LIST Nbl NetBufferLists; ULONG Count 0; while (Nbl ! NULL) { Count; Nbl NET_BUFFER_LIST_NEXT_NBL(Nbl); } InterlockedIncrement(g_ReceiveCount); InterlockedIncrement(g_ReceiveNblCount); NdisReturnNetBufferLists(NdisBindingHandle, NetBufferLists, ReceiveFlags); }这个回调用NET_BUFFER_LIST_NEXT_NBL遍历链统计收到的包数量然后把整条 NetBufferList 链通过NdisReturnNetBufferLists还给 NDIS。协议驱动必须调用这个函数来归还接收缓冲区否则 NDIS 会认为协议驱动还在占用导致网卡缓存耗尽随后网络断开或系统死锁。ReceiveFlags参数里有一个NDIS_RECEIVE_FLAGS_DISPATCH_LEVEL标志位。如果为真说明当前在 DPC 上下文不能调用需要 PASSIVE_LEVEL 的同步 API。初学者常犯的错误是看到NdisReturnNetBufferLists就同步调用在 dispatch level 下使用NdisAcquireSpinLock倒是可以但要避免NdisWaitEvent、避免访问页换出内存。要做真正的抓包发送给用户态程序一般在这个回调里把 NetBufferList 的 MDL 数据复制到预分配的内存池中再通过事件通知用户态线程处理。注意不要直接持有 NBL 引用因为 NDIS 返回之后这个内存就不归你了。4.3 发包路径ProtocolSendNetBufferLists 与 NDIS_STATUS_PENDING协议驱动同样可以发包用NdisSendNetBufferLists把从上层获取的 NBL 交给绑定网卡。发送路径需要在回调里处理的是NdisSendNetBufferLists完成后的通知。看代码可能觉得奇怪因为 SendNetBufferLists 回调不是协议驱动主动调用的入口是上层协议驱动比如系统 TCP/IP通过 NDIS 调用协议驱动发送 NBL 的入口。但协议驱动和微端口驱动不同协议驱动没有发送完成回调只有在它把 NBL 传给下一层比如网卡后下一个层级的完成回调会通过NdisSendNetBufferListsComplete回来。写一个自己的发送函数把构造好的 NBL 发出去NDIS_STATUS SendSinglePacket(PBIND_CONTEXT BindContext, PNET_BUFFER_LIST Nbl) { NDIS_STATUS Status; NDIS_HANDLE NdisBindingHandle BindContext-NdisBindingHandle; NET_BUFFER_LIST_SET_NEXT_NBL(Nbl, NULL); Status NdisSendNetBufferLists( NdisBindingHandle, Nbl, NdisSendFlagsNormal, 0); if (Status ! NDIS_STATUS_SUCCESS Status ! NDIS_STATUS_PENDING) { NdisFreeNetBufferList(Nbl); return Status; } return NDIS_STATUS_SUCCESS; }NdisSendNetBufferLists是异步 API返回值只有 NDIS_STATUS_SUCCESS、NDIS_STATUS_PENDING 和错误状态三种。返回 PENDING 不表示失败只表示挂起。协议驱动不能因为返回 PENDING 就认为包已经发出必须在后续的ProtocolSendNetBufferListsComplete回调里拿到 NBL 的所有权后释放它。这里的坑在于你自己构造的 NBL必须自己完成NDIS 不会替你清理。如果你在失败分支直接释放 NBL但此刻 NBL 已经被 NDIS 接管就会导致双重释放蓝屏。发包时要特别注意NET_BUFFER_LIST_SET_NEXT_NBL(Nbl, NULL)。发送完的 NBL 链中NDIS 会遍历后续所有 NBL 来发送如果你的 NBL 链还连着旧地址NDIS 会顺着链去访问已经释放的内存。所以在把 NBL 交给 NDIS 之前一定要断掉链尾。回调里收到的 NBL 虽然通常是单条链但发到 NDIS 前我们必须确认末尾为空。这是一个看起来无关紧要、实际最容易翻车的细节。4.4 抓包型协议驱动的最小收发策略实际做抓包或监控时不需要发送路径只需要接收路径就够了。接收路径上我们需要在HandlerReceiveNbl里把每一个 NBL 的NET_BUFFER_LIST_NEXT_NBL遍历出来对每个NET_BUFFER用NET_BUFFER_DATA_LENGTH得到数据长度再取NET_BUFFER_CURRENT_MDL开始的一个或多个 MDL 来读取数据。ULONG CountNblBytes(PNET_BUFFER_LIST Nbl) { PNET_BUFFER Nb NET_BUFFER_LIST_FIRST_NB(Nbl); ULONG TotalBytes 0; while (Nb ! NULL) { TotalBytes NET_BUFFER_DATA_LENGTH(Nb); Nb NET_BUFFER_NEXT_NB(Nb); } return TotalBytes; }计算总字节数不能只取第一个 NET_BUFFER 的长度因为一个 NBL 可以由多个 NET_BUFFER 组成。每个 NET_BUFFER 的数据可能分散在多个 MDL 中要真正拷贝数据得遍历 MDL 并用MmGetSystemAddressForMdlSafe映射。这个操作很重抓包时如果每个包都做一次CPU 会被拖垮。常见的做法是在HandlerReceiveNbl里只统计包数量和总字节数定时批量从绑定上下文的一个环形缓冲区里读取摘要数据用户态进程再通过 IOCTL 取走。这既不会阻塞 NDIS 回调也不会因为每个包都做内存复制而影响系统网络性能。5. 避坑备忘录协议驱动加载失败的常见原因与排查5.1 现象驱动装不上事件日志只有“加载失败”原因八成是 INF 里的 Class 写错了或者系统安全策略挡了。协议驱动的 INF 必须用Class Protocol和固定的ClassGuid。如果你从网卡驱动模板改起Class 还是Net安装程序就会到网络适配器列表里找硬件设备协议驱动没有硬件 ID自然失败。也有可能是驱动没有测试签名64 位系统要求签名未签名驱动只能在使用测试签名模式时安装。打开“事件查看器 - Windows 日志 - 系统”筛选来源为“Kernel-PnP”或“Service Control Manager”的错误一般能看到具体返回码。解决方法是先把 INF 改回 Protocol 类执行bcdedit /set testsigning on再重新安装。安装后确认C:\Windows\System32\drivers\SampleProtocol.sys存在。如果文件不存在说明复制失败去 INF 的DestinationDirs里检查DefaultDestDir 12是否少写了分号。5.2 现象协议驱动不在“绑定”列表里网卡看不到它安装成功但打开ncpa.cpl右键网卡属性列表里没有你的协议。NDIS 绑定关系存储在注册表里协议驱动只注册不够还要建立绑定。NdisRegisterProtocolDriver只告诉系统存在这个协议NDIS 会默认把协议绑定到所有符合条件的网卡上但前提是你的回调都能正确处理绑定选项。如果ProtocolBindAdapterEx返回失败NDIS 会跳过该绑定的建立。失败常常来自NdisOpenAdapterEx传入的 MediaType 不对网卡是以太网你填了NdisMediumWan绑定直接失败。对外网接口驱动目标网卡是 Ethernet所以大部分情况下填NdisMedium802_3。想确认当前网卡媒体类型用 WinDbg 断点查看AdapterName对应的网卡注册表项。打开HKLM\SYSTEM\CurrentControlSet\Control\Network\{4D36E975-E325-11CE-BFC1-08002BE10318}找对应的全局唯一标识看媒体类型配置。5.3 现象一发收包就蓝屏Windbg 停在 NDIS 回调蓝屏最常见的原因是引用计数错误和 NBL 释放错误。收到包时协议驱动如果要把 NBL 留在自己的缓冲里必须调用NdisRetainNetBufferList增加一个引用完成处理后要调用NdisRetainNetBufferList对应的NdisFreeCloneNetBufferList或NdisReturnNetBufferLists释放。如果只存不增引用NDIS 回收后你再访问就蓝屏如果存了不释放则内存泄漏最终导致系统卡死。发送路径上自己构造的 NBL 发出去后在ProtocolSendNetBufferListsComplete里要释放不能在错误分支里直接释放。排查时在 Windbg 中用!ndiskd.nbl 地址查看 NBL 引用计数低于 1 就是已经被释放了。处理对策是统一用一个引用计数表每个 NBL 地址对应一个状态机只在“透明期间”持有引用处理完就释放。代码里尽量少绕弯不要同时用“复制一份”和“持有原 NBL”两种路径交替处理最容易出错。5.4 现象Windows 11 24H2/26H2 上加载被拒内存完整性拦截新系统对内核驱动的要求越来越高。内存完整性开启时所有内核驱动都必须是微软签名或 WHQL 签名测试签名在 HVCI 开启时视为无效。现象是 INF 安装时显示“拒绝”或者驱动已经复制到 System32\drivers但设备管理器看不到。解决办法是先把内核隔离里的内存完整性暂时关闭再以测试模式运行。但这不是上策因为目标机器上如果一直开着 HVCI你的协议驱动永远跑不起来。更好的办法是给你的驱动做微软 Attestation 签名或 WHQL 签名这也是部署阶段的必由之路。开发机上不开 HVCI目标机如果要开 HVCI必须提前把驱动提交到微软兼容硬件开发计划。这里没有捷径除非你只做学习实验不部署。Windows 11 26H2 对 NDIS 协议驱动本身没有新的阻断阻断全部来自签名策略。5.5 现象发送永远返回 NDIS_STATUS_PENDING但不发中断如果你实现的是一个带发送能力的协议驱动可能遇到NdisSendNetBufferLists一直返回 PENDING但网卡没有实际发出任何包。常见原因是 NBL 的 MDL 在发送时被修改或内存被释放。NDIS 发送路径是异步的网卡 DMA 会直接读取 MDL 指向的内存如果你把 NBL 所在内存释放了网卡读到的就是垃圾或无效地址DMA 失败后 NDIS 不会收到中断只留下一个永远 PENDING 的请求。另一个常见原因是 NBL 的数据区不完整。比如你从系统上层拿了一个 NBL却强行改变NET_BUFFER_DATA_LENGTH破坏了长度与 MDL 的一致性。解决方法是发送前不要动 MDL只修改NET_BUFFER_DATA_OFFSET或追加一个单独的内存描述。构造发送包时用NdisAllocateNetBufferList分配 NBL再用NdisAllocateNetBufferAndCopyToMdl把数据复制进去之后不要随意改内存。发送挂起时用 WinDbg 的!ndiskd.netbuffer 地址查看 NET_BUFFER 的 MDL 状态和缓冲地址如果地址是虚的且物理地址无效基本就是内存生命周期管理错了。6. 进阶一把用 ndiskd 和内建计数器验证协议驱动6.1 用 ndiskd 看绑定与协议注册环境配好后验证驱动是否正常工作第一站是 WinDbg 里的 ndiskd。这个扩展命令从 Win10 开始内置不用单独安装。进入内核调试后输入!ndiskd.protocol会列出所有已注册的协议驱动找到你的名字比如SampleProtocol。再输入!ndiskd.protocol SampleProtocol -bind能看到它绑定了哪些网卡以及绑定状态是 Open 还是 Pending。如果绑定显示 Active说明绑定已经完成接下来看收发。6.2 用性能计数器与 ping 验证收发路径协议驱动通常不为系统提供性能计数器所以自己可以在驱动里用KeQuerySystemTime记录收到的包速率再通过一个调试打印输出。如果没有用户态程序最简单的验证方法是用ping本机网关观察驱动里的g_ReceiveCount是否增长。你可以在 Windbg 里用dt命令查看全局变量或者提早加一句KdPrint((RECV %d\n, g_ReceiveCount))打开内核调试输出。每次 ping 出去应该能看到计数增加。如果只增加了发送而不知道是否收到回包就在 Receives 回调里设断点Windbg 断下后看断点命中时的NetBufferLists地址再对比 !ndiskd 的输出。收发路径都通了说明这个协议驱动已经能用作外网接口驱动的骨架。做完这一步我通常会加一个简单的OID_REQUEST_COMPLETE回调处理查询网卡状态因为很多协议驱动需要响应上层的 OID 查询哪怕只是返回常量。这是新手最容易漏掉的“看不见的接口”。当初我第一次写完协议驱动收发看着都通但是用户态用DeviceIoControl查不到任何状态折腾半天才发现ProtocolOidRequestComplete一直是空函数NDIS 默认返回不支持应用层自然拿不到数据。驱动开发这种东西细节永远是玄学把回调表填全、把引用计数理清稳定了再谈性能。希望这份笔记能让你少踩我踩过的那些坑把网络驱动接口标准和协议驱动这条路走通。本文还有配套的精品资源点击获取
网站建设高端定制企业官网