“数字已经读出来了”,并不代表输入合法

解析成功之后,仍要检查完整消费、符号与字段范围。

Posted by Bruce Lee on 2026-06-25

系列目录 · 资源与调度 · Read in English

“寄存器写成了 r7oops,怎么还能继续往下走?”

“解析函数成功读出了 7。”

“后面的 oops 呢?”

“它没问。”

编译器后端里,很多错误不是没做检查,而是检查回答了一个比预期更弱的问题。解析出一个整数,只证明字符串开头存在可解析片段,不证明整个字段满足语法;把立即数拆成高低两段,也只证明公式看起来合理,不证明边界处没有多出一位。

寄存器名是一个完整语言

设教学汇编接受 r0r15。合法输入不仅需要数值落在范围内,还需要前缀正确、数字部分非空、字符全部被消费,并明确是否允许正号、空格或前导零。

可以把验证拆成五步:

1
2
3
4
5
检查前缀
检查第一位属于十进制数字
解析整数并检查转换状态
检查解析终点恰好等于字符串终点
最后检查寄存器编号范围

已核实的改动用能够返回解析终点和错误状态的转换方式替代宽松的字符串转整数调用,并明确要求整个数字后缀被消费。相关测试覆盖尾随垃圾、空格、正负号、超长数字、越界编号和别名。

这里不应把“严格”理解成“越少接受越好”。有些别名或带前导零的编号原本被定义为合法,测试也保留它们。严格的意思是完整实现既定语法,而不是顺手收紧所有历史行为。

为什么失败可能延迟到二进制导出

一些生成器先保存寄存器字符串和指令对象,直到编码字段时才解析。于是构造一条指令可以成功,导出时才发现 r7oops。

这种延迟并非一定错误,但接口必须让调用者知道:构造成功不是可编码的最终保证。错误边界需要覆盖导出,导出失败还应保留调用方原有输出,避免把旧有效数据替换成半份新数据。相关测试专门检查了这一点,与 错误处理的边界讨论的状态接口形成配合。

如果希望更早报错,可以在构造阶段把字符串解析为类型化寄存器。但这涉及接口迁移和诊断归属;保留字符串形式也可能有可读性或工具互操作原因。选择哪一层验证,要看整体设计,不能仅凭一个非法输入就宣布所有延迟编码都不可取。

立即数拆分最容易在符号边界翻车

考虑一个教学指令集:机器字宽 W,低段立即数宽 L,并且低段作为有符号数参与加法。我们希望构造无符号位模式 x:

1
2
3
high = ((x + 2^(L-1)) >> L) masked_to_(W-L)_bits
low = sign_extend(low_L_bits(x))
result = (high << L) + low modulo 2^W

为什么高段要加一个偏置?因为低段最高位为 1 时,会被解释为负数,高段必须补偿这个符号扩展。为什么还要掩码?因为接近全字宽上界时,这个补偿可能把高段顶出它的字段宽度。

用 W=8、L=3 的教学例子,x=255。低三位是 111,作为三位有符号数表示 −1。高段经过补偿后在固定字宽下回绕到 0,于是 0 + (−1) 的八位结果恰好是 255。若把补偿后的高段当作无限精度正数直接塞进字段,就可能错误拒绝或编码出多余位。

被阅读的修复对高段加上字段掩码,并针对临界数值验证最终执行结果。这是处理有限位宽语义,不是数学上可以随便丢掉高位。

越界检查必须在第一次写入之前

另一处修改把“立即数超过支持宽度”的检查提前,放在追加伪指令之前。原来的控制流可能先记录高层加载意图,再在展开过程中抛出异常;这样失败以后,真实指令流与伪指令流可能不一致。

提前检查让这一类非法调用不改变两种输出。它是一条局部强保证:这个参数错误发生在副作用之前。它不能推出生成器所有失败都具备回滚能力,因为其他错误可能在发射多条合法指令之后才出现。

设计这类接口时,先做便宜且决定性的验证往往很划算。既减少清理负担,也让负例测试的期望更明确。不过,某些依赖后续上下文才能判断的错误仍然需要延迟处理,不能靠“全部前置”一句话解决。

为什么只对照黄金字节还不够

相关测试同时做两种验证:一组临界输入与预期机器字完全比较;另用一个很小的标量执行模型计算这些指令的结果,确认装载后寄存器等于原始位模式,并对更多采样值重复验证。

二者回答不同问题。黄金字节检查编码是否发生预期之外的变化;执行模型检查展开结果是否恢复原值。若二者来自同一段错误算法,也可能共同犯错,所以教学实验应尽量用独立参考逻辑。

值得注意的是,这个小模型只覆盖它认识的指令子集,不能被称为完整设备模拟器。明确模型边界,比给它一个宏大的名字更能保护结论。

枚举值也需要“完整解析”

同一组边界改动还为规约统计种类增加合法枚举检查。一个 C++ enum 对象不保证运行时只能包含声明过的值:显式转换、反序列化或损坏的配置仍能带入未定义选项。

如果分支只处理已知种类而没有拒绝路径,非法值可能落到默认算法,产生看似合法却语义错误的结果。与寄存器解析一样,问题是不能把“类型看起来像”当成“值已经验证”。

循环次数也有类似情形:字段在大整数类型里能保存,不代表目标标量装载能够表示。验证应放在启用该功能的入口,区分未使用的配置字段与实际要编码的参数。

一个实用的边界选择方法

边界测试不必盲目堆积随机数,可以先列出算法发生分支的位置:是否能由单条短立即数指令表示,低段何时从正数变成负数,高段何时发生进位,全字宽何时达到上界。对每个位置取前一个、当前和后一个值,通常比均匀抽样更容易暴露问题。

字符串同理。先从一个合法名字开始,再分别添加尾字符、前导空格、符号、超长数字和不同前缀。每次只改变一种性质,失败时就容易判断究竟是语法检查、范围检查还是别名表出了问题。若同时塞进多个错误,测试虽能证明拒绝,却未必能说明是哪条规则在工作。

还要区分接受范围和规范输出。同一个寄存器可以允许多个输入别名,但打印时统一为一种形式;这有利于黄金测试稳定,却不意味着其他别名应被拒绝。前导零是否保留,也是表示选择,而非编号语义。

最后,把测试期望写成两部分:它应该成功还是失败,以及它允许改变哪些输出状态。一个错误被正确拒绝,但把原有向量清空,仍然可能违背调用者合同。边界测试真正有价值的地方,是同时约束返回值和副作用,而不是只看有没有抛出异常。

回归矩阵与实验

类别 边界选择 应验证
寄存器语法 空后缀、尾随字符、符号、空格 整串匹配既定语法
寄存器范围 最小、最大、刚越界、极长数字 无截断、无隐式接受
立即数 低段符号翻转前后 高段补偿与低段符号正确
全字宽上界 最大模式及其邻近值 高段字段不溢出
非法立即数 超过支持宽度 真实与伪输出保持原样
枚举 已知集合之外的整数 明确失败而非默认降级

进一步建议用 W=8 或 W=12 的教学字宽穷举所有位模式,检查“展开后解释执行等于原值”;对寄存器字符串则从合法串进行插入、删除、替换变异,验证允许集合与拒绝集合。

性能讨论也应保持克制。严格解析可能避免异常转换成本,但是否改善编译时间取决于调用频率和输入分布。这里最直接的价值是让非法输入停止在正确边界,减少远处出现的神秘编码错误。

“解析成功”只有在明确了成功条件以后才有意义。对编译器而言,前缀看懂了、剩余随意,通常不是一种可以交给设备的宽容。


返回系列目录 · 上一篇 · 下一篇


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !