第4篇:NDIS 驱动是什么鬼 —— Windows 网络栈的地下室

📅 2026/6/30 10:39:58
第4篇:NDIS 驱动是什么鬼 —— Windows 网络栈的地下室
第4篇NDIS 驱动是什么鬼 —— Windows 网络栈的地下室一、下潜到网络栈的“地下室”大部分人一辈子只跟应用层打交道——socket()、connect()、send()。这就像你住在一栋大楼的一楼大堂宽敞明亮有空调有沙发。你完全不需要知道脚下的地下室里正在发生什么。但如果你想要拦截——不是看一个包是劫持一个包——那你必须下地下室。那里又黑又冷管道交错空气中有一股潮湿的混凝土味道。但所有的管道都从那里经过。Windows 的这个地下室叫 NDIS。二、什么是 NDIS它的位置在哪里NDIS 全称 Network Driver Interface Specification。名字很朴素——“网络驱动程序接口规范”。它在 Windows 网络栈里的位置简单说是这样的┌──────────────────────────┐ │ 你的程序 (socket) │ ← 我要上 google.com ├──────────────────────────┤ │ TCP/IP 协议栈 │ ← 我知道怎么封装 TCP 包 │ (tcpip.sys) │ ├──────────────────────────┤ │ NDIS │ ← ★ 我们站在这里 ├──────────────────────────┤ │ 网卡驱动 (e1d.sys等) │ ← 我会跟网卡芯片说悄悄话 ├──────────────────────────┤ │ 网卡硬件 │ ← 电线 / WiFi 天线 └──────────────────────────┘NDIS 在中间。对上跟协议栈打交道“你要发包给我。”对下跟网卡驱动打交道“网线上来了数据给我看看。”。所有进出这台机器的网络流量都必须经过它。这就像一个城市的供水系统。你家水龙头是应用层一拧就出水自来水厂是网卡硬件水源而 NDIS 是埋在地下的主供水管道。你要在水里加氟、过滤、或者——如果你有恶意的话——下毒最好的地方是主供水管道。在水龙头那里加只能影响一家一户在主管道加影响整座城。三、NDIS 的三驾马车Miniport、Protocol 与 FilterNDIS 内部有三种角色构成了一个三层金字塔Miniport Driver微型端口驱动最底层管硬件的。每个网卡厂商都要写一个。Intel 的网卡有 Intel 的 MiniportRealtek 有 Realtek 的。它知道怎么跟特定的网卡芯片对话——哪个寄存器是写 MAC 地址的哪个中断是数据到了——但它不知道 IP 和 TCP 是什么意思。它的世界观止步于以太网帧。Protocol Driver协议驱动最上层管协议的。TCP/IP 协议栈本身就是 Protocol Driver。它的世界观是 IP 地址、端口号、序列号。它不知道也不关心网卡是 Intel 的还是 Realtek 的。Filter Driver过滤驱动夹在中间管拦截的。它是 NDIS 6.0Vista 之后才引入的角色。一个 Filter Driver 可以插入在 Miniport 和 Protocol 之间——对于从网线进来的包先经过 Filter再到 Protocol对于发出去的包先经过 Protocol再到 Filter再到网线。WinPkFilter 是 Filter Driver 和 Protocol Driver 的合体。这种双重身份让它极其强大——既能在包到达协议栈之前拦截Filter 的能力又能自己作为一个假协议栈接收包Protocol 的能力。四、搬运数据包的容器INTERMEDIATE_BUFFER在 WinPkFilter 的世界里每个被抓到的数据包都住在一个叫INTERMEDIATE_BUFFER的结构体里。这个结构体是整个系统最核心的数据结构。你不用记住它的每一个字段但要知道它的存在和大概形态前面一坨是元数据——这个包来自哪个网卡、是进还是出、多长。最后有一个大数组m_IBuffer[1514]里面装着完整的以太网帧——从目的 MAC 地址开始一直到最后的 CRC 校验虽然通常已被硬件剥离。1514 这个数字怎么来的标准以太网 MTU 1500 字节 14 字节帧头 1514。如果开启 Jumbo FrameMTU 可以到 9000帧总长就是 9014。这就是我们在整个系统中搬运数据包的基本单位。不是一个 IP 包不是一个 TCP 段——是一个完整的以太网帧。五、用户态与内核态的通信桥梁IOCTL用户态程序我们的 exe和内核态驱动WinPkFilter之间的通信方式叫IOCTLI/O Control。它的流程就像写信用户态打开设备文件\\.\NDISRD就像拿到信纸调用 DeviceIoControl(句柄, 操作码, 输入, 输出, …)」写信、贴邮票、寄出Windows 内核把请求转发给驱动的 IRP 处理函数邮局分拣驱动处理完结果拷回用户态的 output buffer回信到达DeviceIoControl返回你拆开信封读回信WinPkFilter 定义了几十个操作码但最常用的是这五个操作码干什么GET_TCPIP_INTERFACES列出系统上绑了 TCP/IP 的网卡SET_ADAPTER_MODE设置网卡工作模式READ_PACKETS批量读取拦截到的包SEND_TO_MSTCP注入一个包到协议栈“这是从网线来的”SEND_TO_ADAPTER注入一个包到网卡“这是要发出去的”注意READ_PACKETS是复数——批量操作。一次 IOCTL 调用要花几百个 CPU 周期主要是上下文切换。如果你一个一个读包每个包都付这笔税。一次读 512 个平摊下来每个包的成本几乎为零。六、内核对齐与防蓝屏的生存之道内核态和用户态共享同一套结构体定义——INTERMEDIATE_BUFFER在内核驱动里的含义和用户态程序里完全一致。如果两边对结构体布局的理解差了一个字节——比如某个字段的对齐方式不一样——数据就全乱了。而内核态数据乱了的结果通常是蓝屏。这就是为什么Common.h有 1000 行。它用#pragma pack(push,1)强制 1 字节对齐用大量的typedef确保 32 位和 64 位系统布局一致。它甚至定义了每个结构体的 WOW64 版本比如INTERMEDIATE_BUFFER_WOW64因为 32 位程序在 64 位系统上运行时指针大小不一样。这些看起来啰嗦的设计背后是驱动开发者被蓝屏折磨出来的血泪史。七、预告亲手“偷”出第一个数据包下一篇我们会真的从驱动里读出一个包。不是看结构体定义不是看 MSDN 文档——是让代码跑起来从网卡上偷一个包下来然后逐字节翻译成人话。这大概是整个系列里最让人兴奋的一篇因为你第一次能亲眼看到网络上流动的数据长什么样。本文是《从0到1编写一个硬核软路由》系列的第四篇。上一篇第3篇全景架构图 | 下一篇第5篇第一次偷包