ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

深入解读TDI驱动源代码:从核心结构到最小骨架实操指南

深入解读TDI驱动源代码:从核心结构到最小骨架实操指南 简介TDI驱动源代码是一份面向Windows驱动开发与网络监控场景的完整工程适合需要了解TDI编程、进程网络行为监测的开发者。代码为作者自行搭建框架未借用tdifw等源码实现了应用层实时获取每个进程打开的网络端口变化、按进程统计流量、按连接查看流量以及禁止指定进程访问网络等功能兼容Win7/XP的32位与64位系统。压缩包共68个文件包含8个cpp、7个h、2个c等源码以及工程配置、makefile、编译生成的2个sys驱动和exe测试工具另有18个xml、18个obj、3个pdb等工程辅助文件整体仅567KB。已有528人学习下载。限速功能虽未实现但代码预留接口读者可在此基础扩展借此理解360流量防火墙限速的核心思路。由于作者仅用10天完成框架清晰但细节有待完善应用层仅提供接口代码适合作为二次开发起点。 我最早接触“TDI驱动源代码”这个关键词是在翻一些老驱动项目时候。TDITransport Data Interface传输数据接口是Windows内核里一套非常经典的网络传输接口当年很多网络过滤、协议分析、流量统计工具都建立在它之上。如果你现在搜到一堆TDI源码却不知道从哪开始读或者想自己写一个内核态的网络通信模块这篇文章就是给你准备的。我会从一个实际动手写过TDI驱动的角度把它的定位、核心数据结构、请求模型、一个最小可跑的骨架以及我踩过的坑全部拆开讲清楚。即便你之前没写过Windows驱动只要懂一点C语言和IRP的概念也能跟着走下去。1. TDI驱动到底是个什么东西1.1 TDI的定位Windows网络栈的一个中间层Windows的网络体系不是一坨整体它是分层的。物理网卡在最下面上面是NDISNetwork Driver Interface Specification这一层负责跟网卡打交道再往上是传输协议驱动比如当年的TCP/IP驱动、NetBIOS、IPX等再往上是各种内核态的客户端比如重定向器、网络服务。TDI就夹在传输协议驱动和这些客户端之间提供了一套统一的内核接口。用生活里的话来说它像是一个“插座标准”。你家里墙上有很多电器它们形状各异但插头规格是统一的因为国家规定了插座标准。TDI干的事情就是规定所有传输协议驱动必须向上一层提供同样的“插孔”所有内核态客户端也必须按同样的“插头”来对接。这样一来TCP/IP想换IPv6、想增加新的传输协议上层应用感知不到。1.2 它解决了什么问题在没有TDI之前每个传输协议都有自己的一套接口上层要同时支持TCP、UDP、NetBIOS就得分别写适配代码。TDI统一之后驱动开发者只需要学会一套接口就能同时操作多个传输协议。对当时的人来说这极大降低了内核网络编程的复杂度。从源代码阅读的角度TDI驱动的核心代码通常分为两类一类是TDI客户端驱动也就是调用TDI接口来发起连接、收发数据的驱动比如远程桌面早期版本、文件和打印共享的重定向组件另一类是TDI过滤驱动它挂在TDI层中间可以拦截、观察甚至修改传输数据早期很多防火墙和个人安全软件都这么干。1.3 到了今天为什么还要读它的源码这个问题很现实。TDI在Windows Vista之后官方就不再建议新驱动使用微软逐步用Winsock KernelWSK替代了它。到了Windows 8之后TDI接口基本处于“能用但不推荐、文档残缺、新SDK里不再提供完整头文件”的状态。但是老系统的兼容性维护还在很多工业设备、银行终端、嵌入式Windows系统的网络组件仍然是当年的TDI驱动。还有一个更常见的场景你在网上找到的很多“网络过滤驱动”教学源码都是TDI时代的想搞懂里面的思路就必须先把TDI本身看明白。所以读TDI源码更多是为了读历史、读思想、读那些一直延续到今天的IRP处理套路而不是为了在新系统上直接用它打赢天下。2. 读TDI驱动源代码前要补齐的底子2.1 环境准备WDK版本选择读源码和写源码不一样但为了能编译、能调试你得先把工具链准备好。我建议用Visual Studio 2019或2022配合对应的Windows Driver Kit。严格来说新WDK里已经移除了TDI的头文件但你不一定非要手搓出TDI头文件因为很多开源项目里自带一份老的头文件包。我个人更推荐的做法是找一个历史版本的WDK 7或WDK 8源码参考把tdi.h、tdikrnl.h、tdistat.h这几个文件单独抽出来放到你的项目文件夹里。这几个头文件定义了TDI的核心常量、结构体和函数原型是读源码的地图。有个容易忽略的细节TDI驱动里很多目标设备名是用宏定义的比如\Device\Tcp、\Device\Udp、\Device\Rawip。不要直接在代码里反复写字符串源码里通常会用DD_TCP_DEVICE_NAME这样的宏统一定义。你读代码时先找到这些宏就能快速定位驱动是在跟哪个传输协议通信。2.2 核心模型文件对象、设备对象、IRPTDI编程模型的精髓可以总结为“三件套”设备对象、文件对象和IRP。设备对象传输协议驱动创建的命名设备代表一个协议栈入口。比如\Device\Tcp就是TCP/IP协议驱动的设备。文件对象客户端驱动通过IoCreateFile打开这个设备后得到的一个句柄。一次打开就代表一条“会话”的上下文。IRP后续的绑定地址、连接、收发数据全部转换成IRP下发下去。把这三者理解成一次寄快递的过程设备对象是快递网点文件对象是你开好的一个账户IRP就是你每次填写的快递单。你先把账户开了之后每寄一件东西都填一张单子网点按单子干活。2.3 TDI请求的两种形态TDI_REQUEST与直接IRP老源代码里你会看到两种写法非常容易混淆。第一种是使用TDI_REQUEST这种结构把请求类型比如TDI_SEND、TDI_CONNECT、TDI_DISCONNECT、文件对象、请求参数全部封装在一个结构体里然后调用TdiBuildSend、TdiBuildConnect等辅助函数来构建IRP。这种写法是微软TDI头文件提供的标准方式可读性好但代码量大每个请求都要设置一堆参数。第二种是绕过辅助函数直接构造IRP、设置IoStackLocation的MajorFunction比如IRP_MJ_INTERNAL_DEVICE_CONTROL和Parameters。这种写法更底层但更灵活很多过滤驱动和追求性能的源码都这么写。我建议你读源码时先认准第一种等看懂再去看第二种。3. 一个最小可跑的TDI客户端驱动骨架3.1 设计思路明确一下我们要实现什么一个内核驱动启动时打开\Device\Tcp解析一个IP地址主动对外建立一个TCP连接然后随便发几个字节。这个流程虽然简单但覆盖了TDI驱动最核心的几步创建设备、打开传输地址、绑定地址、连接、发送。我们不管收数据因为收发原理一致把发送搞通接收就是多一个事件回调的事。为了让驱动能被加载测试我们还要给它提供一个应用层控制的入口。用IRP_MJ_DEVICE_CONTROL接收用户态下发的连接参数这样就不用在驱动里硬编码IP了。3.2 主要数据结构一个最小项目里我会定义下面这些关键对象typedef struct _TDI_CONNECTION_CONTEXT { HANDLE TransportHandle; // 打开传输设备得到的句柄 PFILE_OBJECT TransportFileObject; HANDLE AddressHandle; // 打开地址设备得到的句柄 PFILE_OBJECT AddressFileObject; HANDLE ConnectionHandle; // 连接对象句柄 PFILE_OBJECT ConnectionFileObject; TA_ADDRESS RemoteAddress; // 远端地址 TA_ADDRESS LocalAddress; // 本地地址 PETHREAD SendingThread; // 不一定要线程看设计需要 } TDI_CONNECTION_CONTEXT;TA_ADDRESS是TDI里表示传输地址的结构它是一个通用容器里面包含了地址族、地址长度和地址数据。对IPv4 TCP地址数据部分就是TA_TCP_ADDRESS里面再嵌套TA_IP_ADDRESS。套娃得厉害读源码时耐心拆开看。3.3 关键代码骨架下面是入口函数和打开传输设备的部分代码。注意这段代码为了展示核心逻辑做了精简实际项目需要加完整的错误处理NTSTATUS TdiDriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { NTSTATUS status; PDEVICE_OBJECT deviceObject NULL; UNICODE_STRING deviceName; UNICODE_STRING symbolicLink; // 创建控制设备对象用于应用层通信 RtlInitUnicodeString(deviceName, L\\Device\\TdiDemo); status IoCreateDevice(DriverObject, sizeof(TDI_CONNECTION_CONTEXT), deviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, deviceObject); if (!NT_SUCCESS(status)) { return status; } RtlInitUnicodeString(symbolicLink, L\\DosDevices\\TdiDemo); status IoCreateSymbolicLink(symbolicLink, deviceName); if (!NT_SUCCESS(status)) { IoDeleteDevice(deviceObject); return status; } DriverObject-MajorFunction[IRP_MJ_DEVICE_CONTROL] TdiDemoDeviceControl; DriverObject-DriverUnload TdiDemoUnload; return STATUS_SUCCESS; }接着是打开TCP传输设备。这里要注意必须通过IoCreateFile打开而且DesiredAccess、ShareAccess这些参数要符合TDI的要求否则后续一切白搭NTSTATUS OpenTcpTransport(PHANDLE TransportHandle, PFILE_OBJECT *TransportFileObject) { NTSTATUS status; OBJECT_ATTRIBUTES objectAttributes; UNICODE_STRING deviceName; IO_STATUS_BLOCK ioStatusBlock; RtlInitUnicodeString(deviceName, DD_TCP_DEVICE_NAME); InitializeObjectAttributes(objectAttributes, deviceName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL); status ZwCreateFile(TransportHandle, GENERIC_READ | GENERIC_WRITE, objectAttributes, ioStatusBlock, NULL, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_OPEN, 0, NULL, 0); if (!NT_SUCCESS(status)) { return status; } status ObReferenceObjectByHandle(*TransportHandle, FILE_ANY_ACCESS, *IoFileObjectType, KernelMode, (PVOID *)TransportFileObject, NULL); return status; }打开传输设备只是拿到了一张“通用入场券”。真正要跟某个地址通信还得创建地址对象和连接对象。这一步很多新手会跳过去导致后面TdiBuildConnect永远返回STATUS_INVALID_PARAMETER。正确顺序是用TdiOpenAddress或者手工构造IRP打开一个地址对象绑定本地IP和端口。用TdiOpenConnection创建一个连接对象。调用TdiBuildConnect下发连接请求。绑定本地地址这一步如果不需要固定本地端口内核会临时分配但如果你的过滤驱动或者中间层逻辑需要绑定到特定网卡就得显式指定。实际操作中我见过不少源码在这块直接写死0.0.0.0:0这是合法的但会导致连接全部走默认路由多网卡机器上容易出问题。3.4 编译、签名和部署WDK环境下编译直接选“Kernel Mode Driver”项目类型输出.sys文件。64位系统需要签名测试环境可以开启测试签名模式bcdedit /set testsigning on然后在项目配置里选择“Test Signature”证书。这一步卡住过很多人实际只是签名配置没做。部署时我习惯用sc命令sc create TdiDemo type kernel binPath C:\Windows\System32\drivers\TdiDemo.sys sc start TdiDemo调试的话用WinDbg做内核调试最靠谱。如果只是验证加载是否成功先看sc query TdiDemo返回的状态再看!devobj \Device\TdiDemo确认控制设备有没有创建成功。4. 常见问题与排查技巧实录4.1 我离真正跑通还差这几步下表整理了我自己以及周围朋友在写TDI驱动时最高频的几个报错顺着这个表排查能省很多时间。现象可能原因排查方向STATUS_OBJECT_NAME_NOT_FOUND打开的TDI设备名不存在确认用的是DD_TCP_DEVICE_NAME而不是普通网络名STATUS_INVALID_HANDLE文件对象句柄没转换成对象就开始下发IRP检查是否调用了ObReferenceObjectByHandle连接永远超时没有预先打开地址对象检查绑定流程是否完整应用层DeviceIoControl返回缓冲区不够输入输出缓冲区长度与驱动里设置不匹配检查METHOD_BUFFERED与OutputBufferLength蓝屏IRQL_NOT_LESS_OR_EQUAL在DispatchLevel上执行了分页代码TDI和IRP处理中涉及内存拷贝时注意降低IRQL这里额外提醒一句TDI驱动里很多请求的回调是在DISPATCH_LEVEL下执行的比如连接完成回调、接收事件回调。你在回调里绝对不能直接调用IoCreateFile这类需要PASSIVE_LEVEL的函数也不能直接操作分页内存。很多源码看似能跑但在高负载下崩溃原因就是回调里的IRQL处理不严谨。4.2 用WinDbg定位问题的手感调试TDI驱动我很少靠加DbgPrint硬猜。一般流程是先用!process 0 0确认目标进程是否创建了句柄。再用!handle 句柄值查看句柄对应对象类型和引用计数确认文件对象是否存活。如果IRP下发了但没返回用!irp 地址看IRP的pending状态和栈位置。有一次我遇到一个很奇怪的问题驱动在虚拟机里一切正常到了实体机就随机蓝屏。我抓了dump发现是某个结构体没对齐导致ASSERT失败。解决方法也简单把TA_ADDRESS缓冲区从栈里挪到堆里并用RtlZeroMemory初始化后再填字段。这个经验告诉我TDI代码里结构体套结构体非常深编译器对齐可能会插入填充字节老代码又经常用#pragma pack(1)来压紧凑。看别人源码时留意这个宏复制代码时不能只复制自己关心的那一段上下文的对齐指令也要一起带过来。4.3 读别人TDI源码的正确打开方式网上捡来的TDI源码质量参差不齐。我不建议一上来就从头读到尾那样很容易迷失在IRP处理的长河里。我的顺序是先看DriverEntry搞清创建了哪些设备、注册了哪些MajorFunction、是否有Filter设备挂载。再看Unload看它释放资源的顺序这能反向告诉你它到底创建了什么。然后看一个完整的请求链路从TdiBuildConnect到完成例程把它当一条线捋完。最后看过滤驱动才有的拦截逻辑比如在TdiBuildReceive完成例程里做数据篡改这类改动通常是项目核心。有人会问网上还有一些TDI源码里的TDI_FILTER前缀比如\Device\TdiFilter是做什么的这是TDI过滤驱动创建的控制设备它通过IoAttachDevice挂到真正的TDI设备上。如果你看到这样的代码说明项目本质是过滤数据而不是主动发起连接。这两种项目结构差异很大别混着读。5. 从TDI源代码里能迁移出来的通用能力TDI代码老归老但它的内核编程思想并不过时。至少有三点能力是能平移到现代Windows驱动开发上的。第一IRP的完整生命周期管理。不管是TDI还是今时今日的WDF驱动驱动对IRP的排队、取消、完成、复用逻辑思维方式完全一致。你在TDI代码里看到的IoMarkIrpPending、IoCompleteRequest、CancelRoutine在后面写WDF的EvtIo*回调时依然要理解背后的IRP机制。第二异步完成模式的写法。TDI驱动几乎全是异步先下发请求注册完成例程完成例程在任意上下文被操作系统回调。这种“一事一回调”的模式在今天的UWP驱动、过滤器驱动中更加普遍。把TDI源码里的异步状态机理顺了后面学什么驱动都事半功倍。第三用户态与内核态的交互手法。TDI驱动源码里通常有一堆配套的Win32应用源码它们通过DeviceIoControl下发命令。这个机制到现在还是Windows驱动的主要交互通道。你在TDI源码里看到的缓冲区模式METHOD_BUFFERED、METHOD_IN_DIRECT、METHOD_OUT_DIRECT选择今天依然一模一样。所以与其纠结“TDI过时了我还要不要学”不如把它当成一本活的教材去读。它的字节序处理、地址结构解析、链式结构体初始化都是内核编程的基本功操练场。最后再分享一个个人经验如果要在老系统上维护TDI驱动的兼容性千万别自己动手重写那些辅助构建IRP的宏。直接用原版WDK头文件里的宏虽然繁琐但那是经过无数驱动验证过的路径。我自己曾图省事用IoBuildDeviceIoControlRequest直接构造TDI请求结果在Windows XP和Windows Server 2003上表现不一致白折腾了一个星期。后来老老实实照着老代码的TdiBuildXxx系列重写问题全部消失。有时候前辈留下的“笨办法”恰恰就是最靠谱的办法。本文还有配套的精品资源点击获取
返回列表