Bug 报告:Keil C251 V5.60.13.0 在 OPTIMIZE(6~9) 下编译器崩溃 (最终精确定位版)
报告编号:C251-2026-0001
报告日期:2026-08-16
报 告 人:sadate(STC技术论坛)
状 态:待 Keil / STC 官方确认
1. 概述
Keil C251 V5.60.13.0 编译器在优化级别 OPTIMIZE(6) 及以上(6/7/8/9,含 SPEED/SIZE 两种目标)编译一段合法且无未定义行为的 C 代码时发生编译器内部崩溃:
C251.EXE 进程以访问违规(ExitCode = 0xC0000005)退出;
- 不输出任何错误或警告信息;
- 不生成
.obj 文件,下游链接报 L210: module not found;
- 该行为与内存模型无关(
XSMALL/LARGE 均可触发);
- 降级到
OPTIMIZE(5) 及以下即恢复正常。
该问题首次出现在真实工程的 函数中(STC32G144K246,XSMALL 模型)。通过深入研究,已构造出与项目零依赖的 8 行最小可复现用例(见第 6 节)。
结论:
Keil 官方文档与公开论坛中未检索到针对此问题的已知/已修复 Bug 记录;社区仅有"高优化等级可能对 xdata 产生非法访问"的零散经验性提示。据此判断此为未公开记录的编译器代码生成缺陷。
2. 环境
| 项 |
值 |
| 编译器 |
Keil C251V5.60.13.0(C251 COMPILER, 2007–2023 Arm Ltd.) |
| 工具链路径 |
E:\Keil_v5\C251\BIN\C251.EXE |
| 目标芯片 |
STC32G144K246(251 内核,source mode;宏 TB_TARGET=10) |
| 操作系统 |
Windows 11 |
| 触发内存模型 |
XSMALL(LARGE 同样可触发) |
| 触发优化级别 |
OPTIMIZE(6)~OPTIMIZE(9);OPTIMIZE(9, SPEED) / OPTIMIZE(9, SIZE) 均可 |
| 正常级别 |
OPTIMIZE(0)~OPTIMIZE(5) |
3. 复现步骤
-
进入报告目录:
附件:repro_min.c
cd bugreports\c251_xsmall_opt9_crash\report
-
以崩溃级别编译最小可复现用例 repro_min.c:
E:\Keil_v5\C251\BIN\C251.EXE repro_min.c DEFINE(TB_TARGET=10) OPTIMIZE(9) XSMALL CODE STRING(CODE)
-
观察退出码与输出产物(崩溃时无任何输出、不生成 .obj)。
4. 预期行为
编译器应正常生成 repro_min.obj(或输出合法的编译错误/警告),进程退出码为 0。
5. 实际行为
E:\Keil_v5\C251\BIN\C251.EXE e:\MCU\TinyBASIC\bugreports\c251_xsmall_opt9_crash\report\repro_min.c DEFINE(TB_TARGET=10) OPTIMIZE(9) XSMALL CODE STRING(CODE)
--- 无任何输出 ---
ExitCode = 0xC0000005 (Access Violation / 访问违规)
NO OBJ PRODUCED (CRASH)
C251.EXE 在编译过程中静默崩溃,无错误、警告信息;
- 不生成
.obj,下游 L251 链接报 L210: module not found;
- 同一文件在
OPTIMIZE(4) 下正常编译,成功生成 .obj。
6. 最小可复现用例
文件:e:\MCU\TinyBASIC\bugreports\c251_xsmall_opt9_crash\report\repro_min.c(standalone,零项目依赖;m[2] 全部下标合法)。
static union { unsigned char m[2]; } xdata g; extern unsigned int f(unsigned int);
void save(unsigned char *c)
{
unsigned int n = f(0U), i = 0U;
unsigned char xdata *p = (unsigned char xdata *)g.m;
p[i++] = c[0], p[i++] = c[0], p[0] = f(n);
}
说明:
- 三个操作(两次后递增索引式存储、调用
f(n)、对 p 回写)合并为一条逗号表达式——C90 保证从左到右求值,调用在两个存储之后执行;
f(n) 返回值隐式窄化即可,无需 (unsigned char) 强转;
7. 根本原因分析
-
崩溃位于优化器/代码生成阶段,而非词法/语法解析:OPTIMIZE(5) 与 OPTIMIZE(6) 之间存在明确质变阈值。按 C251 文档,级别 6 开启 LOOP OPTIMIZATION,7~9 进一步叠加 EXTENDED LOOP OPTIMIZATION、GLOBAL OPTIMIZATION、LOOP UNROLLING 等激进变换。崩溃正是从 6 级这一层开始出现。
-
精确定位到"显式强转的本地指针 + 索引式存储 + 跨调用复用"的代码生成路径:
- 必须"显式强转 + 本地指针变量":无强转或无本地指针变量而内联强转均不崩溃 → 缺陷位于"强转后的 union 成员地址 → 本地指针寄存器"的分配路径,而非单纯的索引表达式;
*p++(单指令指针自增)不崩溃,而 p[i++](需"基址 + 索引"地址计算)崩溃 → 缺陷在索引式存储的****地址计算(基址变址)****路径;
- 直接
g.m[i] 不崩溃→ 缺陷涉及"本地指针寄存器承载强转后的 union 成员基址"的分配;
- 回写必须走
p 而非其他数组→ 编译器需在调用点前后维护同一个"强转派生的 xdata 基址 + 索引寄存器"的状态,跨调用保存/恢复时出错。
-
与 union 类型强相关:struct 成员不崩溃、union 成员崩溃。union 成员访问使编译器需处理成员地址 = ****联合体基址(offset 0)****的类型化地址计算,再经显式强转后作基址变址;在 OPT≥6 的全局优化/寄存器分配中,疑似对该"强转 + union 成员基址 + 索引变址" IR(内部寄存器)形态生成错误(如索引寄存器保存/恢复遗漏、DPTR 调度不一致),最终访问非法内存崩溃。
-
数据流要求是"返回值跨存储区存活":n 必须在索引存储之前由调用赋值并随后使用——说明崩溃路径与****"外部调用返回值 → 寄存器 → 跨索引存储区存活 → 调用点保存/恢复"****的寄存器分配有关。
-
与 STC32G / 内存模型无直接因果关系:LARGE 模型同样崩溃,说明与模型配置无关,而是上述 IR(内部寄存器) 形态在优化器中的通用缺陷。
说明:编译器内部 IR/寄存器分配细节无法直接观察,需 Keil 官方以 -j(内部调试)复现确认具体故障 pass。当前外部行为证据指向:OPT≥6 优化器处理"显式强转存入本地指针的 xdata union 成员基址 + 调用前多次后递增索引式存储 + 调用后同一指针回写,且外部调用返回值跨存储区存活"时崩溃。
8. 规避措施
在官方修复前,任选其一即可安全编译:
-
降低优化级别到 ≤5(当前工程采用的方案,实测稳定):
C251 ... OPTIMIZE(4) XSMALL
-
使用 #pragma OPTIMIZE(5) 仅对该文件降级,其余文件保持高优化(C251 支持 #pragma OPTIMIZE(n) 文件级指令)。
-
改写存储方式为指针式后递增 *p++ = ...(实测 OPT9 下不崩溃)——最小改动即可规避,无需降优化:
unsigned char xdata *p = (unsigned char xdata *)g.vars_mirror;
*p++ = ...; /* 替代 p[pos++] = ...; */
-
或将 vars_mirror 从 union 成员改为 struct/独立 xdata 数组(实测 struct 不崩溃)。
9. 附录:最小化过程中的两个 C251 前端怪癖
以下两点与本 Bug 无关,但会影响最小化工作,特此记录。
9.1 内存空间关键字不能作形参名
C251 中 code、xdata、far 等是内存空间关键字,不能用作函数形参名。如:
void set_error(unsigned char code); /* ERROR C25: syntax error near ')' */
void set_error(unsigned char x); /* 正常 */
用保留内存关键字命名形参会报 C25 语法错误(且后续调用点报 C95: too many actual parameters),这是预期的语法限制而非编译器缺陷。
9.2 特定代码形态触发误导性 C25 解析错误
用例(带标签裸 struct struct S {...}; ... struct S *c; + 本报告所述函数体形态)会在任意优化级别下报出大量级联 C25 语法错误(如 syntax error near 's16'、redefinition of 'p'、'array': initialization needs curly braces),而等价的 typedef 风格代码解析完全正常。此现象疑似 C251 前端的另一个独立解析健壮性缺陷,与本次崩溃无关——请以 typedef 风格编写复现用例。