跳转至

Bug案例库

bug经验谈

格式化输出解析不完整 → va_list 参数错位 → 野指针读内存”

真相

根因:自己的 klib printf/vsprintf 不支持 %c,但 am-tests 的帮助输出用了:复制printf(" %c: %s\n", ch, tests[ch]); 于是:%c 没被识别/没消耗参数,下一次 %s 取参数时,错误地把 ch 当成 char *,当 ch == 'H' 时,ch 的整数值是 0x48 于是 %s 得到“字符串指针”= 0x00000048,strlen 去读地址 0x48 → 越界崩溃 你看到的 “每次都读到 'H' 参数”其实是:循环枚举 tests 时,走到 ch='H' 那项,刚好触发了参数错位爆炸。

这也是为什么错误地址总是那么诡异又稳定:0x48 不是随机,是 'H' 的 ASCII。

关键错误

错误 1:把 0x48 当成RTC_ADDR宏问题 这方向有可能,但没有先验证一个事实:0x48 是 ASCII 'H' 这一步如果早点做,会立刻想到:这很可能是“某个字符 H 被当成指针用了”。

看到小地址(0x48、0x34、0x61…)这种“低得离谱”的非法指针,第一反应必须是: 把 int/char 当成了指针, va_arg 参数错位,结构体布局/函数原型不匹配(ABI) 错误 2:没有尽早把崩溃点缩小到“printf 格式化路径” 当时已经有 ftrace:崩在 pad_and_write / format_core,这几乎已经明示是 printf 的锅。 应该更快做两个验证: 注释掉所有 printf,看是否还能跑(定位到“就是 printf 引起”) 用最小复现:只打一条含 %c %s 的 printf

正确的 debug 复盘路线(从诡异地址开始)

Step 1:抓住“诡异地址”的语义 观察到 fault address = 0x00000048 立即做转换:0x48 = 72 = 'H' 结论假设 A:某个字符 'H' 被当成了指针。 Step 2:回到崩溃现场的调用链 有调用链(从日志/ftrace)类似: printf -> format_core -> pad_and_write -> my_slen(0x48) 结论假设 B:my_slen 的参数本应是 char*,却拿到了 'H'。 Step 3:在代码里找所有可能把东西传给 %s 的 printf 一搜就能找到帮助输出那句: 复制 printf(" %c: %s\n", ch, tests[ch]); 此时问一个非常工程的问题: “我的 printf 实现,支持 %c 吗?” Step 4:对照 format_core 支持列表 只支持:d u x s % 没有 c。 Step 5:推演 va_list 错位(这是最硬的逻辑链) 格式串:"%c: %s\n" 参数:(ch, tests[ch]) %c 未实现 → 不 va_arg → 参数列表还停在 ch %s → va_arg 取 char * → 取到的却是 ch 当 ch='H' 时 → 指针 = 0x48 → crash 完全解释了: 为什么偏偏是 0x48 为什么每次都稳定在帮助列表那儿炸 为什么你感觉“总是 H” Step 6:最小验证(你后来其实已经验证成功) 修 %c 或临时把 %c 改成 %d,程序立刻不炸,help 列表正常打印。 这一步就是“科学实验”的闭环。 Step 7:根因修复 在 format_core 增加 %c 支持(并记住 va_arg(ap, int) 因为默认整型提升)。

小地址崩溃往往不是“随机指针”,而是“把小整数当指针”(ASCII/返回值/错误 cast) printf 是 ABI/va_list 雷区:格式解析少支持一个 specifier,后面全错位

译码实现错误如何溯源报错 PC mismatch。

关键数据: DUT : ...0214 REF : ...1214 Diff: 0x1000 (4096) 逻辑链条: 锁定指令:通过反汇编定位到 0x80000854 是一条 beq (B-Type)。 分析立即数:目标偏移是 0x9BC (正数)。 异常:0x9BC 的二进制是 ...1001 1011 1100。第 11 位是 1,第 12 位(符号位)是 0。 定位:代码 SEXT(..., 12) 错误地以第 11 位作为符号位进行了扩展,导致正数变负数 教训:用DiffTest诊断指令行为 在 DiffTest 里看到 DUT 和 REF 的差值是一个非常整齐的 2 的幂(如 0x1000, 0x800, 0x10000)时,大概率是符号扩展(SEXT)或位宽截断的问题