我知道编辑这个网站吗?

2011-10-16 - 调试的艺术

以下是来自 Johannes Schindelin 的博客文章,最初发布在 ImageJDev 网站上。

这是一个实验。我将以完全非技术的风格来写这篇博客文章,只是为了好玩。希望您喜欢它,如果不喜欢:停止阅读。

当加热器因达到所需温度而自动关闭时,总会发出闪烁的小声音,将我吵醒。墙上播放着动态的图画,百叶窗就像画家一样,用周日清晨的阳光作为颜料进行绘画。

当思绪从遥远的梦乡飘来时,我的眼睛慢慢适应了一天的开始。

如果这是一部电影,你现在会听到轻柔舒缓的音乐,训练有素的电影观看者总是将这种轻柔舒缓的音乐视为震惊时刻即将到来的信号,在这一时刻,突然有人或邪恶的东西眨眼间进入场景,音乐立即变得响亮,以便未经训练的电影观看者会抓住他们的扶手或他们的伴侣,以更稳定的为准。

就像电影里一样,但没有音乐,也没有观众,它又回到了我的身边。过去的一周一开始还不错,但最后却以一个灾难性的周五告终。这是令人恐惧的周五,一切都失败了,你无法停止工作,因为你不想在办公桌上留下太多东西,周一早上它会像你最大的敌人一样迎接你,脸上带着令人讨厌的傻笑,不需要任何语言的陪伴。那些可怕的星期五之一,我相信我可以再投入几个小时,至少解决一个问题,却在经过漫长而毫无结果的努力后放弃,午夜过后很久,失败地回家。

所以我就在那里,盯着我的墙,所有观看小灯跳舞的快乐都消失了。它消失了,因为我知道我需要调试这个程序,找到问题的根源,一劳永逸地解决它,否则我将不会享受我的周日。

倒叙。我九岁了,我哥哥很高兴他终于找到了我们可以分享的东西。他以非凡的耐心向我解释如何编写程序。他说,这是一个指令列表,计算机会严格执行这些指令。就像方向一样,当你解释如何到达某个地方时。他很兴奋并希望我分享这种兴奋。 “你能告诉我怎么去你的学校吗?”他问我。我说,这很简单,只要去大教堂,你就在那里。不不,他说,我不是本地人,其实我是一个只听得懂左、右、前三个字的人。在我的解释进行到一半时,他对我进行了赞扬和纠正,我失去了耐心。

现在我打开电脑。它用与我留下的相同的错误消息向我打招呼。根本没有意义的错误消息。

幸好昨天我被迫关掉了电脑,不然我又会感到苦恼了。很久以前我就不再咒骂机器了。毕竟,它确实——不忠实地做到了——它不能有信仰或任何其他感觉,这是好的,否则我会相信它试图让我生气——它被告知的事情。所以我骂电脑的时候其实是在骂自己。所以我不再这样做了。

在周五晚上那些毫无成果的时光里,我犯了很多错误,其中一个错误就是呆得太晚了。另一个是我无意中打开了兼容模式,支持旧版本的编程语言。突然间,错误消息消失了。但我太累了,没有意识到正是这种模式使错误消失了。嗯,最后我确实意识到了,所以那段时间也算没有那么白费。

倒叙。半年来,我断断续续地编程。这很有趣,但有时也很无聊,因为事情进展得太慢了,不符合我的喜好。部分原因是我的学习速度比我想要的要慢,但大多数时候是程序执行速度太慢。当然,我哥哥说,那是因为你不用机器语言。我问,机器语言是什么。

我做的第一件事是验证模式更改是否导致错误消息缺失。是的,如果我重新打开和关闭它,该消息也会关闭和打开。调试的很大一部分是纯粹的运气,偶然发现了一些奇怪的东西,而这些东西最终证明是导致错误的原因。

好的,第一步完成了,但是现在怎么办?此外,测试错误是否出现的时间太长。因为我可以从命令行构建并启动程序,并且命令行允许我编写一个简单的程序“执行 A,如果可行,也执行 B”,所以我就这样做了。建造它。如果有效,就开始吧。一旦启动没有错误,请使用我认为触发错误消息的程序部分。是的,这样就可以了,我仍然收到错误消息,并且测试问题是否发生的速度要快得多。 倒叙。我的家人包括我都在旅行。我们在一些朋友家里,我刚刚发现他们的儿子是一名专业程序员。我问他懂不懂机器语言。他笑着说,汇编器,是的,我知道。我以编写汇编代码为生。哦,我差点喊出来,你能教我吗,我哥哥想让我先精通BASIC,但我想了解这个汇编语言。越早越好。我的父亲对我的纠缠感到有点尴尬并道歉。那家伙试图友善地说,好吧,也许明天,无论如何我不能在餐桌上教你。

下一步。我需要减少有问题的代码来进行调查。由于设置兼容模式标志后错误就会消失,因此它一定与代码生成有关。这是我的预感,调试的很大一部分是遵循预感。在处理像计算机程序这样严格逻辑的事物时,忽略逻辑推理似乎有点违反直觉。但程序是由人类编写的。

*关于“调试”这个词的历史的一种理论是这样的:很久以前,当电子产品由电子管组成,而不是晶体管或芯片时,当微型计算机仍然占据着充满电子产品、热量和噪音的整个房间时,程序出现了问题。那时的程序只是布线方案,硬件和软件之间的区别尚未发明。即使经过双重、三重、四重检查,接线都完好,但简单的测试仍然得出错误的结果。然后一名工程师按照他的直觉重新进入计算机。过了一会儿,他回来说,我发现了这个错误,展示了一只大昆虫的残余物,它在使计算机短路的任务中牺牲了自己的生命。 *

通常我会使用一个名为“调试器”的程序,它允许我一个接一个地执行程序,并检查中间结果。但它不允许我退后一步,有些东西告诉我,我不能那样解决这个特定的问题,有些东西告诉我,在显示错误消息之前很久就出了问题,这只是一个结果。此外,负责错误消息的代码不是我的,我只是使用它,但我还不明白它到底是做什么的,深入的。在我有机会理解问题之前,我需要更好地理解代码。调试的很大一部分是学习足够的知识,以便能够理解正在发生的事情。

但首先我需要减少可能导致该错误的代码量。减少问题意味着将其精简到最本质的部分。调试的很大一部分只是将问题减少到不太可能的原因。对于我的特定问题,我认为涉及两个文件,都包含源代码,通过不属于我的代码相互交互。我将这两个文件复制到一个新位置,并删除对我没有复制的其他源代码文件的引用,一一删除,直到生成结果。我焦急地测试了错误消息(迄今为止我唯一的问题指示器)是否仍然显示。否则我也删除了包含错误的代码。是的,错误消息仍然显示。很好,否则花在提取这两个文件并构建它们上的时间就白费了。好吧,几乎是徒劳的。这将是对夏洛克·福尔摩斯最好的模仿:一旦你排除了所有其他选项,剩下的那个,无论可能性有多大,都一定是正确的。

倒叙。他可能以为我会忘记。但我真的很想学习机器语言。他说,午饭后他的防御就会崩溃,但这将用铅笔写在纸上。是的,我微笑着说道。当事情变得太复杂时,我们就会停下来,他说,仍然想让我泄气,并想把他的时间花在比向汇编程序介绍一个不到 10 岁的害虫更令人兴奋的事情上。不会的,我说。第二天晚上,我知道计算机中的一切都是位和字节,而且它比我想象的还要愚蠢:它真正遵循的指令是在如此低的级别上工作的,甚至无法将两个大于 15 的数字相乘,我真的很感谢我的兄弟首先教了我一种高级编程语言。

现在我开始从这两行中删除越来越多的代码行。我没有一一仔细地这样做,而是一口气扔掉了一半,因为我现在可以非常快速地测试我是否也删除了有问题的代码。每当错误消息消失时,我都会在文本编辑器中单击“撤消”并删除另一半。行数越来越多,代码越短,测试运行得越快。它开始变得有趣。

倒叙。我现在已经十四岁了,已经写了很多程序,更重要的是,我从阅读其他程序员的代码中学到了东西。其中一个教训是,当我阅读绘制圆圈的代码时,对它使用的公式感到困惑。就这样我知道了毕达哥拉斯。但更有趣的是函数的概念。将执行特定操作的代码包装在“函数”中,并使用我可以在调用该函数时更改的输入参数,而无需更改该函数的代码。例如,我可以用红色绘制一个圆圈,然后用绿色绘制一个圆圈,只需为颜色提供不同的输入参数即可调用相同的函数。 我将这两个文件的每一个都减少到了不到 20 行,无论我在过去十分钟内尝试了什么,如果不“修复”错误,我都无法进一步减少文件。现在是时候查看哪个函数显示错误,以及我们如何到达那里,因为真正的错误可能发生在该点之前。我在这里使用的编程语言 - Java - 使这变得相对容易。对于每个函数调用,它记录源代码中的哪一行对应于该调用。由于函数经常调用函数,而函数又调用其他函数,而这些函数又调用其他函数,依此类推,因此我们经常会得到嵌套调用列表。我插入代码来显示我怀疑发生错误的列表,然后再次运行测试。现在我知道涉及哪些功能并检查相应的源代码以获取更大的了解。

倒叙。在我学习函数的同时,我的兄弟向我介绍了编程语言C。它具有与我多年前开始编程时所习惯的相同的高级便利性,但线索是:有一个名为“编译器”的程序,可以将源代码翻译成机器语言。这与我的第一种编程语言不同,它依次读取指令并尝试解释它们。编译器的输出是全速运行的原始机器语言。

现在我知道错误发生在对所谓的“字节码”执行健全性检查的地方,这种健全性检查仅对较新版本的编程语言有意义。突然间,当我在兼容模式下构建代码时,错误消息消失了,这也是有道理的。不幸的是,这意味着错误可能出现在我的代码中,因为健全性检查不是在兼容模式下执行的。如果健全性检查正确标记了我的代码的问题,则在未检查时该问题也会存在。是时候阅读一些有关新的健全性检查的文档了。

倒叙。我哥哥让我看一些奇怪的问题。他相信,他用新的 C 编译器(全新的 1.0 版本,学生版)测试的示例源代码会产生错误的结果。它只是将华氏温度转换为摄氏度,但看起来减去32后乘以因子9/5而不是5/9。我看了一下源代码,没有发现错误,所以我建议看一下汇编代码。我们启动一个反汇编程序,该程序获取机器语言程序的原始字节并以人类可读的格式显示它。对于“人类可读”的某些定义:它用一些三个字母的“助记符”代替命令字节,这是所有汇编程序员使用的操作的通用昵称。结果很难读,但它清楚地表明编译后的代码与源代码不一致。我们发现了一个错误。

虽然Java没有机器语言,但它有类似的所谓“字节码”。它只是源代码和机器语言之间的另一层,因为 Java 可移植到许多机器语言。例如,我的计算机使用 Intel 机器语言,而我正在使用的手机则使用 ARM 语言。研究了字节码的文档——尤其是关于新版本的部分——半个多小时后,我很想看看代码,它严重扰乱了健全性检查,以至于显示了一条错误消息。

我盯着计算机,查看反汇编的字节码,与我在文本编辑器窗口中写入的注释进行比较,来回移动,慢慢开始理解健全性检查的工作原理。我尝试将检查手动应用于字节码的某些部分,并与代码的中间结果进行比较,为了这个调试会话,我告诉程序为了我的乐趣而显示这些结果。我越来越确信我的代码是正确的,并且健全性检查中出现了一些奇怪的问题。我现在已经调试了几个小时,但现在我已经在错误的踪迹上,我可以感觉到它的气味,我开始追赶它,因为我觉得我已经接近一劳永逸地解决这个问题了。然后,一眨眼的功夫,我就知道了。我知道问题是什么。我几乎解决了这个问题。调试的很大一部分,实际上是最大的一部分,是寻找问题的原因。在大多数情况下,一旦确定原因,解决方案就很简单。

事实证明,该支票不需要像我提供的那样的输入。该检查想要验证某个函数的输入参数的类型是否正确。它只是不包括对“未设置值”的检查,这通常本身就是一个错误,但在我的特定测试用例中不是。突然间,该错误消息实际上有意义了:它只是一个延迟检查,延迟测试输入参数的有效值。只是在我的特定场景中,在将以前未设置的值设置为有效值之前,不会到达代码。 在这里和那里稍微摆弄一下,我最终在正确的位置添加了对未设置值的检查。修改后的代码现在表明未设置的值不是错误的类型。我的代码运行起来,它毕竟是正确的。

添加三行代码,问题解决。

加热器发出的噪音表明它刚刚自行关闭,因为它达到了所需的温度。阳光透过沙沙作响的树叶和几乎紧闭的百叶窗,将自己投射在墙上。现在是下午两点半,我的周日才刚刚开始。这是一个美丽的深秋的日子,天空中没有一丝云彩。是时候离开床去外面享受色彩了。 免责声明:为了叙述的方便,有些事实已被歪曲或历史。与生者或死者的相似之处并非完全巧合。