ST语言数组下标越界访问引发的运行时错误边界检查

发布于 2026-03-17 03:38:46 · 浏览 5 次 · 评论 0 条

ST语言(Structured Text)是IEC 61131-3标准定义的五大PLC编程语言之一,广泛用于工业自动化控制系统中。其语法接近Pascal和C,支持结构化逻辑、函数调用、数组操作和复杂数据类型。但正因为其“类高级语言”的灵活性,开发者容易忽略底层运行时约束——尤其是数组下标越界访问这一类看似微小、实则致命的错误。

这类错误在调试阶段往往不暴露:程序能编译通过,仿真运行正常,甚至上电初期也无异常。但一旦实际工况触发越界读写(例如传感器突发脉冲导致索引计算偏移、HMI手动输入非法值、循环计数器未重置),轻则数据错乱、控制失准;重则覆盖关键内存区域(如任务堆栈、系统变量区),引发PLC硬复位、通信中断或I/O模块锁死。更隐蔽的是,某些PLC固件对越界访问不做拦截,而是静默读取相邻内存单元,造成“偶发性故障”,极难复现与定位。

本指南聚焦ST语言中数组下标越界访问的成因、检测机制、防护策略与现场排查方法,所有内容均基于主流PLC平台(如倍福TwinCAT、罗克韦尔Logix、施耐德Unity Pro、西门子S7-1500 TIA Portal)的共性行为,不依赖特定品牌扩展指令。


一、ST语言数组的底层存储模型与越界定义

ST语言中的数组声明语法为:

ARRAY[下界..上界] OF 数据类型

例如:

VAR
    TempBuffer : ARRAY[0..9] OF REAL;  // 10个元素,索引0~9
    MotorCmd   : ARRAY[1..4] OF INT;   // 4个元素,索引1~4
    StatusLog  : ARRAY[-2..2] OF BOOL; // 5个元素,索引-2,-1,0,1,2
END_VAR

关键点在于:ST数组是静态分配的连续内存块,编译时即确定总字节数。以 ARRAY[0..9] OF REAL 为例:

  • 每个 REAL 占4字节(IEEE 754单精度);
  • 总长度 = 10 × 4 = 40字节;
  • 内存地址假设起始为 0x1000,则有效访问范围为 0x1000 ~ 0x1027(含)。

所谓“越界访问”,指代码中使用了超出声明范围的下标值进行读或写操作。例如:

TempBuffer[10] := 25.5;  // ❌ 越界写:索引10 > 上界9
MotorCmd[0] := 1;        // ❌ 越界写:索引0 < 下界1
StatusLog[-3] := TRUE;   // ❌ 越界读:索引-3 < 下界-2

此时,PLC运行时的行为取决于固件是否启用边界检查(详见第二部分),而非语言标准本身强制要求。IEC 61131-3标准未规定必须进行运行时边界检查,仅要求编译器报告明显静态越界(如常量下标超限),而动态计算下标(如 i+1j*2)完全交由厂商实现决定。


二、不同PLC平台的边界检查机制对比

是否执行运行时下标检查,由PLC固件/运行时环境(Runtime)控制,与ST编译器无关。下表总结主流平台默认行为与开关路径:

PLC平台 默认是否启用数组边界检查 如何启用/禁用 越界时典型表现
西门子 S7-1500 (TIA Portal) 否(编译期仅警告,运行时不检查) 在项目属性 → “编译” → 勾选 启用运行时检查数组访问检查 若启用:触发 OB121 编程错误组织块,CPU进入停止模式,诊断缓冲区记录 F3001 错误码
罗克韦尔 Logix (Studio 5000) 在控制器属性 → “高级” → 勾选 Enable array bounds checking 若启用:触发严重故障(Major Fault),CPU停机,事件日志记录 Array Index Out of Bounds
倍福 TwinCAT 3 是(默认启用) 在项目设置 → “Build” → “Runtime Checks” → 可单独关闭 Array Bounds Check 抛出 TC_RuntimeError 异常,可被 TRY...CATCH 捕获,或触发 ErrorHandling 例程
施耐德 Unity Pro 无全局开关;需手动插入 CHECK_ARRAY_BOUNDS() 函数(非标准,依赖库版本) 静默越界(读取随机值/写入相邻变量)

核心结论不能依赖平台默认开启边界检查。生产环境必须显式配置并验证,否则越界即等于“未定义行为”。


三、四类高发越界场景及对应防护代码模板

以下场景在电气自动化项目中占比超85%,全部提供零依赖、跨平台兼容的ST防护写法

场景1:循环遍历中计数器未重置或步长错误

危险代码

FOR i := 0 TO 10 DO  // 错误:上界应为9(对应ARRAY[0..9])
    TempBuffer[i] := SensorValue[i];
END_FOR;

防护方案:永远用 LOW()HIGH() 内置函数获取边界

FOR i := LOW(TempBuffer) TO HIGH(TempBuffer) DO
    TempBuffer[i] := SensorValue[i];
END_FOR;
  • LOW(数组名) 返回声明下界(如 ARRAY[1..4] 返回 1);
  • HIGH(数组名) 返回声明上界(如 ARRAY[1..4] 返回 4);
  • 此写法自动适配任意声明形式([0..9][1..4][-5..5]),杜绝硬编码。

场景2:索引由外部输入(HMI/Modbus/OPC UA)直接赋值

危险代码

TempBuffer[InputIndex] := NewValue;  // InputIndex可能为-1、15、100等任意值

防护方案:显式范围校验 + 默认兜底

IF (InputIndex >= LOW(TempBuffer)) AND (InputIndex <= HIGH(TempBuffer)) THEN
    TempBuffer[InputIndex] := NewValue;
ELSE
    // 写入安全值或触发报警
    AlarmFlag := TRUE;
    AlarmCode := 101;  // 自定义越界错误码
END_IF;

场景3:多维数组嵌套访问时维度混淆

危险代码

VAR
    Matrix : ARRAY[0..2, 0..3] OF INT;  // 3行×4列
END_VAR
// 错误:将行列顺序颠倒,或越界行索引
Matrix[5, 2] := 1;  // 行越界(最大行索引为2)

防护方案:为每维定义命名常量,强制语义清晰

VAR
    MATRIX_ROWS   : INT := 3;   // 行数
    MATRIX_COLS   : INT := 4;   // 列数
    Matrix        : ARRAY[0..MATRIX_ROWS-1, 0..MATRIX_COLS-1] OF INT;
END_VAR

// 使用时明确维度含义
IF (RowIdx >= 0) AND (RowIdx < MATRIX_ROWS) AND 
   (ColIdx >= 0) AND (ColIdx < MATRIX_COLS) THEN
    Matrix[RowIdx, ColIdx] := Value;
END_IF;

场景4:指针解引用后未验证目标有效性(仅适用于支持指针的平台如TwinCAT)

危险代码

pBuf := ADR(TempBuffer);
pBuf^[15] := 99.9;  // 直接解引用越界地址

防护方案:指针运算前计算绝对地址并比对数组首尾

VAR
    pBuf : POINTER TO REAL;
    pStart, pEnd : POINTER TO REAL;
END_VAR

pBuf := ADR(TempBuffer);
pStart := ADR(TempBuffer[LOW(TempBuffer)]);
pEnd := ADR(TempBuffer[HIGH(TempBuffer)]) + SIZEOF(REAL); // 指向末尾后一地址

IF (pBuf >= pStart) AND (pBuf < pEnd) THEN
    pBuf^ := 99.9;
ELSE
    // 拒绝非法指针操作
END_IF;

四、实战:构建可复用的数组安全访问函数库

为避免重复编写校验逻辑,建议创建如下ST函数块(Function Block),供全项目调用:

FUNCTION_BLOCK SafeArrayWrite
VAR_INPUT
    pArray  : POINTER TO ANY;      // 通用指针,指向任意数组首地址
    Index   : DINT;                // 待写入索引
    Value   : ANY;                 // 待写入值(需与数组类型一致)
    ArraySize : DINT;              // 数组总元素数(编译期已知,传入常量)
    LowerBound : DINT;             // 数组下界(如0或1)
END_VAR
VAR_OUTPUT
    Success : BOOL;               // TRUE=写入成功,FALSE=越界拒绝
END_VAR
VAR
    pTarget : POINTER TO ANY;
END_VAR

// 计算目标地址:pArray + Index * 元素字节数(此处简化,实际需按Value类型推导)
// 实际工程中,推荐为每种类型(INT/REAL/BOOL)单独建立专用函数,避免ANY类型陷阱
IF (Index >= LowerBound) AND (Index < LowerBound + ArraySize) THEN
    pTarget := pArray + (Index - LowerBound) * SIZEOF(Value);
    pTarget^ := Value;
    Success := TRUE;
ELSE
    Success := FALSE;
END_IF;

调用示例

// 对 TempBuffer[0..9] OF REAL 写入
SafeArrayWrite(
    pArray := ADR(TempBuffer),
    Index := InputIndex,
    Value := NewValue,
    ArraySize := 10,
    LowerBound := 0,
    Success => WriteOK
);
IF NOT WriteOK THEN
    TriggerAlarm('TempBuffer Index Out of Range');
END_IF;

⚠️ 注意:ANY 类型在部分PLC中存在类型擦除风险,强烈建议优先使用类型特化函数(如 SafeWrite_REAL_Array, SafeWrite_INT_Array),确保编译期类型安全。


五、现场故障排查:三步法定位越界源头

当系统出现间歇性崩溃或数据异常,按以下步骤快速锁定越界点:

第一步:启用运行时诊断(以西门子S7-1500为例)

  1. 在线连接PLC → 打开“诊断缓冲区”;
  2. 复现故障(如点击HMI越界按钮);
  3. 查看最新条目中是否含 F3001(数组访问错误)或 F3002(指针访问错误);
  4. 记录触发时的 OB编号、块名、网络号、时间戳

第二步:反向追踪索引来源

  • 在诊断提示的块内,搜索所有对该数组的写操作(:=);
  • 对每个写操作,向上追溯索引变量(如 i, Index, Pos)的所有赋值路径
  • 重点检查:
    • 是否来自 FBFCIN 参数?→ 检查调用处传入值;
    • 是否来自 FOR / WHILE 循环?→ 核查循环条件边界;
    • 是否来自 HMI 变量绑定?→ 检查HMI画面中该变量的输入限制(如数值框设置最小/最大值)。

第三步:注入临时断点验证

在疑似越界写操作前插入诊断代码:

// 在 TempBuffer[Index] := X; 前插入
IF NOT ((Index >= LOW(TempBuffer)) AND (Index <= HIGH(TempBuffer))) THEN
    DebugLog := CONCAT('越界! Index=', DINT_TO_STRING(Index));
    DebugLog := CONCAT(DebugLog, ', Low=', DINT_TO_STRING(LOW(TempBuffer)));
    DebugLog := CONCAT(DebugLog, ', High=', DINT_TO_STRING(HIGH(TempBuffer)));
    // 将DebugLog发送至HMI文本框或写入DB供上位机读取
END_IF;

通过实时查看 DebugLog 内容,100%确认越界值及上下文。


六、设计阶段防御:自动化代码审查清单

在编码规范中强制加入以下检查项,从源头阻断越界:

检查项 检查方式 不合规示例 合规写法
禁止硬编码数组上界 人工审查 / 静态扫描工具 FOR i:=0 TO 9 DO FOR i:=LOW(arr) TO HIGH(arr) DO
所有外部输入索引必须校验 代码走查 arr[Inp] := val; IF Inp IN [LOW(arr)..HIGH(arr)] THEN arr[Inp]:=val; END_IF;
多维数组访问必须标注维度语义 评审会议 mat[i,j] := x; mat[RowIdx, ColIdx] := x; 并声明 RowIdx/ColIdx 范围
指针操作必须配套地址范围验证 单元测试覆盖 p^ := 1; IF p IN [pStart..pEnd] THEN p^:=1; END_IF;

✅ 最佳实践:将上述规则集成至CI/CD流水线,使用开源工具(如 plcopenxml-parser + 自定义Python脚本)自动扫描ST源码,对违规行标记告警。


七、边界检查的性能代价与取舍建议

启用运行时边界检查会带来约3%~8%的扫描周期延长(取决于数组访问频次)。对于毫秒级响应的高速运动控制,需权衡:

  • 必须开启:安全相关功能(SIL2/SIL3)、数据记录、配方管理、HMI交互逻辑;
  • 可关闭:纯本地高速循环(如PID内环计算),但需满足:
    ① 索引由内部确定且逻辑简单(如 i := i + 1; IF i > 9 THEN i := 0; END_IF;);
    ② 已通过静态分析证明无越界路径;
    ③ 关键变量增加看门狗校验(如每周期检查 i 是否 ∈ [0,9],越界则复位)。

终极原则:宁可牺牲微秒级性能,不可容忍一次越界导致的产线停机。


在ST代码中写下第一个 LOW()HIGH() 函数调用,就是为控制系统筑起第一道内存防火墙。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文