-
-
[原创]基于 .NET 4 实现从欧姆龙 Sysmac Studio工程文件解析全局符号变量表
-
发表于: 2026-9-24 16:53 229
-
基于 .NET 4 实现从欧姆龙 Sysmac Studio 工程文件解析全局符号变量表
摘要:欧姆龙 NJ/NX 系列 PLC 的 EtherNet/IP 通讯(CIP 符号寻址)需要提前知道每个变量的符号名、完整路径与 PLC 数据类型。本文介绍如何在不依赖任何第三方库、目标框架仅为 .NET Framework 4.0 的前提下,直接解析 Sysmac Studio 的
.smc2工程文件,从中还原出完整的全局符号变量表——包括 ZIP 压缩包的手工解析、SLWD 变量表格式、DUT 结构体展开、变量树构建与"完整路径名 → 数据类型"映射的生成。
一、背景与需求
一个实际工程(焊机生产线,NX502-1600)的全局变量表包含 3,042 个顶层变量、44 万多个叶子变量。上位机驱动与模拟器都需要这样一张表:
- 驱动侧:浏览变量树、按符号名读写变量;
- 模拟器侧:按
VAR://名称解析内存地址,模拟批量读写。
变量表有三个来源:运行时从 PLC 上传、解析 txt 导出文件、直接解析 .smc2 工程文件。前两者依赖 PLC 在线或人工导出步骤,而 .smc2 是 Sysmac Studio 的工程存档本身——离线、完整、无需额外操作,是实现"打开工程即能通讯"的最佳数据源。
但有两个现实约束:
- 格式未公开——.smc2 的内部结构只能靠逆向分析;
- 目标框架是 .NET 4.0——没有
System.IO.Compression.ZipArchive(.NET 4.5 才有),必须手工解析 ZIP。
二、.smc2 工程文件的内部结构
.smc2 本质上是一个 ZIP 压缩包,内部按工程 GUID 组织:
<项目GUID>/
<GUID>.xml 变量表 (SLWD 格式) / 数据类型 (DUT) / PLC 配置
<GUID>.oem 工程实体清单 (变量表/POUs/任务等)
<GUID>.manifest CPU 型号信息
<GUID>.NexBuildResults 编译版本
- 变量表以 SLWD 文本格式嵌在 xml 条目中(文件头 30 字符内出现
[SLWD即判定); - 结构体(DUT)与枚举定义也是 xml(
<DataType BaseType="STRUCT">/BaseType="ENUM"); - 哪个 GUID 是全局变量表、属于哪个 POU,由
.oem实体清单回答。
三、第一步:手写极简 ZIP 读取器
.NET 4.0 没有 ZipArchive,好在 .smc2 只用 Deflate 压缩(方法 8),按 ZIP 规范解析三种结构即可:
[本地文件头] [数据] ... [中央目录] ... [EOCD]
internal class Smc2ZipArchive : IDisposable
{
// 1. EOCD 签名 0x06054b50: 从文件尾部回扫定位 (末尾注释可能长达 64KB)
// 读出入口数量 / 中央目录偏移与长度 (不支持 ZIP64)
// 2. 中央目录签名 0x02014b50: 逐条目读 Method / 压缩前后大小 /
// 本地头偏移 / 条目名 (UTF-8)
// 3. 读取: 按本地头偏移定位, 跳过 30 字节固定字段 + 文件名 + 扩展区,
// 方法 8 用 DeflateStream 解压 (RFC1951 裸 deflate 流, 与 ZIP 方法 8 一致)
}
要点:
- 回扫 EOCD:ZIP 末尾注释最长 64KB,从尾部向前扫描签名最稳妥;
- 循环读满:
FileStream.Read不保证一次读满,封装ReadExactly处理短读; - 全部条目名用
StringComparer.OrdinalIgnoreCase存字典,读取时按名字索引。
整个读取器不到 150 行,零依赖。
四、SLWD 变量表格式
SLWD 文本每行一条变量记录,列以 TAB 分隔、每列为 K=V:
++D=N=HMI_Button<TAB>D=ARRAY[0..9,0..4] OF BOOL<TAB>AT=%W0.0<TAB>
Com=&comment<TAB>G=VAR_GLOBAL<TAB>NTP=PublicationOnly<TAB>R=1...
解析规则:
| 键 | 含义 | 键 | 含义 |
|---|---|---|---|
N |
变量名 | R |
保持属性 (=1) |
D |
数据类型 (IEC 写法原样保留) | RO |
只读 (=1) |
AT |
硬件地址原文 | Ord |
排序号 |
Com |
注释 (XML 实体编码) | G |
作用域 (VAR_GLOBAL/VAR/...) |
IV |
初始值 | NTP |
网络公开属性 |
实现上的三个细节:
- 键前缀剥离:
K前面的+是行/列标记,参与匹配前剥掉; - 后键覆盖:同一行重复出现的键,后出现的生效(
data[key] = value即可); +GN=行跳过:只是作用域标记,记录自身的G=值更精确。
Com= 里的 &、< 等需要做 XML 实体解码——注意 & 必须放在 >/< 之后替换,避免双重解码。
五、OEM 实体清单:回答"变量表属于谁"
一个工程含全局变量表 + 各 POU(Program/FB/Function)的局部变量表,它们的归属记录在 .oem 中:
<Entity type="Program" name="ST10" id="{guid-A}" />
<Entity type="Variables" subtype="Local" id="{guid-B}" /> <!-- 属于 ST10 -->
<Entity type="Variables" subtype="Global" /> <!-- 全局表 -->
<Entity type="DataType" name="Recipe_Data" id="{guid-C}" />
<Entity type="NexTask" name="PrimaryTask" id="{guid-D}" />
解析器按文档顺序扫描 <Entity .../>,维护"当前 POU"上下文:遇到 Program/Function/FunctionBlock 进入上下文,Variables/PouBody 等实体挂在当前 POU 名下。最后得到 变量表 GUID → POU 名 的映射,全局表命名 Global,局部表用 POU 名。
六、DUT 结构体与枚举
结构体成员的类型信息不在 SLWD 里,而在 DUT 定义中:
<DataType Name="Recipe_Data" BaseType="STRUCT" OffsetType="NJ">
<DataType Name="WeldTime" BaseType="UINT" OffsetChannel="0" OffsetBit="0"/>
<DataType Name="WeldPressure" BaseType="ARRAY[0..3] OF REAL" .../>
</DataType>
<DataType Name="State_ENUM" BaseType="ENUM"> ... </DataType>
- 成员是自闭合的
<DataType ... />引用,正则平铺收集即可; - 跳过无名/无类型成员和内联
STRUCT声明; - IEC 类型名大小写不敏感(工程里
ST_Robot_01IN被引用为ST_Robot_01In很常见),DUT 字典必须用OrdinalIgnoreCase; - 枚举定义可能在另一个 xml 里,与结构体分开扫描(
BaseType="ENUM"收集类型名,供模拟器把枚举按 4 字节整数读写)。
七、变量树构建:递归展开 + 数组延迟展开
每张变量表构建为顶层节点列表,BuildTypedNode 按类型名分派:
| 类型名 | 结果 |
|---|---|
STRING[n] |
STRING 叶子 |
ARRAY[a..b,c..d] OF X |
ARRAY 节点(存维度/上下界/元素类型元数据) |
| 已知 DUT 名 | STRUCT 节点 + 成员子树(递归) |
Sysmac 库类型(如 _sAXIS_REF) |
内置成员表构建的 STRUCT 子树 |
| 其它 | 标量叶子(类型名原样保留) |
两个关键设计:
1. 数组延迟展开。一个 ARRAY[0..99,0..99] OF REAL 有 1 万个元素,全部预展开会让 44 万叶子的工程再膨胀一个数量级。所以数组节点只存边界元数据,GUI 浏览展开树节点时再按需生成 Name[x,y] 元素;例外是结构体数组——元素类型是结构体时预展开(浏览体验与性能的平衡点)。
2. 结构体子树模板缓存。同一 DUT 类型会被大量重复展开(结构体数组每个元素都重建一遍完整成员子树),首次构建后把成员节点字典缓存起来复用节点引用——树节点仅供只读浏览,共享子树是安全的。配合"类型名不含 [ 先短路、不跑正则"的检查,44 万叶子的工程导入从几十秒降到 3.7 秒。
递归保护:activeDuts 集合防止结构体递归自引用(含自身数组的 DUT)导致死循环;模板缓存"先登记空模板、再递归填充"处理自引用。
八、完整路径 → 类型映射:为通讯服务
驱动树浏览与模拟器通讯共用同一棵树。模拟器按地址读写需要的是平铺映射:
"H.PLC_Mode" -> "INT"
"CCD[0].Out_STRING" -> "STRING"
"H.PLCData.XXX" -> "REAL" (结构体成员)
"_EC_CommErrTbl[5]" -> "BOOL" (数组元素)
"RecipeType" -> "ARRAY[0..9] OF Recipe_Data" (裸数组名, 支持整数组读写)
loadVariableTypeMap 递归枚举节点子树:结构体节点递归成员(路径加 .成员名),数组节点先登记裸数组名、再按边界枚举元素路径(结构体元素在树上直接递归,标量元素直接登记)。
网络公开过滤是通讯正确性的关键:实体 PLC 上只有设置过"网络公开"的变量才能被 EtherNet/IP 访问(SLWD 的 NTP 字段非空)。同一份解析结果,传 onlyPublishedGlobals 即过滤未公开变量——实测 44 万叶子的工程,过滤后只剩 306,345 个网络公开变量(顶层 3,042 → 355)。
九、控制器信息提取
CPU 型号/固件/IP/任务周期等散落在不同文件中(.oem/.manifest/.NexBuildResults/配置 xml),用一组窄正则逐个文件累积提取:
| 信息 | 来源片段 |
|---|---|
| 型号 + 固件 | SetUnit "NX502-1600 Ver.1.66" |
| IP / 掩码 / 网关 | NexCpuEipTcpIp 配置块(NX5xx 双网口有 Port1/Port2 两段) |
| 任务周期/优先级 | TaskExecutionCondition / ExecutionPriority |
| 作者/语言/时间 | <SolutionModel> 元数据 |
一个值得记录的坑:CompileVersion 在 NX 工程里第一个匹配往往是占位的 0.0,0.0——取第一个非占位的值才是真实编译器版本。
十、小结
整套解析器 约 1,500 行、零第三方依赖、纯 .NET 4.0,拆解下来的技术点:
- ZIP 三种结构的手工解析(EOCD 回扫 / 中央目录 / 本地头)+ Deflate 解压;
- SLWD 文本格式的键值解析与 XML 实体解码顺序;
- OEM 实体流式扫描维护 POU 上下文;
- DUT 递归展开 + 模板缓存 + 数组延迟展开的组合拳;
- 完整路径 → 类型映射与网络公开过滤。
同一份实现同时服务两个程序集:驱动侧(变量树浏览)与模拟器侧(批量读写通讯),实际工程 44 万叶子导入 3.7 秒,验证与 Sysmac Studio 变量监视器导出的清单完全一致。
本文对应的实现代码见
OmronNjNxEipDriver解决方案中的OmronSmc2File.cs(驱动与模拟器两个工程保持同步的双份拷贝)。
[招生]科锐逆向工程师培训(2026年11月13日实地,远程教学同时开班, 第57期)