调试小故事(3)2周时间找到的 Bug, 只用一行代码解决

来自:Quora网站,作者 Udayan Banerji “作为程序员,你解决的最有趣的问题是什么”的回答。

当年我在 Intel 做编译器工程师的时候,曾经遇到过一个离奇的 Bug。简单的说,一个用于测试设备性能的安卓应用程序会随机崩溃。程序非常简单,界面上有个按钮,按下后会开始测试运行一段时间。

1.我没有这个应用程序的源代码,只能看到它编译后的字节码(Bytecode)。首先我在调试器中测试,第一次没有问题,随后也没有问题。前后至少跑了30次都没有看到崩溃的现象;

2.在调试器之外运行程序才能看到随机崩溃的问题。经过观察运行20次左右程序会崩溃一次;

3.我在字节码中搜索20这个数值,所有包括20次循环,20次递归的字样。没有发现任何潜在的问题,程序仍然会崩溃;

4.我发现一个更糟糕的现象:如果在这个安卓手机上使用 USB 键盘,问题会消失;

5.经过一个周末的休息之后,我重整旗鼓,再次投入这个问题中。这次从崩溃后的 Java 环境中入手。从日志看起来,出现问题时有一个断言(assert)错误:一个大浮点数不等于 NaN ("Not a Numnber")

6.于是我返回继续在字节码中查找浮点数除法的相关内容。我在字节码中一个又一个的检查了所有的浮点运算相关代码。然后将代码转化为 x86 汇编语句,放在一个循环中执行测试。最终,我发现了一段运行20次就会崩溃的代码。这一刻我的心情犹如“鸟渴催宵漏,鸡鸣引曙光”描述的一样;

7.接下来我逐步分析这段汇编代码,发现了8个处除以0的操作。真相只有一个!一个数除以0结果是无穷大,这在电脑中被视作一种异常,同时无穷大在浮点运算中会被记作NaN。但是,还有未解的疑问!

8.手工测试除以 0 的汇编代码并不会出现崩溃。写一个除以0的运算,并且循环20次仍然不会崩溃;但是当我在这20次循环之后继续做一些运算,结果是错误的。

9.拼图只差最后一块;

10.我祭出了大招,打开 dbg 开始观察CPU 寄存器;

11.观察发现 x87的堆栈在缓慢增长,直到它最大容量(8 个浮点数),这里提到的x87 意思是 8087,它是专门用来进行浮点运算的辅助处理器,作为 8086的数字协处理器,很早之前就出现在现代电脑的 CPU 中;

12.x87对于除以0产生的异常硬件无法自动处理,在编译生成代码的过程中编译器会将所有浮点运算相关内容转化为 x87 的指令,同时还会添加处理除以0这种异常的相关代码,但是这次编译器的除以0代码没有清空 x87 的堆栈。

13.当 x87 的堆栈溢出后,没有抛出错误,而是对于所有浮点运算都返回 NaN。前面我们已经知道NaN也是除以0得到的结果(堆栈溢出是一种堆栈错误。当发生这个错误时,必须在编译器中清空堆栈,否则它会持续发生)

14.所以,当发生8次除以0之后, x87 堆栈满了,所有的后续在 x87 上的操作都会被视作除以0错误返回 NaN.

15.最终的解决代码只有一行:在处理除以0 的路径上增加清除 x87 堆栈的一行操作。

本文来自:

https://www.forbes.com/sites/quora/2017/11/14/the-most-interesting-bug-ive-fixed-in-my-programming-career-to-date/?sh=261e5509b826

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注