首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
逆向工程
发新帖
3
2
[原创]C#实现西门子S71200与S71500控制器优化DB块符号访问
发表于: 2026-9-4 16:58
6088
[原创]C#实现西门子S71200与S71500控制器优化DB块符号访问
Coder_shen
2026-9-4 16:58
6088
# C#实现西门子S71200与S71500控制器优化DB块符号访问 > 本文基于 S7CommPlusDriver 代码库撰写,完整梳理从协议栈、符号寻址算法(SymbolCrc + LID)、在线符号浏览,到「离线工程浏览与在线浏览无缝统一」的架构设计与验证方法。文中所有数字与示例均来自真实 S7-1500 PLC 与真实博途工程的对拍测试。 --- ## 目录 1. [背景:优化 DB 块符号访问为什么难](#一背景优化-db-块符号访问为什么难) 2. [整体架构](#二整体架构) 3. [连接建立与安全通道](#三连接建立与安全通道) 4. [符号寻址核心:ItemAddress / LID / SymbolCrc](#四符号寻址核心itemaddress--lid--symbolcrc) 5. [在线符号浏览:PObject 契约与按需懒加载](#五在线符号浏览pobject-契约与按需懒加载) 6. [在线/离线一体化:一个函数让博途工程变成「虚拟 PLC」](#六在线离线一体化一个函数让博途工程变成虚拟-plc) 7. [离线解析器 TiaProjectParser](#七离线解析器-tiaprojectparser) 8. [多 PLC 工程支持](#八多-plc-工程支持) 9. [类型边界:Variant 叶子](#九类型边界variant-叶子) 10. [验证体系:四层对拍门禁](#十验证体系四层对拍门禁) 11. [快速上手](#十一快速上手) 12. [结语与获取方式](#十二结语与获取方式) --- ## 一、背景:优化 DB 块符号访问为什么难 ### 1.1 从绝对寻址到符号寻址 经典 S7-300/400 通信(如 S7.NET 生态)依赖**绝对地址**:`DB1.DBX0.0`、`M10.2`。但 S7-1200/1500 的博途工程默认给 DB 块开启**「优化的块访问」(Optimized Block Access)**,其根本变化是: - 变量在 PLC 内存中**不再有固定的字节偏移**,编译器可以自由重排、合并、按位打包; - 传统按偏移读写的报文(PEAK/POKE 式)对优化块**完全失效**——商用库 AGL 对优化块返回的偏移统一是 `-1`,这正是「没有偏移」的标准语义(`AGL40_SYMBOLIC_DATABLOCK_IS_OPTIMIZED`); - 唯一稳定的寻址方式是**符号寻址**:用「符号名 + 类型」算出的 **32 位符号 CRC** 加上逐层解析得到的 **LID(逻辑索引目录)访问序列** 来定位变量。 这带来两个技术门槛:其一,符号 CRC 与 LID 的算法需要逆向;其二,符号名只是入口,最终要拿到 PLC 内部的类型树(结构体/数组/UDT 的每一层),才能把 `"DB1".Motor.Speed[2]` 这样的字符串翻译成合法的访问地址。 ### 1.2 为什么用开源方案 S7CommPlus 协议(S7-1200/1500 使用的通信协议,区别于经典 S7 协议)从未公开,官方只有闭源商业库(AGLink40)。S7CommPlusDriver 是一个开源 .NET 实现: - 协议栈上游基于 <a href="elink@171K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6@1K9r3!0E0j5i4y4Q4x3X3c8$3x3W2)9J5c8W2x3%4b7$3!0E0L8g2m8D9N6i4y4p5M7X3W2$3k6i4t1`.">thomas-v2/S7CommPlusDriver</a>(LGPL 许可,感谢 Thomas Wiens 的协议逆向工作); - 非 TLS 认证算法基于 <a href="elink@259K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6T1L8$3&6C8i4K6u0V1k6r3g2$3i4K6u0r3d9r3q4J5M7r3!0e0y4H3`.`.">bonk-dev/HarpoS7</a> 的逆向实现; - 本项目在协议栈之上完成了大量工程化工作:真实 PLC 全量符号对拍、性能优化(密钥轮换微秒级)、以及本文重点的**离线工程浏览与在线浏览无缝统一**。 > ⚠️ **重要声明**:驱动仍处于开发与测试阶段,协议实现可能存在未知缺陷,请勿用于生产环境。 ## 二、整体架构 解决方案 `src/S7CommPlusDriver.sln` 共 7 个项目: ``` S7CommPlusDriver.sln ├── S7CommPlusDriver/ # 核心协议库(net4.0 起,C# 7.3) │ ├── Core/ # S7CommPlus 协议对象模型(PObject、DatablockInfo、 │ │ # Softdatatype、PValue、PLCInformation…) │ ├── ClientApi/ # 客户端 API │ │ ├── Browser.cs # 符号树浏览(在线) │ │ ├── OfflineSymbolSource.cs # 离线符号源(博途工程"虚拟 PLC"引擎) │ │ ├── PlcTag.cs / PlcTags.cs # 标签抽象 + ReadTags/WriteTags 封装 │ │ └── ItemAddress.cs # 变量寻址结构(SymbolCrc + LID) │ ├── Internal/ # S7CommPlusSymbolCrc(符号 CRC 算法)等 │ ├── Net/ # 传输层(S7Client、MsgSocket) │ ├── OpenSSL/ # TLS P/Invoke 层(OpenSSL 3.x,x86/x64 双实现) │ ├── Legitimation/ # 密码合法化认证 │ ├── Alarming/ # 报警处理 │ ├── Subscriptions/ # 订阅管理 │ ├── MmTimer.cs # 高精度多媒体定时器(驱动密钥轮换) │ └── S7CommPlusConnection.cs # 主连接入口(唯一对外门面,约 2600 行) ├── HarpoS7Wrapper/ # 非 TLS 认证/加密 P/Invoke 封装(C#) ├── HarpoS7Native/ # 非 TLS 认证算法 C++ 原生实现 ├── S7CommPlusGUIBrowser/ # WinForms 浏览器演示程序 ├── DriverTest/ # 无头功能测试 + 对拍验证工具 ├── TiaProjectParser/ # 博途工程离线解析器 └── Zlib.net / TestHarpoS7Native # 依赖与原生库基准测试 ``` 三个值得注意的设计决策: **1. 统一连接门面。** `S7CommPlusConnection` 承载全部对外 API——`Connect/Disconnect`、`ReadValues/WriteValues`、`Browse`、`getTypeInfoByRelId`、`getPlcTagBySymbol`、`GetListOfDatablocks`、`SetPlcOperatingState`、`GetCommentsXml`、以及后文的主角 `LoadSymbolsFromFile`。上层(GUI、测试、用户代码)只见一个类。 **2. 真 AnyCPU。** 早期版本靠 x86/x64 两套编译配置 + `_WIN64` 条件编译硬编码原生 DLL 名,需要两次编译出两个包。改造后同一份编译产物按 `IntPtr.Size` 运行时分发:`HarpoS7Wrapper` 与 `OpenSSL` 都采用 `Impl64`/`Impl32` 嵌套静态类模式(`DllImport` 要求常量 DLL 名,故外层公开方法在运行时转发到对应实现),64 位 OS 自动加载 x64 原生库、32 位 OS 自动加载 x86。 **3. 协议版本矩阵。** 协议实现覆盖 ProtocolVersion V1/V2/V3;TLS 加密连接与非 TLS 路径按连接参数自动选择。 ## 三、连接建立与安全通道 建立会话是一个多步骤握手过程(简化描述): ``` TCP 连接 → 会话协商(Hello/Version) → 认证(TLS 或非 TLS 两条路径) → 会话密钥协商 → 登录(可选密码合法化) → 通信资源协商 → 读取 PLC 信息(型号/MLFB/固件/PLC 名) → 可开始读写与浏览 ``` ### 3.1 非 TLS 认证:HarpoS7 非 TLS 路径分 **PlcSim**(软 PLC)与 **RealPlc**(S7-1200/1500 实机)两套认证算法,核心原语包括: - **GF(2^128) LUT 变换**:4096 字节查找表,生成时涉及既约多项式 `x^128 + x^7 + x^2 + x + 1` 的常量拆分; - **Monolith 布尔电路变换**(自动生成的位操作代码); - **大整数压缩(BigIntCompressor)**:`overflow × 0x2F` 的 256 位乘法与低位对齐存储。 这些算法先在 C# 上逆向实现并真机验证,随后**移植为 C++ 原生库**以换取性能。为保证移植逐字节等价,配套了一套自动对拍工具链:**22 个 API 测试用例(认证 8 / 合法化挑战 4 / 密钥导出 2 / 摘要 2 / 密钥存储 3 / 指纹识别 3)与 C# 参考实现逐字节比对,22/22 全部通过**。 对拍的价值在移植过程中被两次实锤: 1. **大整数压缩存储位置错误**:`harpo_bigint_compress()` 把 `overflow * 0x2F` 的乘积写到了 `buffer[5]` 之后的位置,而 C# `BigInteger.Multiply` 从低位 dword(`buffer[0]`)起填充——压缩结果不一致,直接影响种子变换; 2. **LUT 生成器常量参数顺序错误**:`make128(0x00008005, 0, 0x00000001, 0)` 把 64 位 XOR 常量 `0x01_0000_8005` 错误拆到 lo/hi 两个 qword 上,导致 4096 字节 LUT 从第 2 项起全体错误,**PLC 直接拒绝认证**(校验和与 PLC 不一致)。 这两个 bug 都属于「算法正确、细节错位」类型,只有逐字节对拍能抓出来——这是协议实现工作的核心方法论。 ### 3.2 密钥轮换 连接生命周期内会话密钥每 **30 分钟**轮换一次,由高精度多媒体定时器驱动。早期每次轮换都重新生成密钥材料并执行完整变换(PlcSim ~0.1 ms、RealPlc ~4.5 ms),改造为**密钥材料复用(save-once-reuse)**后——同一连接内复用 seed/IV、仅 challenge 变化——轮换耗时压缩到 **20 μs 以内**(PlcSim 路径 14 μs,加速 7.6 倍)。轮换流程还对齐了商用库语义:challenge(GetVarSubStreamed)与 SetVariable 共用同一个 SequenceNumber,不自增。 ## 四、符号寻址核心:ItemAddress / LID / SymbolCrc 这是本文主题「优化 DB 块符号访问」的核心。一次变量访问最终要落到一个 `ItemAddress` 上: ### 4.1 ItemAddress 结构 ```csharp public class ItemAddress : IS7pSerialize { public UInt32 SymbolCrc; // 符号 CRC(见 4.3) public UInt32 AccessArea; // 访问区域:DB 块 = 0x8A0E0000 + 块号 public UInt32 AccessSubArea; // 子区域:DB_ValueActual / ControllerArea_ValueActual public List<UInt32> LID; // 逻辑索引目录(逐层成员索引) } ``` 序列化按 `SymbolCrc → AccessArea → LID 个数+1 → AccessSubArea → LID[]` 顺序,全部用 VLQ(变长量)编码进请求报文。 - **AccessArea**:`SetAccessAreaToDatablock(number)` 即 `0x8A0E0000 + 块号`;I/Q/M/T/C 区则用各自的 NativeObjects rid(`theIArea_Rid` 等); - **访问串**:`GetAccessString()` 输出十六进制点分形式,如 `8A0E0001.78.F`——`8A0E0001` = DB1,`.78`、`.F` 是两层成员的 LID。解析函数 `new ItemAddress("8A0E0001.A")` 支持反向构造。 ### 4.2 LID 访问序列:符号名如何翻译成地址 `getPlcTagBySymbol("\"DB1\".Motor.Speed")` 的翻译过程是一个**逐层递归**: ``` parseSymbolLevel() 取出第一层 "DB1" → 命中块 browsePlcTagBySymbol(ti_relid, symbol, varInfo): 1. getTypeInfoByRelId(ti_relid) 取该层 PObject 2. parseSymbolLevel() 取出下一层名(引号/点号/下标三段式解析) 3. 在 VarnameList.Names 中定位成员 → 得到 PVartypeListElement 4. varInfo.AccessSequence += "." + varType.LID ← 逐层追加 LID 5. 同时把该层 (名, 类型) 记入 SymbolCrcPath 6. 若成员是数组 → 展开下标(见下) 7. 若成员是结构/数组元素 → 递归进入下一层 ``` 数组下标展开有专门的算法:布尔数组元素数向上对齐到 8 的倍数(PLC 按位打包);行主序的 `dimSize` 累乘把多维下标 `[i,j]` 折成线性索引;数组元素若是结构体,访问序列末尾**追加 `.1`**。最终 `"DB1".Motor.Speed[2]` 可能翻译成 `8A0E0001.78.F.2`——每个数字都是协议层真实的逻辑索引。 ### 4.3 符号 CRC 算法 符号 CRC 是优化块的「变量身份证」。驱动实现(`Internal/S7CommPlusSymbolCrc.cs`)与西门子公开资料中 S7-1500 符号寻址的算法一致: - CRC32,多项式 **`0xF4ACFB13`**,MSB-first,初始值 0,无结果异或; - **单段**(一个成员名):`inner = CRC32(UTF8(名字) + TypeCode字节)`,最终值 `= CRC32(inner 按 LE32 写入)`; - **数组段**:`inner = CRC32(UTF8(名字) + 0x10(数组类型码) + 元素TypeCode + 下限(LE32))`; - **多段路径**:`CRC32(inner0 | 0x89 | inner1 | … | innerN)`——层与层之间插入结构子分隔字节 `0x89`; - **TypeCode 映射**:`BBOOL → BOOL(1)`;`AOMIDENT/EVENTATT/HWINT 等 → DWORD(6)`;`HWANY/HWIO 等 → WORD(4)`;`OB* → INT(5)`;`PORT/RTM/DBANY 等 → UINT(6)`;其余类型用自身枚举值。 按驱动算法实际计算的例子(可直接用驱动复现): | 符号 | 计算路径 | SymbolCrc | |---|---|---| | `Speed` : REAL | 单段,TypeCode=8 | `0x888EC040` | | `MotorSpeed` : REAL | 单段,TypeCode=8 | `0xA132E807` | | `Motor.Speed` : REAL.REAL | 两段,`0x89` 分隔 | `0x37AC4EA5` | | `Setpoints` : Array[0..9] of REAL | 数组段(0x10 + 8 + 下限0) | `0x521E174E` | 一个微妙但关键的点:**含数组元素下标的路径 CRC 恒为 0**(`ContainsIndexedArray → SymbolCrc = 0`)——元素定位信息已经完整编码在 LID 里,CRC 只对「无下标」的符号路径负责。 ### 4.4 批量读写 `ReadValues` 的完整流程: 1. 校验许可证 → 初始化返回值与错误列表(错误初值 `0xFFFFFFFFFFFFFFFF`); 2. **按 `TagsPerReadRequestMax` 分块**(PDU 尺寸感知,长列表自动切多请求); 3. 每块构造 `GetMultiVariablesRequest`——**TLS 或安全连接用 ProtocolVersion V2,普通连接用 V3**; 4. 发送并等待响应 → `GetMultiVariablesResponse.DeserializeFromPdu` → `checkResponseWithIntegrity` 完整性校验; 5. 逐项回填值与错误码(单变量失败不影响同批其它变量,`ReturnValue` 非 0 时错误明细在 ErrorValues 里)。 `WriteValues` 走 `SetMultiVariablesRequest`(AddressListVar + ValueList),同样分块、同样完整性校验。上层封装 `PlcTags.ReadTags(conn, tags)` 让调用方只面对 `PlcTag` 抽象。 ## 五、在线符号浏览:PObject 契约与按需懒加载 ### 5.1 浏览流程 ``` Browse(out varInfoList) ├── 探索 PLC 对象树(OMS/OMM):PLCProgram(ExploreParents=1) │ ├── 递归收集 Failsafe 层级(FailsafeRuntimeGroup 8157 / FailsafeControl 7842) │ │ └── 下的 DB 块(ClassId=4600=MC_DB_Class_Rid,安全型 DB) │ └── ObjectOMSTypeInfoContainer 一次性取得全部类型信息 │ ├── 存入 Browser.m_objs(浏览缓存) │ └── 同步填充 typeInfoList(getTypeInfoByRelId 缓存,见下) └── 逐块/逐区展开全部叶子 → VarInfo{Name, AccessSequence, Softdatatype, SymbolCrcPath, ContainsIndexedArray} ``` ### 5.2 PObject 契约:每展开一层取一层 浏览模型围绕一个核心契约——**`getTypeInfoByRelId(relid) → PObject`**,给定关系 ID 返回**当前一层**的成员清单: ``` PObject ├── VarnameList.Names[] # 本层成员名(数组父只占一个条目) └── VartypeList.Elements[] # 每个成员的类型形态(OffsetInfoType): ├── POffsetInfoType_Std # 叶子(标准变量) ├── POffsetInfoType_Struct # 结构成员 → RelationId 指向下一层类型 ├── POffsetInfoType_Array1Dim # 一维数组 {下限, 元素数, RelationId} ├── POffsetInfoType_ArrayMDim # 多维数组 {各维下限/元素数, RelationId} └── POffsetInfoType_FbArray # FB 实例数组 ``` GUI 树每展开一层,就用节点上挂的 relid 调一次 `getTypeInfoByRelId`,按元素形态生成下一层节点——**与 PLC 的真实交互模型一致:每展开一层取一层**。配合 Browse 阶段的类型信息缓存(`typeInfoList`),后续展开零网络请求。这里曾有一个隐蔽 bug:Browse 只把类型信息存进 Browser 内部缓存、`typeInfoList` 恒空,导致 GUI 每次展开都实时向 PLC 发 ExploreRequest——网络抖动即抛 "Could not get type info";修复后展开全部走本地缓存。 浏览正确性还修过一批:Failsafe 安全型 DB 完全缺失(PLC 在 `ExploreParents=0` 时不返回 Failsafe 子树)、FB 实例数组不展开(`Is1Dim()/IsMDim()` 被标 TODO)、嵌套 UDT/FB/Struct 子节点静默丢失(新增 TypeInfoFetcher 按需回源)、类型信息里空名称条目污染节点列表(SanitizeVarnameList 倒序清理)。 ## 六、在线/离线一体化:一个函数让博途工程变成「虚拟 PLC」 ### 6.1 问题 早期版本里离线浏览博途工程(.apXX 归档或工程目录)与在线浏览是**两套完全不同的代码**:离线有独立的建树/展开/选中/路径拼接分支;展开一个块就把整棵子树一次转完(ConvertOfflineNode 全递归)、展开任一个区就一次走完全部标签表、启动时还要后台预热全部叶子;树根还额外包了一层项目名。用户操作繁琐,体验与在线割裂。 ### 6.2 设计:`LoadSymbolsFromFile` 单入口 改造目标一句话:**S7CommPlusDriver 只增加一个公开函数 `LoadSymbolsFromFile(工程路径)`,调用之后浏览符号变量树、GUI 构建变量树与在线方式完全一致;旧用户从不调用它,行为零变化。** ```csharp var conn = new S7CommPlusConnection(); // 加载博途工程(目录或 .apXX 归档),连接对象从此成为"虚拟 PLC" int res = conn.LoadSymbolsFromFile(@"D:\Projects\tiaProject18"); // 传 null 即卸载工程,回纯在线浏览 conn.LoadSymbolsFromFile(null); ``` 实现上,连接对象持有 `OfflineSymbolSource _offlineSymbols`;当它非空时,四处浏览 API 在**许可证检查之前**路由到离线源,未命中回落在线: | API | 离线路由行为 | |---|---| | `GetListOfDatablocks` | 返回工程内 DB 块清单(映射为 DatablockInfo),不发任何网络请求 | | `getTypeInfoByRelId` | 查离线 relid 注册表,命中即返回 PObject;未命中回落在线(支持「离线树 + 在线补充」混合场景) | | `Browse` | 返回离线全量叶子清单 | | `getPlcTagBySymbol` | 先做离线符号解析(树走查 + 对拍叶缓存 CRC),成功即返回 PlcTag;失败回落在线 | 由此自然成立四个特性: - **GUI 零分支**:`S7CommPlusGUIBrowser` 删掉全部离线专用代码(负载类、独立展开/选中/路径拼接、全叶预热),在线/离线共用同一套 `BuildProjectTree()` / `AfterExpand` / `AfterSelect` / `btnRead`——离线树只是在线树的另一个数据源。**对旧用户无感**是硬指标,不是口号。 - **离线浏览 + 在线读值闭环**:离线打开工程 → `Connect` 真实 PLC → 选中离线树的叶子直接 `Read` 出值。`Connect/Disconnect` 不清离线源:断开只影响在线读取、树保留。 - **树根统一从 PLC 名开始**:在线连接第 8 步已读取 PLC 名,现存入 `PLCInformation.Name`(5 字段:Name/PLCFamily/PLCType/Firmware/MLFB);离线从工程 PLC 信息镜像同一组字段。GUI 树根 = PLC 名,**不再外面包一层项目名**。 - **一个连接对象贯穿始终**:btnConnect 复用已有 conn(不 new),不丢已加载的工程。 ### 6.3 真正的按需懒加载 离线侧的关键约束:**不能为了建树而全量解析工程**。TiaProjectParser 打开一个大型 V19 工程会产出 14 万行符号,全量展开在内存与启动时间上都无法接受。改造后的引擎: 1. **`Parse()` 去掉 `ParseAllObjects` 全量解析**:只做 Open(打开存储容器)+ ExtractAll(抽取符号表与块接口元数据)+ 块清单扫描(`FindDataBlocks` 只读块的存储对象列表,不展开接口);PlcInfoExtractor 树走查失败自动回落表扫描; 2. **relid 注册表**:从 `0x80000000` 起按需「铸币」——块首次被展开时分配一个 relid 并缓存其符号树,之后该 relid 换成树节点继续往下走;区节点复用在线固定 relid `0x90010000/0x90020000/0x90030000/0x90050000/0x90060000`(I/Q/M/T/C),不进注册表; 3. **逐层 BuildPObject**:`getTypeInfoByRelId` 只把**当前一层**子节点转成 PObject——数组父单条目(VarnameList 只含数组父一条,元素节点由 GUI 自己生成,与在线同款语义)、结构 RelationId 指向子层类型、MDim 维度反序、叶子 Softdatatype 用树节点 Type(已实测与在线 sd 值一致)——**契约与在线同形,GUI 的 AfterExpand 一行不用改**; 4. **懒加载正确性的硬证据**:懒路径的全量导出与改造前基线**逐字节一致**(83259 行 `cmp` 门禁)。 首次展开一个块 ≈ 在线一次类型信息请求的代价;之后的每一层展开都是零成本缓存读取。 ### 6.4 离线读值为什么敢给 CRC 在线读优化块/非优化块都依赖 32 位符号 CRC 与 LID 访问序列,任何算错都会读到错误地址。离线读值的 VarInfo **一律取自对拍叶缓存**(`ExportBlockLeaves`/`ExportTagLeaves` 产出的、经真实 PLC 全量对拍校准的那批叶子),**不从树节点现算 CRC**——从根上消除「离线 CRC 算错 → 读错地址」的风险。对拍叶缓存本身按块/按区懒生成,且多 PLC 场景下按 PLC 键控。 ## 七、离线解析器 TiaProjectParser `TiaProjectParser` 直接解析博途工程文件,无需 TIA Portal 与任何西门子运行时: - **输入**:工程目录或 `.apXX` 归档(解包后的同一套容器结构,两者逐字节一致已验证); - **容器**:`PEData.plf` + 分层 XML 元数据(`Database.FileHeader.ProductVersion` 二进制头恒在,V13=1300.109.701.1 → 版本刻度 1300)+ 存储对象流; - **磁盘格式只有 3 个家族**:FileFormat V10 / V11 / V14——**V12 起至今全部走 V14 族解析器**,库内版本分支全是 `<= FileFormat.V10/V11/V14`,无 V19+ 专属逻辑。这解释了为什么一个 2017 年的解析器能打开 V21 工程; - **实测版本矩阵**:V13×2、V14×2、V15×2、V19(141634 行、25 DB)、V20×3(S7-1200/1200 G2/1500)、V21(Modbus SCL 例程,3383 行)全部打开/解析/导出成功; - **语义细节**:最新流(博途当前视图)解析——每 InstId 取最新副本、关系解析最新胜出;AT 视图子成员输出;Variant(63) 平坦行;InOut 区 DB 引用成员 → Pointer 平坦行;数组元素逐项展开; - **优化块的 -1 偏移**:V19 工程所有偏移 -1 是**正确行为**(优化块本就没有静态字节偏移),不是解析失败。 ## 八、多 PLC 工程支持 真实工程可含多台 PLC(如 tiaProject18 含 2 台:CC1 253 个块/113 个输入标签、CC2 27 个块/3 个输入标签)。设计要点: - **relid 编码扩展**:区 relid = 区基址 + `plcIndex × 0x100`(plcIndex 0 与原固定值完全相同,向后兼容;每区窗口 0x10000、步长 0x100,上限 256 台 PLC);PLC 节点哨兵 = `0x90070000 + plcIndex × 0x100`; - **块表按 PLC 分组**:`GetListOfPlcs()` 列 PLC 名;`SelectPlc(name)` 切换活动块表并镜像该 PLC 的 5 字段信息;`GetListOfDatablocks(plcIndex)` 按索引取块; - **GUI 树**:多 PLC 时每台 PLC 名**直接作为并列树根**(不包工程名层,与单 PLC「根 = PLC 名」一致),各带 "Loading..." + PLC 哨兵自动展开一层(块列表 + 5 个区);选中哪个 PLC 节点就显示哪个 PLC 的信息与符号,符号遍历经过 PLC 子树自动带出活动 PLC,`Read` 落在正确的 PLC 上。 一个值得记下的 API 设计教训:`GetListOfDatablocks()` 二参数重载曾误委托到索引 0,导致 `SelectPlc` 切换后仍返回 PLC[0] 的块表。语义约定必须明确:**二参数 = 活动 PLC,三参数 = 按索引**——并写进 `--conn` 门禁逐 PLC 检查(selectSwitch 一致性)。 ## 九、类型边界:Variant 叶子 tiaProject4 的 MB_SERVER_DB(`TCON.CONNECT`、`TSEND.DATA`、`TRCV.ADDR` 等 **24 个 Variant 叶**)曾出现选中后数据类型与符号地址双双空白。根因有三层,每一层都独立成立: 1. **树与对拍缓存缺口**:离线符号树(非对拍模式)含 Variant 节点,但**对拍叶缓存故意排除 Variant**(镜像在线 Browse 不产出该类行)→ 树走查找到节点后按 Access 回查缓存落空,返回 null; 2. **TagFactory 缺 case**:`PlcTags.TagFactory` 的 Variant case 被原作者注释掉,注释理由是「Variant isn't added inside of the instance-db as a variable!」——tiaProject4 证明该假设不成立,default 分支返回 null; 3. **协议层未实现**:`PValue.Deserialize` 对 Variant 抛 `NotImplementedException`,驱动不支持 Variant 值反序列化。 修复策略是「显示与取值解耦」:缓存落空时从树节点**合成显示用 VarInfo**(Access/Type 取树节点,CRC=0——非优化块 LID 读仍可尝试,优化块读在 PLC 侧干净失败);补 `PlcTagVariant` 显示用标签类(写值抛 `NotSupportedException`);`ReadTags` 捕获 `NotImplementedException` 返回 -1 防 GUI 崩溃(GUI 选中叶子即触发读)。结果:类型显示 `Variant`、地址显示 `8A0E0001.78.F`,普通叶子路径零回归。 ## 十、验证体系:四层对拍门禁 协议实现工作的第一原则:**没有对拍,寸步难行**。本项目有四层门禁,全部自动化: ### 第一层:真实 PLC 在线对拍(verify7) 以 192.168.0.250 的 S7-1500(PLC 名「Powin储能项目C区」)为真值,离线解析结果逐叶对比在线 Browse: ``` SUMMARY online=75042 offline=83259 matched=75042 onlineOnly=0 offlineOnly=8217 segMismatch=0 crcMismatch=0 ok=75042 GATE=PASS READ tried=50 ok=50 fail=0 ``` - `onlineOnly=0`:离线不漏一个在线符号; - `segMismatch=0 / crcMismatch=0`:**75042 个符号的 CRC 与访问序列全部一致**——这是「离线解析的优化块地址与真机完全一致」的最强证据; - READ 50/50:随机抽样 50 个变量实读 PLC 全部成功; - `offlineOnly=8217` 有明确解释:PLC 上残留 8 个**旧版本块**(8A0E0064/012C/0190/03F2/03FC/0406/0410/041A)在离线工程里已被删除——不是解析缺口; - 叶清单固化为字节基线文件,每次回归 `cmp`。 ### 第二层:商用库对拍(AGL 名字真值) 以 AGLink40(原生 C++ 树 dump,AglTree 工具)为名字真值,对 **25 个真实工程(tiaProject1-25,覆盖 V13~V21)全量矩阵对拍,0 真实缺口**。要点: - V19 工程(141634 行导出):叶子名 **35316/35427(99.7%)精确命中**,AGL-only 111 行全是 `In_EventStatus`(S7_Pointer 叶子,我方按当前视图展开成其结构成员,AGL 只列叶子本身); - AGL-only 未命中叶子在全部 25 个工程中**无一例外是 S7_Pointer**——全部可解释; - p18 双 PLC 分开验证:CC2 块 2357 公共 + 142 全命中、标签叶名 143/143; - 对拍还**双向**发现真值库自身的问题:AGL 6.1.0.5 完全漏读 p18 CC1 的 F 安全子系统(F 块 DB209_C、F02000_-KE61 及标签全体缺席),我方完整; - AGL 无法加载 V21(-393099)——V21 无 AGL 真值,改用 plf 内嵌 XML 元数据交叉验证(12/12 顶层名、39 行 Modbus 结构逐一吻合)。 ### 第三层:统一 API 门禁(--conn) headless 验证 `LoadSymbolsFromFile` 后的行为与直用离线引擎完全等价: - `Browse` 叶数、块表块数、首块与 Inputs 区的 `getTypeInfoByRelId` 成员数逐项一致(tiaProject18:Browse **159165/159165** 完全相等); - 多 PLC 逐台检查:每台块表 + 区 relid 变体 + `SelectPlc` 切换后活动块表一致性; - `--sym <symbol>` 符号解析探针全链路抽查,如 `'"MB_SERVER_DB".TCON.CONNECT' → addr=8A0E0001.78.F type=Variant → OK`; - 报告写入 `<out>.conn_check.txt`,任何一项 FAIL 即门禁失败。 ### 第四层:构建与字节门禁 VS2017 MSBuild 0 错误;TiaSymbolExport 导出与基线**逐字节一致**(任何解析器改造不得改变一行导出输出);HarpoS7Native 22/22 逐字节对拍(见第三节)。 ## 十一、快速上手 ### 11.1 在线:连接 + 浏览 + 符号读值 ```csharp using S7CommPlusDriver; using S7CommPlusDriver.ClientApi; var conn = new S7CommPlusConnection(); int res = conn.Connect("192.168.0.1", password: "your_password"); if (res != 0) throw new Exception($"Connect failed: {res}"); // 全量浏览:拿到所有叶子(含 CRC 与访问序列) conn.Browse(out var leaves); Console.WriteLine($"leaves: {leaves.Count}"); // 按符号名解析标签("DB1".Motor.Speed → 8A0E0001.78.F + SymbolCrc) var tag = conn.getPlcTagBySymbol("\"DB1\".Motor.Speed"); Console.WriteLine($"{tag.Address.GetAccessString()} type={tag.Datatype}"); // 读值(内部自动分块、按项返回错误) int r = PlcTags.ReadTags(conn, new[] { tag }); Console.WriteLine(r == 0 ? $"value: {tag}" : $"read failed: {r}"); ``` ### 11.2 离线:工程文件当 PLC 用 ```csharp var conn = new S7CommPlusConnection(); // 加载博途工程(目录或 .apXX),连接对象成为"虚拟 PLC" conn.LoadSymbolsFromFile(@"D:\Projects\tiaProject18"); // 多 PLC:列出、切换、逐台浏览 foreach (var plc in conn.GetListOfPlcs()) { conn.SelectPlc(plc); conn.GetListOfDatablocks(out var blocks); Console.WriteLine($"{plc}: {blocks.Count} blocks"); } // 与在线完全同一套 API:按符号解析 var tag = conn.getPlcTagBySymbol("\"DB1\".MotorSpeed"); Console.WriteLine($"{tag.Address.GetAccessString()} {tag.Datatype}"); ``` ### 11.3 离线浏览 + 在线读值闭环 ```csharp // 离线打开工程 → 连接真实 PLC → 离线树的叶子直接读真值 conn.LoadSymbolsFromFile(@"D:\Projects\tiaProject1"); conn.Connect("192.168.0.250"); var tag = conn.getPlcTagBySymbol("\"DB1\".SetLimitsHW.Value"); PlcTags.ReadTags(conn, new[] { tag }); // 断开不影响已加载的工程与树 conn.Disconnect(); ``` ### 11.4 纯地址读写(底层) ```csharp var addr = new ItemAddress("8A0E0001.A"); // 或 SetAccessAreaToDatablock(1) + LID conn.ReadValues(new List<ItemAddress> { addr }, out var values, out var errors); ``` ## 十二、结语与获取方式 从协议栈到工程浏览,S7CommPlusDriver 走通了一条完整的技术路线:逆向 → C# 参考实现 → C++ 原生移植逐字节对拍 → 真机全量验证 → 离线/在线一体化。其中「一个函数让博途工程变成虚拟 PLC」的设计说明:**好的抽象不是加一层胶水,而是让两个数据源共享同一套契约**——PObject 契约 + relid 语义 + 对拍叶缓存 CRC 就是那个契约:在线是网络实现,离线是工程文件实现。 优化 DB 块的符号访问,本质是三道坎:符号 CRC 算法要算对、LID 访问序列要逐层走对、类型树要按需取对。本文给出的实现全部经过真实 PLC 与 25 个真实工程的对拍检验。 驱动仍在演进(订阅、报警、TLS 路径等方向),欢迎试用与反馈。**再次提醒:勿用于生产环境。** **测试 Demo 下载(本文全部验证场景对应的工程与程序):** ``` git clone https://gitee.com/lircy/S7CommPlusV3Driver.git ```
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
收藏
・
3
点赞
・
2
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
komawang
感谢你的积极参与,期待更多精彩内容!
5天前
zhczf
谢谢你的细致分析,受益匪浅!
2026-9-7 12:55
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
Coder_shen
6
发帖
4
回帖
10
RANK
关注
私信
他的文章
[原创]零依赖解析西门子 STEP 7 工程:一个 .NET 离线变量表解析器的设计全记录
96
[原创]基于 .NET 4 实现从欧姆龙 Sysmac Studio工程文件解析全局符号变量表
75
基于C#实现CodesysV3符号变量访问
288
[原创]C#实现西门子S71200与S71500控制器优化DB块符号访问
6075
基于 C# 零依赖西门子 API 实现从博途工程文件解析符号变量树
161
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部