一个只在掌机复刻版及之后版本出现的隐藏 Boss 顶着 32000 点生命值,离有符号 16 位整数的上限 32767 只剩 767 点余量。第二阶段它吸收全部属性,打上去的属性魔法转为治疗。治疗把血线推过 32767,计数器回绕成负数,死亡判定触发,一场按正常流程很难赢的战斗就这么结束。公开提交记录显示这个技巧被实际使用过。下面讲机理,也讲同一类累加写进今天的项目怎么防,适合做游戏后端、嵌入式存储和 C/C++ 数值模块的人。
32000 点血的 Boss 为什么怕治疗
32000 离 32767 只差 767。这点余量在正常玩法里不算什么,压掉三万二的血要靠装备和回合数硬磨,团灭一次就得重来。真正的破口在第二阶段。它吸收所有元素,你扔上去的属性魔法全变成治疗,血不降反升。767 点以内的净治疗就能把计数器送过上限,之后的数值不再由设计者决定。
吸收全属性怎么从致命设定变成回血通道
第一阶段它反制物理攻击,把一个队员的 MP 抽干。第二阶段吸收全部元素,每次受击还反制全队造成混乱。设计意图很直白,物理和属性两条输出路都堵上,逼玩家准备好兜底混乱的手段,靠回合数硬磨。
问题在于吸收属性只改了伤害方向,没有审计数值去向。治疗量照样流进同一个生命值字段,字段却没有上限检查。一个用来封路的机制,最后成了唯一能把血线推过 32767 的通道。
32767 这条线背后是什么在管事
生命值在内存里是 2 字节字段。早期主机版的反汇编里,敌人与角色的当前生命值、最大生命值地址各相差 2 字节,战斗血条与目标槽位一律按 16 位读取。有符号 16 位的正整数上限正是 32767,再大 1 就跨线。
证据边界要交代。那份反汇编覆盖早期主机版,这个 Boss 只存在于掌机复刻版及之后版本,反汇编里看不到它。把 2 字节的结论移到掌机复刻版,依据是同一套战斗逻辑的移植,属推断,未直接验证。老游戏的汇编结论在这里只作位宽佐证。
回绕成负数之后游戏凭什么判定它死了
死亡判定挂在排队标志上,不写在更新生命值的那一步。反汇编里的死亡例程遍历敌人槽位,查到排队标志位才放溶解动画并清理状态字段。负值之后由哪条指令翻这个标志、用的什么比较条件,我没有核到,只能确认负值走上了一条本该由正常流程才会走到的路径。
同一作还有一处同源的数值怪癖。属性技公式里的元素防御超过 127 时按补码形式处理,伤害算成负数,等于给敌人加血。两处共同点是取值域缺少保护。溢出瞬间界面显示成什么样、溢出后它是否还能继续行动,我没核到,不下判断。
同类累加写进今天项目该怎么防
治疗量、累计值、计数器这些写法今天照样散落在项目里。四条手段按粒度从细到粗排。
- 显式检查运算。GCC 与 Clang 的
__builtin_add_overflow(a, b, &res)把操作数提升到无限精度再比较,结果写不回目标类型就返回真。C23 起可以用标准头<stdckdint.h>里的ckd_add一族,写法可移植。它解决写入前就知道要超界,随即拒绝或夹取。 - 用未定义行为消毒器在测试期抓。Clang 的
-fsanitize=undefined含溢出检查,单独开是-fsanitize=signed-integer-overflow,配-fno-sanitize-recover让溢出即中止;位宽丢失另开-fsanitize=implicit-integer-truncation。它解决开发期就撞见那次回绕。 - 部署选项二选一。GCC 的
-ftrapv在加减乘溢出时陷波,-fwrapv假定按二进制补码回绕,两者互相覆盖。先定这段代码期望什么语义再选,别停在默认。 - 外部输入在入口处夹取范围。存档、协议、配置读来的值先夹取再进运算。有符号溢出是未定义行为,实现可以陷波也可以当作永不发生,绝不能依赖回绕。
怎么判断自己的模块有没有这类问题
看三处。有没有 2 字节或更窄的有符号血量、计数、累计值字段;这些字段的写入路径上有没有上限检查;外部输入有没有在入口夹取过。三处都松,就随时可能重演一次 32000 加 767。