在电气自动化系统中,ST(Structured Text)语言是IEC 61131-3标准定义的高级文本编程语言,广泛用于PLC(可编程逻辑控制器)控制程序开发。其语法接近Pascal,支持数组、结构体、指针、函数块等特性,便于实现复杂算法和数据结构操作。但正因其灵活性,开发者若忽略底层安全约束,极易引入隐性缺陷——其中指针未判空即解引用是最典型、最危险的一类错误,可在运行时直接触发PLC硬件级异常,导致任务中断、I/O冻结、甚至整机复位,危及产线安全与设备寿命。
以下为一套完整、可落地的防护方案,覆盖编码规范、编译检查、运行时监控与故障响应四个层级,所有措施均基于主流PLC平台(如倍福TwinCAT 3、施耐德Unity Pro、西门子TIA Portal + SCL兼容ST扩展)验证可行,无需额外硬件,纯软件层面实施。
一、问题本质:ST语言中的指针与NULL语义
ST语言标准(IEC 61131-3 Ed. 3)未定义NULL指针常量,也不强制要求指针初始化。声明 pVar : POINTER TO INT; 后,pVar 的初始值是未定义的——可能是随机内存地址,也可能是0(取决于PLC固件实现)。当执行 iValue := pVar^;(解引用)时:
- 若
pVar = 0,多数PLC将触发“非法地址访问”硬件异常; - 若
pVar指向只读存储区或未映射地址,同样触发异常; - 异常未被捕获时,PLC运行时环境(RT)终止当前任务周期,进入安全状态(如输出清零、停止扫描),严重时重启CPU。
关键点在于:ST标准不提供IF pVar <> 0 THEN ... END_IF这类显式判空语法。因为POINTER类型不可直接与整数比较——pVar <> 0 是语法错误。
正确判空方式依赖PLC平台扩展:
- TwinCAT 3 支持
ADR(0)获取空地址,并允许pVar <> ADR(0); - TIA Portal(SCL模式)提供
ISVALID(pVar)函数; - Unity Pro 使用
pVar <> 0(因其实现将0视为空指针);
但这些都不是标准ST语法,跨平台移植时必须隔离处理。
二、四级防护体系:从编码到运行时
1. 编码层:强制初始化 + 封装安全解引用函数
禁止裸指针声明与解引用。所有指针变量必须显式初始化,并封装为带空值检查的访问接口。
// ✅ 正确:声明即初始化,且使用安全访问函数
VAR
pSensorData : POINTER TO ARRAY[0..99] OF REAL := ADR(gaSensorBuffer); // TwinCAT写法
pMotorCtrl : POINTER TO MOTOR_STRUCT := ADR(gMotor);
END_VAR
// ✅ 安全解引用函数(TwinCAT示例,返回默认值+置位错误标志)
FUNCTION SafeDerefReal : REAL
VAR_INPUT
pSrc : POINTER TO REAL;
bDefault : REAL := 0.0;
pError : POINTER TO BOOL; // 错误标志地址,可选
END_VAR
VAR
bValid : BOOL;
END_VAR
bValid := (pSrc <> ADR(0));
IF NOT bValid THEN
IF pError <> ADR(0) THEN
pError^ := TRUE;
END_IF
SafeDerefReal := bDefault;
ELSE
SafeDerefReal := pSrc^;
END_IF
调用方式:
fTemp := SafeDerefReal(ADR(gSensorTemp), 25.0, ADR(bReadErr));
⚠️ 注意:
ADR()返回的是地址,非指针变量本身。pSrc参数类型必须严格匹配POINTER TO REAL,否则编译报错。
2. 编译层:静态分析 + 自定义检查规则
利用PLC IDE的静态检查能力,规避运行时风险:
- TwinCAT 3:启用
Build → Options → PLC → Check pointer usage,勾选Warn on possible NULL dereference; - TIA Portal:在
Options → Settings → PLCSIM Advanced → Runtime Checks中开启Pointer validity check(仅仿真环境) -
示例用法
if name == "main":
import sys
if len(sys.argv) != 2:
print("Usage: python check_ptr.py <ST_SOURCE_DIR>")
else:
scan_st_files(sys.argv[1])
3. 运行时层:地址有效性校验 + 硬件看门狗协同
PLC无操作系统,无法捕获段错误信号,需主动校验地址有效性:
-
地址范围白名单校验:
所有指针必须指向已知合法区域(如全局DB块、输入映像区)。获取指针地址值后,与预设区间比对:// TwinCAT:获取指针数值地址(UINT型) uiAddr := UINT(ADR(pSensorData)); // 全局缓冲区地址范围:0x20000000 ~ 0x2000FFFF(示例) bInRange := (uiAddr >= 16#20000000) AND (uiAddr <= 16#2000FFFF); -
结合硬件看门狗:
在主循环开头置位软件看门狗标志,在安全解引用函数末尾清除。若因指针异常导致任务卡死,硬件看门狗超时复位CPU,避免I/O悬停。// 主程序循环 PROGRAM MAIN VAR wdgFlag : BOOL := FALSE; END_VAR wdgFlag := TRUE; // 周期开始置位 // ... 其他逻辑 ... fVal := SafeDerefReal(ADR(gSensor), 0.0, ADR(bErr)); wdgFlag := FALSE; // 解引用完成清除配合硬件看门狗定时器(如TwinCAT的
FB_Watchdog),当wdgFlag持续为TRUE超200ms,强制复位。
4. 故障响应层:崩溃现场捕获与热恢复
一旦发生崩溃,必须快速定位并最小化影响:
-
崩溃日志固化:
PLC异常时,固件自动将最后N条指令地址、寄存器快照写入非易失存储(如EEPROM或SD卡)。需在启动时读取并解析:// 启动时检查崩溃日志 IF gFirstScan THEN IF ReadCrashLog(ADR(gCrashInfo)) THEN // 解析gCrashInfo中记录的异常地址 uiFaultAddr := gCrashInfo.uiFaultAddr; // 匹配符号表,定位到ST源码行号(需提前导出MAP文件) END_IF END_IF -
热恢复策略(无停机修复):
对非关键路径指针(如HMI数据缓存),采用“软失效”设计:解引用失败时返回默认值,记录错误计数,连续10次失败后切换备用指针或降级模式。// 热恢复结构体 TYPE SAFE_PTR_REAL : STRUCT pRaw : POINTER TO REAL; uiFailCnt : UINT := 0; fLastValid : REAL := 0.0; bEnabled : BOOL := TRUE; END_STRUCT END_TYPE METHOD GetSafeValue : REAL VAR bValid : BOOL; END_VAR IF NOT bEnabled THEN GetSafeValue := fLastValid; RETURN; END_IF bValid := (pRaw <> ADR(0)) AND IsAddrInRange(UINT(ADR(pRaw))); IF bValid THEN fLastValid := pRaw^; uiFailCnt := 0; GetSafeValue := fLastValid; ELSE uiFailCnt := uiFailCnt + 1; IF uiFailCnt > 10 THEN bEnabled := FALSE; // 禁用该指针,触发告警 END_IF GetSafeValue := fLastValid; // 保持上一次有效值 END_IF
三、典型场景实操:温度采集模块指针防护改造
假设原有代码如下(存在高危漏洞):
// ❌ 原始代码:无初始化、无判空、直解引用
PROGRAM TEMP_READ
VAR
pTempBuf : POINTER TO ARRAY[0..19] OF REAL;
iIndex : INT := 0;
END_VAR
// 主循环
pTempBuf^[iIndex] := READ_TEMP_SENSOR(iIndex); // 危险!pTempBuf未初始化
按四级防护改造后:
// ✅ 改造后代码(TwinCAT 3)
PROGRAM TEMP_READ
VAR
// 1. 编码层:强制初始化 + 类型封装
sTempSafe : SAFE_PTR_REAL;
iIndex : INT := 0;
bInitDone : BOOL := FALSE;
END_VAR
// 初始化块(仅首次扫描执行)
IF NOT bInitDone THEN
sTempSafe.pRaw := ADR(gaTempBuffer); // 指向已分配全局数组
sTempSafe.bEnabled := TRUE;
bInitDone := TRUE;
END_IF
// 主循环:使用安全访问方法
IF sTempSafe.bEnabled THEN
// 2. 运行时校验(含地址范围检查)
sTempSafe.GetSafeValue(); // 内部已做判空与范围检查
// 3. 写入时同样校验
IF sTempSafe.bEnabled THEN
sTempSafe.pRaw^[iIndex] := READ_TEMP_SENSOR(iIndex);
END_IF
END_IF
// 4. 故障响应:监控失败计数
IF sTempSafe.uiFailCnt > 0 THEN
CALL ALARM_POST('TEMP_BUF_FAIL', sTempSafe.uiFailCnt);
END_IF
配套全局变量声明:
// 全局数据块(保证地址固定)
VAR_GLOBAL
gaTempBuffer : ARRAY[0..19] OF REAL := [20(0.0)];
END_VAR
四、跨平台适配表:主流PLC指针安全语法对照
确保同一套防护逻辑在不同品牌PLC上一致生效,需按平台调整底层判空语法。下表列出关键差异:
| PLC平台 | 空指针常量表示 | 判空语法 | 地址转UINT函数 | 备注 |
|---|---|---|---|---|
| TwinCAT 3 | ADR(0) |
pPtr <> ADR(0) |
UINT(ADR()) |
最严格,推荐为基准平台 |
| TIA Portal (SCL) | 0 |
ISVALID(pPtr) |
DINT(ADR()) |
ISVALID是SCL扩展函数,非标准ST |
| Unity Pro | 0 |
pPtr <> 0 |
INT(ADR()) |
需在项目设置中启用指针扩展 |
| Codesys 3.5 | ADR(0) |
pPtr <> ADR(0) |
UDINT(ADR()) |
与TwinCAT语法高度兼容 |
⚠️ 跨平台迁移时,将判空逻辑封装进函数块(FB),通过
#ifdef宏或平台检测变量分发实现分支。例如:#IFDEF TWINCAT bValid := (pPtr <> ADR(0)); #ELSIF TIA bValid := ISVALID(pPtr); #ENDIF
五、测试验证清单:确保防护生效
部署前必须逐项验证,避免防护逻辑自身失效:
- 空指针触发测试:手动将指针赋值为
ADR(0),运行程序,确认SafeDerefReal返回默认值且bError置位; - 越界地址测试:构造一个超出PLC内存映射范围的地址(如
16#FFFFFFFF),赋给指针,验证地址范围检查返回FALSE; - 高负载压力测试:在1ms任务周期内连续调用安全解引用10万次,监测CPU占用率与任务抖动(应<±5μs);
- 崩溃注入测试:使用PLC调试工具强制写入非法地址到指针变量,观察硬件看门狗是否在设定时间内复位;
- 掉电恢复测试:断电后重新上电,检查崩溃日志是否完整保存异常前最后3条指令地址。
每项测试需留存截图与日志时间戳,作为功能安全认证(如IEC 61508 SIL2)证据。
六、延伸防护:指针算术的安全边界控制
ST语言允许指针算术(如pPtr := pPtr + 1),但缺乏数组长度检查,易越界:
-
禁止裸指针偏移:所有
+/-操作必须前置长度校验。 -
使用带长度封装的指针类型:
TYPE BOUNDED_PTR_REAL : STRUCT pBase : POINTER TO REAL; uiLen : UINT; // 数组总长度 uiOffset : UINT := 0; // 当前偏移 END_STRUCT END_TYPE METHOD SetOffset : BOOL VAR_INPUT uiNewOffset : UINT; END_VAR SetOffset := (uiNewOffset < uiLen); // 仅当不越界才更新 IF SetOffset THEN uiOffset := uiNewOffset; END_IF
调用 sBounded.SetOffset(5) 后,再通过 sBounded.GetValue() 访问,内部自动计算 pBase^[uiOffset] 并再次校验。
此设计将指针安全性从“开发者责任”转变为“类型系统约束”,大幅降低人为失误概率。
七、总结:防护的本质是确定性
电气自动化系统的核心诉求是确定性——任何输入组合下,输出行为必须可预测、可追溯、可收敛。指针未判空不是代码风格问题,而是破坏确定性的根本缺陷。四级防护体系的价值不在于“防止某次崩溃”,而在于:
- 将不可控的硬件异常转化为可控的软件状态(错误标志/默认值/降级模式);
- 将偶发的随机故障转化为可复现的确定路径(崩溃日志+符号映射);
- 将依赖开发者经验的检查固化为编译期与运行期的强制规则。
当pPtr^不再是一行可能让产线停摆的代码,而是一个经过四重门禁校验的受控访问入口时,自动化系统的可靠性才真正从“概率事件”升维至“工程保障”。

暂无评论,快来抢沙发吧!