FEATURED · 精选文章

ModbusTCP报文分析器自研实录:从Wireshark痛点到手写工具

发布时间 / 2026/9/14 5:04:51
来源 / 创域科博编辑部
栏目 / 资讯中心
ModbusTCP报文分析器自研实录:从Wireshark痛点到手写工具 废话不多说先亮个东西。最近手头一个项目里PLC和上位机之间的ModbusTCP通信时不时抽风Wireshark抓出来的包一堆看得眼睛快瞎了。逼急了我直接自己写了个ModbusTCP报文分析器专治各种报文看不懂、定位慢的毛病。今天就把从需求拆解、协议梳理、代码实现到实际排查问题的全过程原原本本分享出来。这个工具不是什么惊天动地的大项目但绝对实用。它解决的核心问题就一个当ModbusTCP通信异常时怎么快速从一堆报文里找到到底是谁先不按规矩出牌的。适合所有跟PLC、工业网关、电力仪表、楼宇自控设备打交道的工程师也适合正在学Modbus协议、想搞懂报文结构的同学拿来当参考。1. 为什么放着Wireshark不用非要自己写一个分析器先别急着喷“Wireshark它不香吗”。香真香但它解决不了我的全部问题。所以这个项目不是要干掉Wireshark而是补上它在特定场景下的短板。1.1 通用抓包工具的痛点Wireshark对ModbusTCP的支持其实已经不错了能解析出事务ID、功能码这些字段。但我实际用下来有几个很别扭的地方一是批量分析PCAP文件效率太低。现场有时候一抓就是好几个小时几个GB的pcap文件Wireshark打开就卡半天你只能一个包一个包地看想做个统计得自己写display filter很多工控工程师并不擅长这个。二是缺少针对Modbus业务层面的关联分析。比如一个请求和一个响应Wireshark能显示它们挨在一起但不会自动告诉你这个响应超时了、这个异常码对应什么错误含义、这个寄存器的读写频率是多少。这些恰恰是排查问题最需要的信息。三是格式化的导出和告警能力为零。Wireshark能把报文导成CSV或JSON但要把非法报文、异常响应、超时事务自动标出来你得写Lua脚本。说实话现场调试的时候真没这个精力。1.2 这个自研工具能做什么我给自己定的目标很明确做一个专门针对ModbusTCP协议的、带业务视角的报文分析器具备四个核心能力。第一实时抓包和离线解析两不误。既可以直连网卡抓实时报文也可以导入已有的pcap/pcapng文件做离线分析。第二报文按事务关联重排。把同一个Transaction ID的请求、响应、异常响应自动配对一眼看出哪个请求没得到应答、哪个应答超时了。第三功能码和寄存器区间解析。不只是显示“Read Holding Registers”还能解析出具体读了哪些地址、返回了多少个字节、每个寄存器的值是多少。配合寄存器地址表直接翻译成工程含义。第四一键生成统计报告。按从站地址、功能码、错误码这几个维度做统计问题点一秒定位。这套功能组合下来我现场排查一个通信故障的时间从以前的半小时缩短到了五分钟以内。2. ModbusTCP报文结构拆解不知道协议就没法写分析器任何报文分析器的核心都是协议解析。写这个工具之前我先把ModbusTCP的报文结构彻底捋了一遍这里也给读者做个梳理。别嫌基础后面代码解析全靠这几个字段。2.1 MBAP头七个字节的身份证ModbusTCP报文和串口ModbusRTU最大的区别就是有MBAP头Modbus Application Protocol Header。它一共7个字节四个部分事务处理标识符Transaction Identifier2字节。客户端每次发起请求时生成一个标识服务端响应时要原样返回。用于匹配请求和响应。协议标识符Protocol Identifier2字节。对于Modbus协议来说恒为0x0000非0就说明不是Modbus报文。长度Length2字节。指的是后面单元标识符、功能码、数据加起来的字节数。单元标识符Unit Identifier1字节。相当于以前RTU里的从站地址用来区分同一个网关后面挂的不同设备。我建了个模型在代码里直接对应一个结构体。解析的时候第一件事就是把这7个字节分出来长度字段错了后面全乱套。2.2 功能码与数据段真正干活的字段MBAP头后面就是功能码和数据段。功能码1字节决定了这条报文是干什么的常见功能码对照如下功能码名称方向典型用途0x01Read Coils请求/响应读线圈状态0x02Read Discrete Inputs请求/响应读离散输入0x03Read Holding Registers请求/响应读保持寄存器0x04Read Input Registers请求/响应读输入寄存器0x05Write Single Coil请求/响应写单线圈0x06Write Single Register请求/响应写单寄存器0x0FWrite Multiple Coils请求/响应写多个线圈0x10Write Multiple Registers请求/响应写多个寄存器0x17Read/Write Multiple Registers请求/响应先读后写数据段的格式跟功能码强相关。比如0x03读保持寄存器请求数据段是“起始地址2字节 寄存器数量2字节”响应数据段是“字节数1字节 寄存器值N字节”。解析器必须按功能码分支处理否则读到的寄存器值全是错位的。这里有个细节异常响应时功能码会把最高位置1。比如0x03的请求如果出错服务端返回0x83后面跟一个异常码字节。异常码01是非法功能码02是非法数据地址03是非法数据值04是设备故障常见的就是这几个。分析器最好把这些都翻译成人话别让用户自己去翻手册。2.3 字节序问题大端还是小端这是个要命的问题Modbus协议规定寄存器值都是大端字节序高字节在前低字节在后。比如从地址100读到的两个字节是0x12和0x34合并出来就是0x1234十进制4660。但实际工程里很多设备不按常理出牌有的寄存器存的是32位浮点数两个字组合并时要考虑字节序和字序有的设备内部是小端存储需要转换后才是真实值。所以分析器解析显示寄存器值时要提供“大端/小端”“字序AB/BA”的切换选项否则你看到的数值跟PLC里的对不上会怀疑人生。这个设计后来救了我大命。有一回调试现场PLC里明明是500.0报文里寄存器值解析出来却成了一个大得离谱的数切换成小端模式后一切正常——问题根本不在通信层在设备的寄存器字节序配置上。3. 工具整体设计与技术选型C# SharpPcap项目定了要做技术选型就得先拍板。我当时纠结过Python和C#最后选了C# .NET 8 WPF SharpPcap的组合理由后面展开说。3.1 为什么选C#而不是PythonPython写协议解析确实快scapy库加持下十几行就能解析一个包。但我这个工具有个硬需求要做成Windows桌面程序给现场工程师用得能双击打开、能连网卡、能在界面里操作。Python打包成exe总有一堆环境问题界面做起来也蛋疼。C#的WPF开发桌面应用是成熟路线打包成单文件也就几十MB拷到现场电脑上直接跑。抓包库方面SharpPcap是对libpcap的.NET封装接口清晰文档齐全支持实时抓包和离线读取pcap文件完美契合我的需求。界面用WPF还有一个隐性好处数据绑定方便。报文列表和详细解析视图做成两个GridView绑定到同一个ViewModel后台更新数据源界面自动刷新省掉大量手动更新UI的代码。3.2 整体架构三大模块各司其职这个分析器不是一个小脚本而是分了三层抓包采集层基于SharpPcap实现负责实时抓包和pcap文件读取。实时抓包时设置网卡为混杂模式保证能看到所有经过的ModbusTCP报文。这层输出统一的Packet对象包含时间戳、源IP、目的IP、源端口、目的端口和原始字节。协议解析层这是核心负责把原始字节流拆成MBAP头、功能码、数据段三层结构再按功能码进一步解析寄存器读写请求和响应最后做事务关联和异常检测。界面展示层WPF实现的报文列表、报文详情、统计面板、过滤条件四个区域。这一层不处理业务逻辑只管展示。这三层之间用接口隔离抓包层换一种实现比如改用ETW抓包解析层和界面层完全不用动。3.3 关键数据结构设计报文解析的结果我定义了一个ModbusTcpPacket类核心字段如下public class ModbusTcpPacket { public DateTime Timestamp { get; set; } public string SrcIp { get; set; } public string DstIp { get; set; } public int SrcPort { get; set; } public int DstPort { get; set; } public ushort TransactionId { get; set; } public ushort ProtocolId { get; set; } public ushort Length { get; set; } public byte UnitId { get; set; } public byte FunctionCode { get; set; } public bool IsResponse { get; set; } public bool IsException { get; set; } public byte ExceptionCode { get; set; } public Listushort StartAddresses { get; set; } public Listushort Quantities { get; set; } public Listbyte[] RegisterValues { get; set; } }事务关联的逻辑也很简单用Transaction ID 源IP 目的IP 功能码四元组做Key请求进来时存到字典里响应进来时配对取出并计算耗时。超时的请求单独标红显示。4. 核心功能实现抓包、解析、模拟器三步走下面进入正题把几个核心功能的实现思路和关键代码讲一遍。这个工具最核心的价值就藏在这些实现细节里。4.1 实时抓包混杂模式与过滤规则实时抓包这块SharpPcap的使用非常直接。先获取网卡设备列表选一个设置过滤器只放行TCP端口502的包然后开启抓包循环var devices CaptureDeviceList.Instance; var device devices[selectedIndex]; device.OnPacketArrival OnPacketArrival; device.Open(DeviceModes.Promiscuous, readTimeout: 1000); device.Filter tcp port 502; device.StartCapture();有个注意点很多PLC通讯并不默认走502端口尤其是一些网关设备可以自定义端口。所以过滤器的构建要做成“端口可配置”的默认502但用户可以在界面上填别的端口。我甚至加了“不过滤任何端口”的模式用户先抓全量流量再看协议识别结果。混杂模式这个词听起来玄乎其实就是让网卡接收所有经过的数据帧而不是只收发给自己的。调试的时候抓到的不只是自己电脑和PLC之间的流量还有交换机上其他设备的流量——这也是为什么过滤器必须加否则报文列表直接被淹了。4.2 离线pcap解析几GB的文件也不怕离线解析功能说白了就是读文件里的包逐个喂给解析器。但几GB的pcap直接全部加载到内存程序必挂。我采用的方案是流式读取延迟解析using var capture new CaptureFileReaderDevice(filePath); capture.OnPacketArrival (sender, e) { var rawPacket e.GetPacket(); // 立即放入队列UI线程批量刷新 _packetQueue.Enqueue(rawPacket); }; capture.Open(); capture.StartCapture();原始包先塞进并发队列UI线程定时从队列取一批包解析并刷新界面。这样即使有5万个包界面也不会卡死首屏打开速度取决于文件头部数据量而不是整个文件解析时间。另外离线解析意味着可以做批处理分析。我加了一个隐藏功能命令行模式下传入pcap路径工具自动跑完整分析输出一份CSV报告。这样现场抓完包一个命令就能出报告不用打开界面一顿点。4.3 报文模拟器不接设备也能验证分析逻辑这个功能是后加的但我觉得特别值。调试分析器本身总不能每次都跑现场找PLC吧所以我在工具里内置了一个ModbusTCP从站模拟器和一个主站报文生成器从站模拟器监听502端口收到请求后按功能码返回对应的响应报文。寄存器数据随机生成方便测试解析是否正确。主站生成器可以手动构造请求报文指定事务ID、功能码、起始地址、数量、寄存器值一键发送。也可以选择批量连续发送测试高并发场景下的解析和关联。有了这个我写解析代码的时候不需要真实设备本地就能自测。测试方法也很简单主站生成器发一条读保持寄存器请求从站模拟器回响应分析器抓包解析对比界面显示的值和发出去的值是否一致。这也是我给读者的一条建议写协议相关工具一定先准备一个可控的报文源别拿现场设备当测试机出了事故说不清。4.4 报文详情视图你以为你懂了其实你还得会看报文列表告诉你有问题详情视图告诉问题出在哪。详情视图我做了两级展示第一级是结构化字段树把MBAP头、功能码、数据段的每个字段逐行列出字段名、值、说明三列。比如功能码0x03就显示“读保持寄存器Read Holding Registers”寄存器值自动以十进制和十六进制双显示。第二级是原始字节视图十六进制和ASCII对照显示。结构化字段树和原始字节视图支持联动点击字段树里某个字段原始字节视图里对应的字节会高亮。这个联动调试时极有用能快速确认解析器有没有算错偏移量。寄存器值这块还有个小功能解析0x03/0x10这类寄存器读写时把数据区按寄存器大小分组自动计算每个寄存器的值。计算规则支持三种16位无符号、32位浮点ABCD/CDAB两种字节序、32位无符号。选择不同规则数值实时刷新方便对照PLC里的数据类型。5. 实操案例一次寄存器数据错乱的排查实录光说功能太干了拿一个我实际用这个工具解决的真实问题来演示。某个项目现场新接入一批温湿度传感器上位机读到的温度值偶尔会跳变读数从25度直接变成上千。5.1 现场现象还原上位机每5秒轮询一次这批传感器的保持寄存器地址范围是0到20每台设备存温度寄存器0、湿度寄存器1、状态字寄存器2。温度字段在PLC里配置的是32位浮点占两个寄存器所以实际点位地址是0-1和2-3这样的关系。问题出现得很随机有时候一天出现一两次有时候一小时好几次。一开始怀疑是线路干扰查了屏蔽线、接地都没问题怀疑是传感器本身故障换了新设备一样跳。最后决定抓包看看到底数据从哪一步开始错。5.2 抓包定位过程用我这个分析器连续抓了大概40分钟报文列表里果然出现了一条异常有一条请求读地址0到3的四个寄存器响应的数据字节数长度却是6按解析规则读出来是三个寄存器值但请求要的是四个。我当时第一反应是长度字段出问题了。点开详情视图一看MBAP头的Length字段值是11按说法应该是“单元标识符1字节 功能码1字节 数据6字节 应答不对长度字段这个值是0x0011”。再往下看那条响应的Transaction ID和请求能对上但源IP、目的IP都反了看起来像是某个中间设备悄悄插入了一段垃圾数据。后来查了半天真相是传感器的RS485转ModbusTCP网关固件有bug在某种时序下会把上一次的响应残留字节拼进下一次响应里导致响应长度变长。这个案例说明一个道理协议分析器不只是给你看报文更要帮你把报文之间的关联关系、时间线、数据异常显式地呈现出来。如果靠自己肉眼看十六进制短时间根本发现不了长度字段偶尔多一个字节的问题。5.3 统计面板如何辅助快速定位问题定位太快有时候不是因为单条报文看得多明白而是统计面板直接把重点圈出来了。我的工具在抓包结束后会自动生成一份统计按从站地址分组的请求/响应数量各功能码的请求次数和响应成功率异常响应码的分布响应耗时的最大值、最小值、平均值未配对请求和未配对响应的数量那次排查里“未配对请求”这个数字很不正常有17条请求找不到对应的正常响应都是超时重发。点一下“未配对请求”过滤报文列表立刻只剩这些异常记录再逐条看详情异常规律一下就浮出来了。所以说排查效率高不是靠分析器有什么人工智能而是它把“找异常”这件事变成了“点一下”的事人力只需要做最终确认。6. 常见问题与排查技巧实录工具写完不代表就能用好我自己踩了不少坑。这些经验比工具本身更有价值整理成问题列表读者用Wireshark或者其他分析工具时同样适用。6.1 抓不到包权限和混杂模式逐个查常见的是抓包设备选对了但一条报文都抓不到。排查顺序我从实际经验里总结了三条第一确认网卡驱动支持混杂模式。虚拟机和部分USB网卡的驱动对混杂模式支持很差抓不到包很正常。物理机的板载网卡Intel、Realtek主流型号基本没问题。第二确认Windows防火墙没有拦掉本地程序。SharpPcap用的npcap驱动有时候会被Windows防火墙拦截首次运行时的弹窗一定要点“允许访问”。我自己就吃过这个亏程序死活抓不到包最后发现是防火墙把npcap的驱动服务禁了。第三确认报文走的确实是你选的那块网卡。现场电脑不只有一个网卡可用如果PLC是走工业网卡通信的你选了一个WiFi网卡自然是空的。抓包前打开网络连接看一眼确认一下哪块网卡的IP和PLC在同一网段。6.2 报文解析出现半个包TCP粘包与半包处理ModbusTCP走的是TCP流TCP协议本身只保证字节顺序不保证报文边界。数据量大的时候两个Modbus报文可能粘在同一个TCP段里反过来一个大的响应也可能被拆成多个TCP段传输这就出现了“半包”。Wireshark比较聪明会按照协议层重组流。但自己写解析器时这个坑绕不过去。我的解决办法是维护一个TCP流缓冲从抓包层拿到TCP载荷后先追加到缓冲区然后循环尝试从缓冲区里按MBAP头中的Length字段完整剥离出一个个报文剥不出来的就留在缓冲区等下一个TCP段到达。private byte[] _streamBuffer new byte[0]; private IEnumerablebyte[] TryDecodePackets(byte[] payload) { _streamBuffer _streamBuffer.Concat(payload).ToArray(); var packets new Listbyte[](); while (_streamBuffer.Length 7) { int length (_streamBuffer[4] 8) | _streamBuffer[5]; int totalLength 6 length; // MBAP头前6字节 Length字段值 if (_streamBuffer.Length totalLength) break; packets.Add(_streamBuffer.Take(totalLength).ToArray()); _streamBuffer _streamBuffer.Skip(totalLength).ToArray(); } return packets; }这段代码就是个标准的长度字段剥离法等下有个细节需要说明长度字段的值是“单元标识符往后”的字节数所以总长度的计算要小心别把前6个字节算漏了。这个逻辑搞错所有报文解析都会错位。6.3 时间戳对不齐、事务ID重复怎么办现场抓包发现请求和响应关联不上第一反应是Transaction ID重复了。有些老设备的协议栈实现有bug事务ID要么恒为0要么溢出后重复用。我在事务关联逻辑里做了折中先用事务ID匹配匹配不上的再退化成“同IP同功能码来源去向相反时间接近”的模糊匹配。这样做的副作用是有可能错配所以我加了置信度标识用事务ID精确匹配的记录标记“高置信度”模糊匹配的标记“低置信度”。用户看到低置信度记录时就得多留个心眼可以按时间戳手动核对。另外一个坑是时间戳精度。实时抓包用的是系统时间毫秒级离线解析pcap时用的是文件里记录的微秒级时间戳。同一个工具里两种精度混着显示排序的时候偶尔会出现顺序错乱。我的处理是列表里统一显示毫秒排序时用内部的高精度时间字段确保一致性。6.4 几个写给自己的避坑清单解析MBAP头时先把TCP流缓冲处理干净再解析顺序不能反。功能码0x05写单线圈请求数据是“地址2字节 值2字节”值只有0xFF00开和0x0000关两种合法读到别的值直接标记非法。读寄存器请求的“数量”字段最大是0x007D125超过这个值的请求要么是厂商扩展要么是非法报文需要注意区分。异常响应里报文标识符功能码最高位是1判断逻辑用(functionCode 0x80) ! 0最稳妥。统计耗时用响应时间减请求时间记得排除掉跨事务ID的干扰。界面刷新的批量更新频率定为500ms一次比较合适太快界面卡顿太慢看起来不实时。7. 还在做的两个扩展方向这个工具现在基础功能已经完整了但我手头还留了两个待办写出来给想做类似工具的朋友一些启发。一个是自动生成测试脚本。现在报文模拟器能手动构造请求但构造复杂场景还是不够方便。下一步我打算根据抓到的异常报文反向生成自动化测试序列批量验证设备对不同异常报文的响应是否符合预期。说白了就是让分析器本身成为一个测试用例的生成器省掉手工构造的功夫。另一个是导出一份对齐现场的点位表。现在寄存器值解析出来工程师还得自己对表找含义。如果能把Excel格式的点位表导进工具把地址和工程名称对应起来抓包结果里直接显示“温度25.3℃”而不是“寄存器0: 0x41CA6666”实用性会再上一个台阶。这两个功能做出来之后这个工具就从一个单纯的“报文分析器”进化成“通信调试助手”了。这一行就是这样工具永远不嫌顺手晒起来也没完没了。但说到底自己动手写工具的过程收获最大的不是工具本身而是对协议的理解、对问题排查方法的品味全在敲下的每一行代码里。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻