战争故事:我调试过的最难的错误
我在谷歌文档团队工作时,我们每周都会进行一次错误分流,寻找新问题并随机分配给队友进行调查。有一周,我们发现了一个新的最大错误。
这是一个致命错误。这意味着用户在不重新加载的情况下无法进行编辑。它与谷歌文档的发布不符。堆栈跟踪添加的信息很少。用户投诉并没有出现相关的激增,因此我们甚至不能确定是否真的发生了这种情况,但如果真的发生了,那就非常糟糕了。从特定版本开始,该问题仅在 Chrome 浏览器上出现。这并没有听起来那么有用,因为我们经常编写针对特定浏览器的文档错误,这些错误只影响 Internet Explorer、Firefox、Safari 和 Chrome 浏览器中的一个。
我尝试在开发中重现。这样做有两个重要原因:
- 排除当时 Docs 的 JavaScript 编译器 Closure Compiler 的影响。
- 在未经优化的代码中进行调试总是比其他方法更容易。
好吧,我该如何开始呢?我翻阅了我们的日志,寻找曾经遇到过这个问题的内部用户。我希望有人能告诉我:“哦,是的,每次我试着做 $foo 的时候,它都会坏掉”。但没有任何内部用户受到影响。回到绘图板。
我疯狂地编辑了一段时间。我尽可能多地添加了一些深奥的功能,从新闻网站上复制/粘贴了一堆东西到 Docs 中,试图引发问题,还玩了一会儿表格。没有成功。
接下来怎么办?当时,Docs 有一个基本的脚本工具,可以执行重复动作。它主要用于性能基准测试,但因为它能提供一致的行为,所以我试用了一下。我制作了一个 50 页的文档,其中充满了 lorem ipsum,然后让脚本对整个文档进行加粗和取消加粗 100 次。在第 20 次左右,它崩溃了。我检查了控制台,发现就是这个错误!
我又做了几次。并不总是在第 20 次迭代时发生,但通常会在第 10 次和第 40 次迭代之间的某个时候发生。有时它从未发生过。好吧,这个错误是非决定性的。我们开了个坏头。
我在考虑重现案例。大段文字的加粗和取消加粗有什么有趣的地方吗?实际上有。在许多字体和许多文本样本中,加粗文本比未加粗文本更宽。我使用的字体也是如此。因此,这可能与包装大量文本行有关。
我设置了一个断点并开始调查。崩溃看起来是由视图中的一些错误记账造成的,因为实际的崩溃是读取了一个垃圾缓存值并试图对其进行操作,结果导致了崩溃。
当时1,Google Docs 并没有像你想象的那样生成 HTML 页面。它将屏幕上的所有内容都置于菜单下方。为了实现这一点,Docs 拥有一个完整的布局引擎,可以在每次按键时运行。为了在 2010 年代的浏览器中实现这种性能,视图中的所有内容都被缓存到了极致。
这意味着什么?崩溃的地方是错误的下游。错误发生了。然后对错误进行了一些操作。然后,一些累加器接受了错误的值。然后,它们被写入缓存。最终,过了一段时间,出现了足够多的记账错误,导致崩溃。
如果把所有情况综合起来考虑,这是最糟糕的情况。错误是不确定的。那到底是什么原因造成的呢?非确定性累积的子像素渲染错误?杀了我吧 其次,重现速度很慢。光是加载编辑器的开发版本就花了大概 20 秒,触发问题又花了 40 秒。然后,我需要检查状态,直到找到错误的东西,再想办法在错误的东西被缓存或添加到队列的那一刻设置断点。
如果你没有调试过很多神秘问题,这就不是你想要的。通常,你会希望通过二进制搜索来调试神秘问题。
- 问题出在客户端还是服务器上?客户端。
- 问题出在模型还是视图中?视图
- 问题出在用户界面渲染还是通用视图处理?通用视图处理。
- 以此类推,直到找到问题所在。
然后你会不断地将可能导致问题的因素减半,直到问题明显发生在某个组件中,然后这个 bug 的日子就屈指可数了。
还有一件事对我不利。在此之前,我主要负责服务器、模型和网络代码。视图是当时应用程序中最复杂的部分,而我远不是视图方面的专家。于是我叫来了一位同事,他实现了视图的很多功能。因为已经过去 12 年了,所以我已经记不太清了,但我们的对话是这样的:
我:“我正在调查那个视图崩溃问题,它突然成了我们的头号漏洞”
他: “你有关于视图的具体问题吗?我现在有很多事情要处理”。
我:::给他看了重现程序和我目前的发现::
他: “我会清空我的日程表”
就这样,我们坐了整整两天,慢慢地把我们的断点越撞越靠后,离原因越来越近。
大约一天半后,我们有了突破性进展:罪魁祸首出现在一个特定的记账代码块2 中。它位于代码中更新累加器值的部分。于是,我们像以前一样,更新了断点,使其提前触发。我们重新加载文档并执行重现步骤。最终,断点被触发了。我们盯着函数中的变量值。一定是现在就发生了。有什么地方出错了。
计算结果不符。我的同事添加了一些日志语句,然后我们重新加载并再次运行重现案例。这仍然说不通。
我指出函数中间的 Math.abs()
调用。”我们能记录这个 Math.abs()
调用的输出值吗?我们争论这是否值得花时间,但他承认,如果它以某种方式返回负值,实际上就能解释数学。
我们重新运行了重现程序。我们查看了记录值。Math.abs()
返回的是负输入的负值。我们重新加载并再次运行。Math.abs()
返回的是负输入的负值。我们重新加载并再次运行。Math.abs()
对负数输入返回负值。
我们开始猜测为什么会这样。我们检查了函数是否被重载。该函数仍然是内置函数。我们盯着这个函数的每一个字符。一切看起来都很正常。
然后我们请来了我们的技术负责人/经理,他以人类 JavaScript 编译器而闻名。我们向她解释了我们是如何走到这一步的,Math.abs()
返回的是负值,以及她是否能发现我们做错了什么。在说服她我们并没有犯什么大错后,她坐下来看了看代码。她的 CPU 转速达到了 100%,一边盯着代码,一边用俄语嘀咕着解析树什么的,并向调试控制台输入信息。最后,她向后靠了靠,宣布 Math.abs()
绝对是为负输入返回负值。
现在,在谷歌工作的一大好处就是:可以走后门!我联系了 Chrome 浏览器的一位联系人,询问我应该如何找到问题的答案。他们给了我一些烦人的说法:”从技术上讲,这是 V8 的问题,不是 Chrome 浏览器的问题。我在链条上跳了一环,要么提交一个 V8 bug,要么去问 V8 团队的人。我真的不记得是哪个了。V8 团队立即给我指出了他们错误跟踪器中的一个错误,而这个错误已经处于 “修复 ”状态。
到底发生了什么?显然,V8 最近重构了他们的优化通道。根据我的记忆,这就是问题所在:
V8 有两级优化。
- 一个是大多数代码使用的基本级别。编译过程非常快,但优化程度不高。
- 针对热路径的超级优化级别。要了解什么是热路径,可以创建一个 50 页的 Google 文档,并加粗和取消加粗 20 次,想象一下每个单词多次运行的函数需要运行多少次。
在重构时,他们需要为每个操作码提供新的实现。有人不小心把 Math.abs()
变成了超级优化级别的标识函数。
但没有人注意到,因为它几乎从未运行过,而且在运行时有一半时间是正确的。
我们确信已经找到了问题的根源,于是添加了一个特定 Chrome 浏览器版本的临时浏览器检查。如果是该版本的 Chrome 浏览器,它就会在内联中手动执行 if 语句,从而完成操作。我们还添加了一个很长的注释(附带引文),解释 math.Abs()
在该特定 Chrome 浏览器版本中可能返回负值,这是因为 V8 版本出现了回归,请在该 Chrome 浏览器版本的使用率降低到足够低时删除该注释。
就这样:花了 2 天时间发现了一个已经修复的问题,而这个问题本可以在没有交互的情况下得到解决。作为时事通讯的作者,我还能做些什么呢?通常情况下,我喜欢寻找可资借鉴的经验。但花了 2 天时间进行艰苦的调试,却没有找到任何可借鉴的经验。
好吧,这就是生活。这不就是终极一课吗?
1.在我离开多年后,他们将所有内容都改成了画布渲染。从理论上讲,这本可以让他们大大简化布局代码,而我在这里写的东西却依然适用。
2.据我 12 年后的记忆,我们的视图将呈现过程分解为 “任务”,任务可以 “窃取 ”或 “赠送 ”额外的字符给相邻的呈现任务。我不记得这为什么有用了。每个任务都会执行相关代码,并计算出当前被盗或被赠送的字符数。
你也许感兴趣的:
- 【程序员搞笑图片】编程大牛的回答
- 码农不重视文档:开源项目深受其苦
- 阮一峰:中文技术文档的写作规范
- 这样的代码才是好代码
- 谷歌将从下周起完全闭门开发 Android 操作系统
- 谷歌抛弃滚动加载——重新采用「分页」显示搜索结果
- 【外评】泄露API文档揭示谷歌搜索如何把守互联网大门
- 【外评】谷歌搜索 API 文档泄露
- 【外评】披萨上的胶水?两只脚的大象?谷歌人工智被媒体嘲讽
- 【外评】谷歌云计算 VMware 引擎 (GCVE) 私有云宕机事故
(免责声明:我认识 OP IRL。)
我看到很多评论说 “才 2 天?这个 bug 肯定没那么严重”。我有一些想法:
在我目前的工作中,我们的事后总结模板会问 “我们哪里幸运了?在这个例子中,作者无疑是幸运的,因为他们在谷歌工作:1)有足够多的用户持续产生这个 Heisenbug;2)他们可以直接接触到 Chrome 浏览器的开发人员。
此外,作者(和他的团队)在 2 天内分流、根治并修复了一个 JS 编译器 bug。要找出浏览器代码中可能出错的地方,其复杂程度令人咋舌。他之所以 “只 “花了两天时间,是因为他非常非常擅长自己的工作。
修复天数是衡量一个错误有多难的一个很奇怪的标准。很明显,它是由大量与 bug 本身无关的因素决定的,其中包括经验,以及你是否必须单枪匹马,或者你是否能与合适的人交谈。
这个错误符合棘手错误的大多数条件:
* 非确定性
* 巨大的干草堆
* 意外的 “1+1=3 “型错误,其原因不在代码本身
如果花 30 个小时才能重现,调试起来肯定会更慢,而且调试时他必须在桶里从尼亚加拉瀑布往下跳,但我不太确定这些算不算。
前几年[1] 我也遇到过类似的 bug,它与 GraalVM JVM 中的错误优化有关,导致在极少数情况下出现奇怪的行为。如果我当时坐在甲骨文公司合适的 JVM 工程师旁边,我相信我们几天就能解决这个问题,而不是像我一样花上几周的时间。
[1] https://www.marginalia.nu/log/a_104_dep_bug/
我很想看看你其他的验尸模板!我从来没想过要加上 “我们哪里幸运了?”这个问题。
我最近意识到,我的一个问题应该是:”你恐慌了吗?恐慌的结果是什么?是什么导致了恐慌?
我破坏了一个网络,而这个设备把我引向了一条需要使用多个应用程序和多次登录才能重新获得访问权的途径。我惊慌失措,由于网络很小,我漫游并将所有设备转移到我的备份网络。
第二天,在毫无压力的情况下,我意识到自己的错误在于扫描二维码时偏离了正确方向 90 度。我并没有意识到二维码有正确的方向,而是认为二维码的角标识符可以处理任何方向。然后就轻而易举地进入了那个设备。我甚至无法复制其他奇怪的路径。
我最喜欢的 man 页面之一是 scan_ffs https://man.openbsd.org/scan_ffs
Google 推荐的标准 SRE 有一个幸运版块。我们也倾向于用它来讨论倒霉的事情。
一个好的部分是关于您遇到的概念/流程问题的部分,我认为这是对您关于恐慌问题的概括。
例如,你可能对系统的操作有误,导致中断时间延长或恢复复杂化。又或者,曾经有人在 Slack 频道的评论中粘贴了一些复杂的命令,当 PM 和 PO 正在请求更新时,你不得不使用 Sloogle™ 来寻找这些命令。或者,你最终拯救了整个团队,因为那一周你走过了很多兔子洞,但你不能指望团队中的其他人也能像你一样有灵光一现的洞察力。
这可能是有价值的信息,在被遗忘之前将其记录下来或添加到培训材料中。很多事后分析都关注根本原因,这很好,也很有必要,但却没有仔细研究试图止血的过程。
不,二维码是自动定向的[1]。如果你在不同的方向读取到不同的读数,说明你的扫描仪有问题。
[1] https://en.wikipedia.org/wiki/QR_code#Design
似乎确实可以设计出根据不同方向扫描出不同结果的 QR 码,尽管它们看起来有点明显的畸形。
https://hackaday.com/2025/01/23/this-qr-code-leads-to-two-we…
>我没有意识到 QR 码有正确的方向,我以为它们的角标识符可以处理任何方向。
同样,我以为它们的设计是始终有效的。我怀疑是你使用的应用程序或库在设计上没有正确处理它们。
想象一下,如果你不是在谷歌工作,却试图说服 Chromium 团队相信你发现了 V8 中的一个错误。那可能几乎不可能。
我注意到的一点是,Google 没有办法直接询问用户 “嘿,你有问题吗?”,这是他们软件开发方法的一个明显缺点,因为用户和开发者之间完全没有沟通。
我猜想,通过贬低他人的工作,可以让评论者自我感觉良好。作为一般规则/观点。
>在这个例子中,作者无疑是幸运的,因为他们在谷歌工作,1)有足够多的用户持续产生这种 Heisenbug,2)他们可以直接接触到 Chrome 浏览器的开发人员。
我不确定这是不是真的幸运。
解决方法就是不使用 Math.abs。如果他们不在 Google 工作,也会进行同样的调试并使用同样的修复方法。在谷歌工作可能会损害他们的利益,因为一旦他们发现 Math.abs 无法正常工作,他们本可以立即使用 `> 0` 而不是询问 Chrome 浏览器团队。
慢慢添加 printf 语句,直到理解计算机的实际操作,这并不是什么幸运的事,这只是一种好的工作方式。
我希望能更好地回忆起细节,但这已经是 20 多年前的事了。大学时,我曾在 Bose 实习,为其旗舰音响的新型多 CD 换碟机附加装置的固件做 QA 工作。他们给我们提供了不同特性的音乐光盘。我们不得不一遍又一遍地听,一遍又一遍地做,一边做一边运行质量保证管理部门提供的测试用例。但一旦我们完成了特定构建所需的测试,还要进行随机的临时测试。
有一次,我发现了一个 Bug,如果你在一个非常特定的时间按下遥控器上的一连串按钮–我想说是在新曲目开始时按了两次 “下一曲”–整个设备就会崩溃并重启。这简直是一场闹剧;如果他们价值 500 美元的音响因按下 “下一首 “而崩溃,人们会大吃一惊。与这篇文章类似,该产品的工程负责人清空了他的日程表,以重现、发现并修复这个问题。他确实解释了当时的情况,但具体细节我已经记不清了。
总的来说,工作无聊得令人难以置信。同样的几首曲子我听了太多遍,简直开始在梦中听到它们。因此,通过跳出测试用例的框框,找到一个新颖的、严重程度最高的错误,是一件很酷的事情。找到问题让我感觉很棒!在修复过程中,我觉得负责人掉了 20% 的头发,笑死我了。
我已经很久没有把 QA 作为工作头衔了,但这份工作确实给我上了重要的一课,让我学会了如何跳出常规路径进行测试,以及如何为开发团队撰写可重现且有用的错误报告。向所有薪水极低、不受赏识的 QA 人员致敬。这门学科没有得到更多的尊重,真是糟透了。
这就是优秀的 QA 工作。这也说明了为什么 QA 应该在更多组织中真正发挥作用,而不是成为一门萎缩的学科。工程师们喜欢喜欢喜欢测试幸福之路。
这甚至不是因为他们的恶意/懒惰,而是他们对问题/需求的整个解释驱动了他们的实现,然后又驱动了他们的测试。这就好比要求餐馆自我认证是否符合食品安全规范。
如果你不遵循幸福之路,100% 的情况下都会出问题。这就是为什么工程师总是遵循快乐路径。有些工程师甚至认为,任何超出快乐路径的事情都是例外,根本不值得研究。只有当用户无法转向其他产品时,这些工程师才会茁壮成长。只有竞争才能带来更好的产品。
我最喜欢的 “快乐路径 ”开发人员。他在这方面比与我共事过的任何工程师都要差 10 倍,他的做法如下:
规格:允许内部 BI 工具向用户发送计划报告
实施:服务器要求用户的桌面前端当天必须打开,这样计划报告才能生效,即使服务器端发送的是邮件
为什么说这糟透了–拥有这项功能的唯一原因是当用户长时间不在办公室/不在办公桌前时,恰恰是他们当天可能没有打开桌面 UI 的时候。
我最喜欢的例子之一就是工程师如何把问题的前提全部弄错。
最后,他花了很长时间,而且非常顽固,桌面支持团队发现,把桌面用户界面安排在 Windows 调度程序中每天自动打开,这样整个 Rube Goldberg 计划报告就能正常工作了。
您找到了一位 1× 工程师;能保住工作的最差工程师。
随着时间的推移,我们发现他实际上更像是一个 -2x 工程师,但这是另一个故事
编辑:让我想起了一个老笑话
一个程序员被他的妻子送到商店。他妻子说:“买一加仑牛奶,如果有鸡蛋,就买一打。”
程序员带着 12 加仑牛奶回到家,说:“他们有鸡蛋。”
这个傻瓜,他应该买 13 加仑的牛奶
啊,”和 “对我来说毁了这个笑话,因为它表示一个单独的分句。我认为原文是:
>一个程序员被妻子送到商店。他妻子说:”买一加仑牛奶。如果有鸡蛋,买一打”。
“反工作”–他们每工作一小时,就会占用其他工程师一个多小时的时间,用于提问、开会和后期修复。
正确–他们干坏事的速度很快,这就需要 2 倍的维修时间
>更像是一个-2 倍的工程师
你只需要再找一个像他这样的人,砰,+4 倍。
(其实可以想象,如果两个糟糕的工程师能够相互占据足够的空间,那么他们之间的关系大多可以相互抵消,但这并不是最有可能的结果)。
老实说,他的工作听起来根本不涉及幸福之路。
>如果你不遵循快乐之路,100% 的情况下都会出问题
不,这意味着你在处理早期的 alpha、被操纵的演示,或者是某种 vibe 编码的无稽之谈。
胡说,你根本没有描述任何工程。
我的意思是,众所周知,大多数软件 “工程师 “都不懂工程,但你描述的是一个我从未见过的人。
>这就是伟大的质量保证。这也说明了为什么 QA 应该在更多的组织中真正发挥作用,而不是成为一门萎缩的学科。
作为一名软件工程师,我一直为自己在测试代码时的彻底性和对细节的关注而感到自豪。然而,优秀的质量保证人员在审查错误报告时,总会让我怀疑 “他们怎么会想到这样做?
质量保证既是一种技能,也是一种心态。
我的一个朋友因为在眼镜店工作时一遍又一遍地看一些电影而几乎患上了创伤后应激障碍。那是轮换制,这样他们的顾客就可以衡量自己的视力。
我想空姐们对三角洲百老汇表演视频已经非常厌倦了。
迂腐地指出一些探索性测试 “测试用例之外的测试 ”与质量保证之间的区别,质量保证是建立流程/程序,其中一部分应该是 “进行探索性测试以及运行测试用例”,但测试与质量保证之间的区别已经争论了几十年…
不过,我很喜欢这个故事,而且我一直在收集这样的故事,所以感谢您的分享
写得有趣,但 2 天调试 “史上最难的 bug”,虽然准确,但似乎有点夸张。
不过,abs()返回负数倒是很搞笑…… “你只有一个工作……”
在我看来,最难的 bug 是几乎无法重现的 “海森虫”,当添加仪器后就会消失。
我说的也不仅仅是并发问题……
这种 Bug 的重现需要一周时间,由于硬件限制,无法并行处理,而记录仪器会使其消失或以不同方式失败。
2 天倒是挺可爱的。
这种错误的重现需要一周时间,由于硬件限制,无法并行处理,而日志工具会使其消失或以不同方式失败。
我调试过的最难的一个 Bug 需要几个月才能重现,而且只会出现在团队中只有一个人拥有的硬件上。
在一个非常成熟的产品上工作的一个有趣之处在于,错误往往非常罕见,但那些罕见的错误一旦出现,调试起来也非常困难。2 小时、2 天和 2 周的 bug 早就被调试出来了。
这让我想起了我邻桌的一位前同事,有一天他突然感叹说,他刚刚修复了一个自己 20 年前创造的 bug。
实际上,这个错误在某种程度上非常有趣:它出现在显示某些工业设备电子箱内部温度的代码中。字符串转换将温度变量视为无符号 int,而实际上它是有符号的。一位勇敢的现场技术人员在芬兰的冬天,在一个没有暖气的地方检查设备时,才发现了这个特殊的错误,因为设备的内部温度通常比环境温度高出 20 摄氏度左右。
在温度读数方面,这是一个出人意料的常见错误。特别是当系统具有热安全断电功能时,如果温度高于某个温度就会触发断电,但却将 -1 摄氏度解释为实际为 255 摄氏度。
澳大利亚维多利亚州的新居民水表仍在推广中,但已对水温进行了修正。
在今年之前,它们只能处理 0-127 度的水温。这在过去是合理的,但在开始向住宅输送加压水时出现了一些问题,导致报告的温度为负值,如零下 125 摄氏度,这就需要立即关闭水闸以防止结冰问题。
软件方面也从 COBOL 转向了 Ada。真不错。
奇怪的是,我在新奥尔良郊区的沃尔格林招牌上见过这个。
我哥哥是一家 hw 制造商的 wifi 专家。他曾经遇到过这样一个案例,客户将发射功率设置为法定上限的 100 倍。他们恰好是一个海上钻井平台,由于天线基本上是在海上的浮标上,因此发射功率可以豁免。他不得不说服开发人员修复这个非常特殊的错误。
在我从事成熟硬件产品的维护工作期间,如果我想一想我们不得不关闭的客户 Bug 的数量,原因是这些 Bug 无法重现,或者只在特定设置中出现了很短的时间,那真是令人难堪,我们感觉自己就像一群新手。
布莱恩-坎特里尔(Bryan Cantril)几年前曾就这一现象发表过一篇名为 “一路斑马线 “的演讲
https://www.youtube.com/watch?v=fE2KDzZaxvE
作者在此!当我还是一名软实时机器人系统的系统工程师时,我调试过相当多这样的问题,但现在回想起来,没有一个问题让我感觉那么糟糕,因为你只是在阅读系统的相关知识,并反复琢磨,最终你会在一次淋浴的思考中得到答案。也许我只是觉得其中的谜题很有趣,我不知道为什么它们感觉不是那么糟糕。这只是一次耗时两天的蛮干,结果发现该死的编译器坏了。
我也曾在评论中说过我对这件事的看法,但还是想问一下:
关于 “令人筋疲力尽的 2 天野蛮开发”:这是你喜欢的完成工作的方式,还是存在 “不要做其他事情 “的外部压力?我从未在大公司工作过,很多关于工作方式的描述对我来说都很陌生:)。我也习惯于说 “今天解决不了这个问题,最好还是先干点别的,睡一觉再说”。
致命错误的数量是如此之大,以至于我们别无选择,只能非常详细地了解问题,这样如果问题出在我们这边,我们就可以解决它;如果问题是由编译器或浏览器之类的东西造成的,我们就可以避免它。
我们团队的文化也很 “磨人”,所以 “我要加班加点,专门处理我们最头疼的问题 “是一种很正常的行为。在我离开那个团队(以及谷歌)后,我后来所在的大多数团队对非故障的进度都更加宽容了。
谷歌文档有回滚到早期版本的选项吗?或者对一小部分用户进行 A/B 测试?
(感谢您的战争故事!)
这就说得通了。感谢您提供的额外信息!
>虽然 abs() 返回负数很搞笑。
在 Java 中,Math.abs(Integer.MIN_VALUE) 会非常严肃地返回 -2147483648,因为 2147483648 没有 int。
你启发我检查一下 .NET 在这种情况下是怎么做的。
它抛出了一个 OverflowException:(”否定一个二进制数的最小值是无效的。”)
哦不,Pytorch 也做了同样的事情:
a = torch.tensor(-2*31, dtype=torch.int32) assert a == a.abs()
numpy 也是这样。还有 tensorflow
Rust 在 release 中也是这样做的,尽管它在 debug 中会惊慌失措。
未检查的整数溢出再次发作。
这里也一样,我们遇到了一个 IE8 Bug,它阻止了屏幕阅读器(JAWS)的初始语音播报。因为我们都打开了 DevTools,所以没有开发人员能重现它。
我也遇到过类似的问题,在我的机器上测试时工作正常,但我打开了开发工具查看任何潜在的问题。
结果发现,在打开开发工具之前,IE8 不会定义控制台。这让我很头疼。
我现在已经记不起实际的 bug 了,但我早期职业生涯的记忆之一就是通过使用书签来 alert() 值来查找 IE7 问题。(IE7 有开发工具吗?)
IE6 和 IE7 有一个可下载的开发人员工具栏,脚本可以在外部的 Windows 脚本调试器中调试。开发人员工具栏甚至会告诉你哪些元素应用了著名的 hasLayout 属性,这完全改变了元素的渲染方式以及与其他对象的交互方式,这一点非常宝贵。
对我来说,最难解决的错误是几乎无法重现的 “海森虫”(Heisenbugs),它们会在添加仪器后消失。
我最喜欢的一个 bug(具体来说,是堆栈损坏),只有在使用仪器后才能发现。经过大量的调试,结果发现这个 bug 是仪器软件本身的问题,它在某些条件下生成了无效的程序集(调用它自己的一个函数时有 5 个参数,尽管它只需要 4 个参数)。升级到最新版本后,问题得到解决。
“对我来说,最难解决的错误是几乎无法证实的 “Heisenbugs”,这些错误会在添加仪器后消失。
我最喜欢的是那些不仅不会出现在调试器中,而且在调试器中仔细观察后,在正常设置下也不会再出现的错误(只是后来在某个随机时间又出现了)。感觉就像在追逐幽灵。
术语建议: “Gremlins” 🙂
这种重现每天都会有几次,但当你的简历中甚至没有 C/C++ 的时候,当最初设置东西的人都离开了的时候,试着修复 Linux 内核的恐慌吧……
https://news.ycombinator.com/item?id=37859771
关键是修复的难度可能来自很多可能的地方。
是的!我曾经处理过一些复杂的问题,结果发现是供应商交换了硬件,我们花了一个多月的时间试图用软件来解决,最后才弄明白。
部分原因是难以确定实际问题所在–驱动器的满载率与写入的吞吐量。
不幸的是,其中很大一部分原因是组织政治因素,比如系统跨越了两个团队,而这两个团队的报告关系各不相同,合作不畅,测试方法不佳。
> 不幸的是,很多问题都与组织政治有关
根据我的经验,最难解决的 bug 是那些你唯一的重要信息来源是第三方,而第三方却直接对你撒谎的 bug。
有时,这并不是彻头彻尾的谎言。我遇到过硬件、API 和 SDK 文档与发货产品有细微差别的问题。硬件的修订版本多种多样,有的符合文档要求,有的则不同,甚至他们的工程师也不清楚哪个是哪个。
对于类似情况,我们使用内存环形缓冲记录器,根据要求打印日志。它不保存字符串,只保存必要的数据位和格式化函数指针。向该日志记录器写入日志不会影响任何时序。
我认为用天数来计算调试时间一点都不有趣。对于新手来说,琐碎的 bug 可能需要数周时间才能调试完毕。而对于天才开发人员来说,即使不使用任何重现器,光是想想就能知道,调试难度极高的错误需要花费数小时。
在硬件中,当你探测系统时,会经常看到行为发生变化。你的示波器或 LA 探头对系统的影响足以让一个边缘电路正常工作。这绝对令人抓狂。
越接近自然科学,最终依赖逻辑故障排除就会变得 “不合逻辑”。
未定义的(错误)行为越多,你就越会抓狂。
在这种挫败感中,你本应拥有一个基于逻辑的系统,而它却冒出了丑陋的头,无论如何都违背了逻辑:
我总是把它们称为 “量子错误”,因为观察错误的行为会改变错误。绝对让人生气。我更喜欢 “heisenbug”。听起来更顺耳。
我自己的故事: 我花了 10 个小时调试一个 Emacs 项目,它偶尔会导致我的机器内核崩溃。最终原因是两个 debug-print 语句之间的非本地交互。(这不是我的第一个猜测)。Elisp 的 debug-print 函数 #’message 有两个作用:一是追加到日志中,二是在编辑器窗口的一角做一个小的更新通知。如果该窗口角落的图形用户界面对象在一毫秒内被触发几百次,就会导致我的特定机器上的 GPU 驱动程序锁定,而这是我从未从根本上解决过的问题。
Emacs 的 #’message 实现有一个去抖逻辑,如果你重复调试打印同一个字符串,它就会被重复打印。(如果你快速调用 (message “foo”) 50 次,打印出来的字符串就是 “foo [50 times]”)。因此:如果调试-打印检查一个不经常变化的变量(就像刚才的情况一样),就不会出现图形用户界面混乱的情况。当有两个**调试-打印语句处于活动状态时,错误就会出现,因为被打印的内容在两个不同的字符串之间切换,这就规避了除错器。注释掉一条调试-打印语句或另一条调试-打印语句,就能掩盖这个错误。
如果窗口角落的图形用户界面对象在一毫秒内被攻击几百次,就会导致我的特定机器上的 GPU 驱动程序锁定,而这是我从未根除过的原因。
直到最近,通过图形驱动程序让机器崩溃简直易如反掌,即使是意外。我敢打赌,其中很多都是安全问题,而不仅仅是 DoS 向量。WebGL 在鼓励制造商最终正确修复驱动程序方面发挥了巨大作用,因为浏览器宣布这种行为是不可接受的(你不应该通过一个无权限的网页让电脑瘫痪¹),并制定了长长的显卡和驱动程序黑名单,将浏览器最终确定的有条不紊的方法带到了图形领域。
虽然还不够完美,但已经比十年前好多了。
-⁂-
¹ 啊,IE6 容易崩溃的美好回忆,有些甚至会让 Windows 98 崩溃。如果我没记错的话,我最喜欢的是 <script>document.createElement(“table”).appendChild(document.createElement(“div”))</script>。这些东西并不强大。
FWIW:Chrome 浏览器中的此类错误可被利用,在 JIT 编译的 JavaScript 代码中创建越界数组访问。
JIT 编译器包含消除不必要的边界检查的传递。例如,如果您编写 “var x = Math.abs(y); if(x >= 0) arr[x] = 0xdeadbeef;”,JIT 编译器可能会删除 if 语句和 [] 操作符内部的非负数组索引检查,因为它可以假设 x 为非负数。
但是,如果对 Math.abs 进行 “优化”,使其可以产生负数,那么缺乏边界校验就意味着代码会立即访问负数组索引–这可能会被滥用来重写数组的长度,并引发更多诡计。
进一步阅读有关 Chrome 浏览器 CVE 的内容,几乎完全符合这一模式:https://shxdow.me/cve-2020-9802/
>这可能会被滥用来重写数组的长度,从而进一步作恶。
我一直跟踪到这里。JavaScript 允许你通过赋值给负数的索引来修改数组的长度?我很熟悉负索引用于访问数组末尾的东西(比如-1是最后一个元素),但我不明白有人会做什么操作来修改数组的长度,而不是就地修改特定元素。是 JIT 编译的 JavaScript 不遵循使用负索引时通常会发生的 JavaScript 语义,还是你描述的是与其他编译器 bug 结合使用的情况(老实说,即使没有通常的 Math.abs 实现,这听起来也要严重得多)。
通常情况下,我们会进行边界检查,以确保索引实际上是非负的;负索引会被视为属性访问,而不是数组访问(不像 Python 那样会缠绕)。
不过,如果 JIT 编译器已经 “证明 “索引绝不是非负数(因为它来自 Math.abs),那么它可能会省略此类检查。在这种情况下,对 arr[-1] 的访问可能会直接访问位于数组元素前一个位置的内存,例如,这可能是数组元数据的一部分,如数组的长度。
您可以阅读 CVE 概念验证示例的注释,了解 JS 引擎 “认为 “正在发生的事情与代码执行时实际发生的事情:https://github.com/shxdow/exploits/blob/master/CVE-2020-9802….。这个漏洞比我描述的要复杂一些,但使用了类似的核心思想。
我理解缺乏边界检查允许以负索引访问早期内存的想法,但我主要是在纠结为什么底层内存布局首先可以在 JavaScript 中访问。我没有考虑过同样的语法可以用于访问任意属性,而不仅仅是数组索引;这可能就是我忽略的细微差别。
>我一直跟进到这里。JavaScript 允许你通过赋值给负数的索引来修改数组的长度?
这无疑是我对你能做什么的愚蠢理解,基于我有一次为了扰乱别人的思路而做的一些有趣的事情
做以下事情 const arr = []; arr[-1] = “hi”; console.log(arr) 这样就得到了”-1″: “hi”
length: 0
我想这是因为数组其实就是一种特殊类型的对象。(我的解释,可能是错误的)
现在我们可以看到 JavaScript 数组的长度是 0,但既然可以在其中找到该值,我想在浏览器中实现 JavaScript 的低级语言中会有一些长度表示法,然后我认为甚至可以通过某种方式利用这种低级长度表示法与 JS 数组长度之间的差异来利用漏洞。(再说一遍,这些都是我想过但从未调查过的蠢事,而且在某些方面可能错得离谱)
,我记得几年前看到过对数组的一些补充,这样就可以防止在数组中存储数据时出现负索引的可能性,但我的记忆可能有误,因为我没有任何理由担心这个问题。
你提出了一个很好的观点,JavaScript 数组 “只是 “对象,可以通过与数组索引相同的语法赋值给任意属性。我完全可以想象,编译器会利用这一点进行某种优化,从而将数组直接映射到其底层内存布局(可能带有长度前缀),这样就有可能在省略边界检查的错误假设情况下提供对数组的访问。
是的,你知道你说的话让我想起了这些有趣的实验,我已经很久没做过了,我现在想起来了,你可以做
const arr = []; arr[false] = “hi”;
其中 console.log(arr); – 至少在 FF 中 – 给
Array []
false: “hi”
length: 0
这意味着
console.log(arr[Boolean(arr.length)]); 返回
hi
这很有趣,我只是觉得在这个领域的某个地方一定有漏洞,但也许没有,因为它会被很好地覆盖。
编辑:例如,由于索引可以从输出 NaN 的数字运算中实现(出于某种原因),因此你会得到 NaN:”hi”,或者由于 arr[-1] 给你”-1″: “hi”,但 arr[0 -1] 返回的却是 “hi”,这显然是在索引过程中进行了类型转换……我一直认为,在这种情况下,类型转换不可能像 a == b 那样进行
也许是我年纪大了,容易被一些事情吓到。
Javascript 是新的 Macromedia/Adobe Flash。
你可以用它做越来越多的事情,它是如此有趣,直到它突然不再有趣并死去。
这是在 jit 之后。
也就是说,别以为花哨的语言诡计能实现负索引。而是从数组内存访问开始的负偏移。
当有一些内联时,就不会有函数调用到一些索引操作符函数
例如,如果数组是这样实现的(它们不是)
因为在进行了边界校验后,加载 JS 数组中的元素可能会编译成类似 mov 的简单汇编级加载。如果绕过了边界检查,那么 mov 就可以读取或写入任何映射地址。
是的,我都明白。我想我惊讶的是,你竟然可以在 JavaScript 中访问这个结构的任意部分;我想我真的没有深入研究 JIT 编译在运行时的实际作用,因为我没想到会有这种可能。
让我想起了用户无法在 500 英里之外发送电子邮件的经典 bug 故事。
https://web.mit.edu/jemorris/humor/500-miles
只在星期三才会崩溃:
https://gyrovague.com/2015/07/29/crashes-only-on-wednesdays/
我遇到过 “如果不从制造商那里复制大部分为空的演示 Android 项目并将整个现有项目粘贴到其中,16 小时后就会崩溃 “的情况
结果发现有一个未注明的 MDM 功能,如果某个特定名称的软件包没有运行,它就会重启设备。
反编译时,它不应该处于激活状态(他们搞砸了,发送的是 MDM 的调试版本),根据变量名,它应该是 60 秒,但他们把毫秒和秒搞混了
这值得更多的人点赞。绝对经典。
我最难忘的 bug 故事,几乎可以追溯到这个词的起源。
一个实习生得到一块带有新微控制器的开发板。虽然是新一代的,但主要是向下兼容之类的。实习生用嵌入式 “hello world “启动并运行开发板。他们移植了基本的产品代码 – ${thing} 不工作。在扯了足够多的头发后,我给了他们一些指导–{东西}不起作用。好吧,我指导实习生使用 mcu 供应商库/示例,让 ${thing} 独立运行。实习生失败。
好吧,我们遗漏了一些显而易见的重要信息。我们开始结对编程,将代码逐层剥离。最终,我们进入了直接访问手工编码的内存地址的阶段。${thing} 不工作。好吧,设置一个外设并读取状态寄存器。断言失败。好的,设置外设,等待一段时间让数值稳定下来,再读取状态寄存器。断言失败。检查生成的程序集 – nopsled 就在那里。
我们查看手册,将外设切换到我们关心的状态的位没有设置。无论我们如何调试微控制器,无论我们向控制寄存器写入什么内容,该位都不会被设置,外设永远不会切换到我们需要的模式。我们换了一块新的开发板(或者在旧开发板上重新安装 mcu,记不清了),第一次尝试就成功了。
“新设备–一定是新行为 “的想法,加上不容易接触到新硬件,让我们掉进了兔子洞。是的,没有什么太花哨的东西。不过,一想到如果读取状态寄存器会返回写入的值,我就不寒而栗。
如果读取状态寄存器时,写入的值又回来了呢?
我有过这样的经历。结果发现,有些电路板没有连接移位寄存器输出和改变行为的门的线。
有趣的是,这里很多评论都说:”你觉得两天很难吗?我调试的问题是我父亲和他父亲传给我的”。这让我想起了 “四个约克郡人 ”的小品。
https://youtube.com/watch?v=sGTDhaV0bcw
当然,作者的 “错误 ”是把它称为 “我调试过的最难的 bug”。它推动了点击,但也推动了比较。
当然,评论区会充满关于每个人最难调试的错误的战争故事。
这就是人类的工作方式,也是我阅读评论的原因。
是的,当然,我非常喜欢这些故事,这也是我开设这个主题的原因。但我的评论不是针对这个的,我特别提到了评论中否定作者追踪这个特殊 bug 的难度和时间长度的部分。我觉得这很有趣,我的评论基本上就是一个大笑话。
至少作者曾为谷歌工作。以第三方的身份追踪这样一个 Bug,然后试图以某种方式联系到公司里能修复它的人,这又是另一种乐趣,尤其是当它是一家大公司时,如果产品比较老,而且只在维护计划内,那就更是如此了。
我:”在这种情况下,你们的产品对所有客户来说都是坏的,可能已经坏了好几年了,以下是确切的问题和修复方法,我能和能做这项工作的人谈谈吗?
客户支持: “您是否尝试过关闭机器,然后再重新打开?
我遇到的最严重的 bug 是,我使用统计数据尝试将发生率与流量/时间、API 请求、应用程序版本、Node.js 版本、资源分配等联系起来。失败后,我在 Wireshark 中捕获 Prod 流量进行检查……
结果发现,Node.js 并没有优雅地关闭 TCP 连接。它只是默默地放弃连接,如果对方试图重新使用连接,它就会发送一个 RST 数据包。有趣的时光
嘿,不是 nodejs 的问题,而是与 TCP 连接有关的问题。
我不会说出产品名称,因为这不是它的错,但我们有一个由 3 个实例组成的 HA 集群。用户报告说,每天的第一次登录会失败,但仅限于第一个进入办公室的人。按下登录按钮后,系统会在 30 秒后提示登录无效,然后再试着登录,接下来的一天都能正常工作。
原来,IT 部门在节点之间安装了 “被动 “防火墙(流量检测和阻断,但没有 NAT)。节点之间建立了长期运行的 TCP 连接,用于同步。防火墙内部保存了一个已知已建立连接的表,如果这些连接处于空闲状态,防火墙会最终将其丢弃。该产品打开了 TCP keepalive,但 Linux 默认的 keepalive 间隔比防火墙的超时时间长。当防火墙从表中删除连接时,它并没有向任何人吐出 RST 数据包,只是静静地停止了流量。
当当天的第一个用户尝试登录时,所有三个 HA 节点都认为它们的 TCP 连接仍然正常(因为它们没有理由不这么认为),因此必须等待连接超时,然后再将其删除并重新建立。这是一个有趣的问题…
node.js 中的网络连接愚蠢得令人抓狂,而且极难调试,尤其是在 Azure 这样的环境中运行时,端口分配可能会受到无法控制的限制。这已经糟糕到我不会考虑在任何新项目中使用 node.js。
crazy
抱怨 “复制慢”,还说_秒_。亲爱的,亲爱的,这些都是菜鸟的数字!
目前,我们正在处理一个错误,经过 3 周的自动测试和数万次重启后,我们发现文件系统损坏。我们可能再也看不到这个问题了,甚至?只发生过一次。
如果只发生过一次……它可能是最后一类无法修复的错误。宇宙射线位翻转错误。这需要你的软件能够解决,或者在这种情况下,文件系统本身也能解决……除非你真的在研究文件系统本身,在这种情况下,祝你好运。
漫游射线会影响哪些硬件层?带 ECC 的内存基本上是安全的,对吗?L1 缓存和其他硬件呢?
任何东西都有可能随时失效。我们能做的就是减少故障,并估算出故障发生的可能性。有时,这些界限是可以接受的。
我最难的调试其实与软件无关,而是我的第一辆车–80 年代末的大众帕萨特。问题是电池根本无法充电,每次使用时我都不得不跳火,或者把车停在山顶/街道上,然后启动它驶下山坡。
我买了一个全新的电池,但问题依然存在。我开始检查车内所有与电气系统相连的部件。把它们拆下来,尽我所能排除故障,最后甚至绝望地买了一个新的交流发电机和电磁阀。
3 个月过去了,我在车库里待了无数个小时,心想……会不会是……会不会是我买的新电池出了问题?又买了一块电池,一切正常。就这样。
原来我车上的电池已经老化,无法储存足够的电量。而我买的第二块(全新的)电池也出现了同样的问题。
这些故障蓄电池充电后能测量到正确的电压,但却无法获得正确的充电容量,因此汽车无法获得足够的电流来启动发动机。
别让我开始讨论电子产品的古怪世界……但汽车调试是我迄今为止花费时间最长的一次,有一次我几乎把汽车的所有部件都拆了下来,仔细检查线路。
最糟糕的就是买了新零件还不能解决问题,你很少会想到新零件会坏,尤其是像电池这种刚从商店买回来一般不会有问题的东西。
我遇到过的最糟糕的 bug 是一个一直无法运行的 JS 文件,它的跟踪非常隐晦难懂,没有任何意义。TypeScript 和其他程序解析得很好,没有任何问题。
我不知道为什么,在尝试了各种方法 3 天之后,我想到了手工逐个字符重写文件,结果成功了。到底发生了什么?
最后,我用十六进制编辑器并排打开了两个文件,结果发现:”空 “的地方出现了几个奇特的 unicode 字符。
我见过由于错误复制粘贴造成的这种情况。
我在企业系统集成工作中见过这种情况,一些数据交换规范是以 Word 文档的形式编写的,其中有一些表格定义了某些字段的有效字符串值,Word 帮忙将字符串常量中的普通 ascii 破折号替换为相当长的破折号,团队 A 用普通 ascii 手工键入这些常量,团队 B 从 Word 文档中复制粘贴出精确的 unicode 字符串来构建自己的常量。
一旦发现问题,调试起来并不难,而且完全可以避免(用纯文本编写规范)。
所以我很好奇,这是否为你的调试工具箱增添了一招?
我们遇到过一个有趣的 bug,即我们的 VPN 在 macOS 上崩溃。错误很明显,我们减去了两个时间戳,却得到了一个负值,而这是不应该发生的,因为这些时间戳来自单调的时钟。我们花了很多时间分析所有代码,确保参数的顺序正确,减法的值正确,一切看起来都很正常。
然而,我们还是从一台设备上看到了这些崩溃报告(好在这台设备是首席执行官的合作伙伴,因此我们得到了完整的调试报告)。不过,系统日志也很可疑,尤其是在睡眠状态下,时钟跳变很多。最后,我们得出结论:这是硬件(M1 Max)出了问题,操作系统过于信任它,对一个本应单调的时钟返回了失序值。我们更新了代码,使用饱和运算来缓解这个问题。
>在进行重构时,他们需要为每个操作码提供新的实现。有人不小心把 Math.abs() 变成了超级优化级的标识函数。但没有人注意到,因为它几乎从未运行过,而且在运行时有一半时间是正确的。
这就是完美的优化:速度极快,而且大部分情况下都是正确的,如果正数多于负数,正确率可能超过 50%。
一个有趣的问题:
我在一个为客户提供在线备份的服务器软件上工作。我们每天都要对某个文件系统进行数千次挂载/卸载。每隔一个月左右,我们就会遇到一次文件时间戳无法保存的问题,错误发生在文件系统级别。
很难重现!这是一个文件系统错误!所以我们只能从理论上入手,阅读代码,看看它是如何发生的。
过了一会儿,发现条件很有趣。我记不清楚了,但好像是,你需要按照以下步骤操作 1/ 创建一个文件夹 2/ 在其中创建 99 个文件(不能多也不能少) 3/ 创建一个新文件夹 4/ 将 99 个文件中的第一个复制到新文件夹中
问题与某些数据结构缓存和缓存驱逐有关。
我发现了其中的乐趣!
令人惊叹的战争故事。讲得非常好。
老实说,在所有愚蠢的想法中,让你的引擎在重载时切换到一个完全未经测试的模式,而这个模式从来没有人检查过,而且可能需要数年才能发现其中的错误,这绝对是我能想到的最疯狂的事情之一。这种做法往好了说是非常懒惰,往坏了说则是企业文化对表面性能的重视,而不是对可靠性和质量的重视。幸好没有人在航空电子设备中部署 V8。但愿如此。
至少这是一个你可以不放在心上的 bug,你可以说,这确实是一个低级问题。要证明这一点,需要花费大量的时间和精力。
我同意你对这件事愚蠢程度的评价,但我并不感到惊讶。
说白了,采用这种不同的模式是有充分理由的。最糟糕的是没有进行适当的测试。
这类模式可以通过各种方式进行正确测试,例如,可以通过覆盖开关强制一直使用所选模式,而不是使用默认的启发式模式切换。然后,除了默认配置外,您还可以在该配置下运行测试套件。
挑战在于,现在运行所有测试所需的时间至少增加了一倍。对于此类项目(如编译器),通常会有多个这样的开关,因此很快就会出现组合爆炸,即使像 Google 这样的公司也远远无法满足运行所有测试所需的资源。(想想 GCC 有多少个 -f 标志……没有足够的物理资源来运行任何测试套件的所有组合)。
我最希望看到的解决方案是随机测试。除了在每次签入和/或每天运行的单个固定测试套件外,你还可以有一个持续的测试流程,针对从 { 测试套件 } x { 配置空间 } 中随机抽取的(测试、配置)对,不断测试你的主分支。x { 配置空间 } 的随机采样(测试、配置)对进行测试。理想情况下,将其与自动分叉器结合起来,每当发现故障时,分叉器就会返回到旧版本,查看故障是否是最近的回归,如果是,则识别回归点。
随机测试不是越来越成为一种标准做法了吗?即使您有足够的硬件和时间来运行完整的测试套件,您仍然希望添加一些随机性来捕捉测试之间的意外依赖关系。
或许?我很想知道是否有一些好的工具可以集成到 Git 仓库、Jenkins 或 GitHub Actions 等典型设置中。
20 世纪 90 年代末,我的朋友正在用 TI Basic 为他的 TI-83 计算器编写一款游戏。他遇到了一个奇怪的 bug,我们在一个计算器上来回折腾了近一个小时,终于把它归结为一个 IF。IF 的行为与你的预期不符,而且毫无道理可言。在 TI-Basic 的早期版本中,运算符实际上是单个符号,而不是由文本字符组成。沮丧之余,我删除了 IF 符号,插入一个新符号,然后启动游戏。一切都正常了,我的朋友简直不敢相信。这可能是我修复过的最令人沮丧的错误了。
几年前我给别人讲过这个故事,他们说与符号相连的操作码可能会损坏之类的。
看来这是一个故事时间线。下面是我最奇怪的一个故事。
2005 年,当我还只能在网吧以现金支付的方式使用电脑时,一位店主给了我免费使用电脑的时间,条件是我必须在编译器上输入并运行一个打印在 A4 纸上、长达 15 页的 12 年级计算机项目。TurboC++。我欣然接受并开始打字。
当我输入完毕,排除了所有编译错误后,程序并没有像预期的那样运行。几个小时后,我发现打印出来的源代码中有一两页没有按原来的顺序排列。于是,我不得不把代码从一个函数换到另一个函数,最终才使程序正常运行。这真是一堂难忘的课!
店主一定把这个项目卖给了很多学生,而我也得到了一些免费上网的机会。
我们遇到的一个有趣的问题是我们当时选择的数据库的 JDBC 驱动程序。在负载情况下,应用程序核心会发生转储。要知道,这是一个运行本地 JDBC 驱动程序的 Java 应用程序,看不到任何 JNI。经过 gdb 查找才发现,在负载情况下,JIT 编译器有点激进,内联了比 JIT 缓冲区空间更大的代码–结果就是完全随机的核心转储。一旦我找到了问题所在,只需增加 JIT 缓冲区的大小并增加堆和内存即可。跟踪从 java 生成的字节代码生成的汇编程序只是问题的一部分,事实上代码本身与问题无关,这才是有趣的地方,因为缓冲区大小是由 jvm 在完全不同的区域设置的。有趣的时光
在我看来,如果在发布之前没有发现这个问题,V8 的单元测试就太糟糕了。确保所有运算符在优化和未优化时以相同的方式运行,这是毋庸置疑的。
也许吧,但我也能理解有人会合理地认为他们不需要测试 abs(),因为有什么可能会出错呢?
听起来他们的单元测试覆盖了 abs(),但并没有覆盖所有的 abs(),也没有可靠地触发优化后的代码路径:
> 在重构时,他们需要为每个操作码提供新的实现。有人不小心把 Math.abs() 变成了超级优化级的标识函数。但没有人注意到,因为它几乎从未运行过,而且在运行时有一半时间是正确的。
如果它从未被测试过,那么 “几乎从未运行 ”或 “有一半时间是正确的 ”也就无关紧要了。
因此,这里的根本问题在于,他们的测试套件既没有对所有优化级别进行适当的测试,也没有将遗漏标记为破坏 100% 分支覆盖率的致命问题(对于像 abs 这样的简单基元,你肯定希望有 100% 的分支覆盖率)。这意味着他们可能会在不知不觉中破坏很多其他东西。OP 没有讨论 JS 团队是否适当地处理了这个问题,但我们希望他们做到了。
很有道理,这是一项繁忙的工作,很容易被推迟。但代码优化是需要这种双重检查的,所以最终你应该对所有操作码都进行这种检查,然后再包括像 abs 这样的简单操作码,这并不是什么额外的工作。
对我来说,最有趣的是一个 C# 应用程序的随机崩溃。没有任何模式。没有任何功能、用户角色、软件部分或时间。我不得不学习崩溃转储分析,并买了我的第一本 Kindle 电子书(在台式机上,没有 kindle,因为我需要尽快看完它),其中一本有一个小窍门,可以让内存问题在靠近源头的地方崩溃,而不是让它在几小时后被偶然发现。这就是随机性的来源。点击按钮,崩溃。移动鼠标,崩溃。
这套系统已经完美运行了很多年,但 windows 系统升级后,一些自作聪明的人使用了巧妙的技巧,使悬浮菜单在未来(更安全)的操作系统版本中失效。很少触发的悬浮菜单
感谢《高级窗口调试》和《高级 .net 调试》的作者们。
我没有修复这个错误,但我确实重现了它,这样它就可以被修复了,不过这花了好几年时间。在我工作过的一家公司,我们有一个电子邮件档案库,我们发现客户在删除过期电子邮件时遇到的问题越来越多。大多数公司的邮件保存政策是 7 年左右,而这家公司已经成立 10 年了,早期客户开始删除旧邮件。但开发人员找不到 bug,但缩小删除范围通常会奏效,所以通常会被标记为不可重现。虽然开发人员试图调试它,但出于显而易见的原因,没有人会让我们在他们的 prod 电子邮件服务器上探查太多。
我被提升为技术撰稿人,我需要一个更好的测试系统,它不需要客户数据来截图。我需要的是独一无二的数据,因为存档使用的是单实例存储,所以我编写了一个 bash 脚本,创建并发送从古腾堡(Gutenberg)获得的公有领域书籍中随机生成的电子邮件。
这对我来说非常有效,有一次我让它发送了一百万封电子邮件,只是为了好玩。我让我的测试电子邮件服务器和归档服务器在周末处理这些邮件。效果很好,但我的存储空间几乎用完了。没问题,使用删除功能。但没有用。
没用。我在公司内部完全控制的系统上重现了这个错误。工程部和质量保证部都拿到了我的环境副本,并开始研究这个漏洞。
我还了解了删除功能的传说。创始开发者认为没有人需要删除功能,因为这对他来说毫无意义。但在首席执行官、董事会和客户的压力下,他在一个周末敲出了一些代码,并付诸实施。10 年过去了,他早已不在人世,而这也终于开始影响到我们。
开发人员敲了一会儿代码,发现有一个设计缺陷,如果要删除的项目数量超过 500 个,就会失败。质量保证部曾反复测试过该功能,但他们的测试数据集恰好小于 500 项,因此该错误从未触发过。我这么说只是因为《奥斯汀-宝沃斯》(Austin Powers)很有趣。
既然我们可以重现这个错误,也知道存在设计缺陷。删除代码需要更换。更换代码需要花费两年多的时间,因为项目管理人员从不认为它比新功能更重要,尽管客户对此怨声载道。
保留过了保留日期的东西对公司来说是一个很高的责任,我很惊讶他们没有起诉你们来更快地解决这个问题。
我甚至还比不上其他 faang 工程师,但根据我的经验,这远不是一个非常困难的 bug。最难的错误是那些需要花几天时间才能重现的错误。尽管如此,操作员的坚韧是最重要的,我相信他们能解决我过去遇到的任何难题。
你好,我是作者!在谷歌之前,我的工作就是为我们的移动机器人/计算机视觉堆栈调试这类错误,但我觉得它们很有趣,所以本身并不觉得 “难”。最耗时的一次是在一个安装了摄像头的计算机视觉系统上,花了一个月的时间。但这一路上,我们经历了 2009 年游戏笔记本电脑的热节流、深奥的 Windows API、硬件设计以及最终的分布式队列。但解决这些问题的过程非常有趣!我学到了很多东西。我讨厌那个项目,但修复那个漏洞是它的亮点。
我读过那篇博文!
谢谢你的建议,如果我能记住足够多的细节,下个月我可能会这么做!
我曾经遇到过这样的事情。
供应商提供了一个 outlook 插件(ew),可以直接在 outlook 中链接存储(双 ew),并包含一个内置的 pdf 查看器(恶心),供律师事务所管理他们的案件。
一位用户,无论使用哪台电脑、用户账户或任何其他隔离因素,都会导致程序和 outlook 崩溃。
她可以在另一台电脑上用另一个用户登录的账户工作 40 分钟,并重现这个问题。
原来,这是一个内存分配问题。当你通过内置的 pdf 查看器打开保存在附加组件存储中的文件时,它会为其分配内存。但是,当你关闭 pdf 文件时,它不会取消分配内存。在调试了一段时间后,我发现虽然有内存分配,但分配是间隔进行的。
如果有 20 个左右的 pdf 文件分配,然后她在去分配之前切换了客户案例文件,无论可用内存是多少,插件中的内存分配系统都会拉屎并崩溃。
我必须说,这个用户是个绝对的女强人,打字速度可达 300 wpm,阅读 ->关闭 ->分配 ->分配 ->写笔记的速度比我以前见过的任何人都快。在等待供应商提供补丁的过程中,我们让她把自己的速度限制在每 10 分钟 2 个文件,以此作为最初的变通办法。
我不得不给供应商写了一份错误报告,他们才肯看。当然,他们无法通过正常测试重现错误,并多次试图关闭我的错误。他们推出的第一个更新版本把这个问题提高到了每 15 分钟浏览 40 份 pdf 的程度。但她偶尔还是会触及新的上限(我想象着向每位客户收取 7 分钟的费用,或者律师事务所的做法),最终他们不得不重写整个内存系统。
这与 “站立时无法登录电脑 “的 bug 差不多……有人把 D/F 的键帽(例如)换掉了,所以当五星上将试图站立登录时,他在密码栏里输入的是 “doobar “而不是 “foobar”。
至于那位女士,如果她 “因为有人在看 “而稍稍放慢了工作节奏,那调试起来可能会很疯狂…… “只有在没有人看的时候才会发生(而且我也不是以极快的速度结案)”
我喜欢!希望你能给她颁个奖牌什么的。就像约翰-亨利大战蒸汽机。
>供应商提供了一个outlook插件(ew),直接在outlook中链接存储(双ew),并包含一个内置的pdf查看器(恶心),供律师事务所管理他们的案件。
我仍然不明白我们是如何走到今天这一步的
听着,我曾支持过几个不同的法律平台,虽然我讨厌这个角色,但它也是最棒的。
以下是律师的工作:
1. 他们为撰写电子邮件和打电话的时间计费 2. 他们为审核电子邮件的时间计费。3. 4. 他们还为与人面对面的时间计费。
他们还需要在一个空间内收集与案件有关的所有数据,这些数据大多通过电子邮件(或传真,如果他们讨厌你的话)进出。
可悲的是,80% 的数据都可以通过 outlook 轻松实现。建立一个外部应用程序来捕捉所有这些信息是相当困难的,通常需要通过某种方式运行邮件。问题是,为什么要重新发明电子邮件客户端?(可悲的是,他们重新发明了 pdf 阅读器)我看到一些律师事务所将每封邮件都保存为 html 格式,并将其与计费统计数据一起上传到第三方应用程序。这对我来说更容易支持,但用户体验可能会很糟糕。
用户已经存在于 Outlook 中,他们已经了解 Outlook。从用户的角度来看,功能区中的几个按钮(主要是将此归档到 X 打开的案例中、为我计时和为该客户计费)更有意义。
从支持的角度来看,这绝对是一场噩梦。微软绝对不会受理有关内存管理糟糕的插件的支持案例。而插件提供商通常会指责微软。
在采访中,我从不强迫任何人编写代码,我所做的就是尽量让他们告诉我这类战争故事–我想听听你是如何修复它的,为什么它会如此酷炫诡异,我希望你在谈论它时能有一些热情。
我并不总是能让别人这样讲,但讲的人通常都能很好地解决
你要选择的是那种总是喜欢思考战争故事并夸夸其谈的人。
这一点,每当我在面试中遇到这类问题时,我都不知道该怎么回答,因为我最奇怪或最难缠的虫子并不是我内化为战争故事的东西,它只是另一天的事情。
这就像那些 “当你与其他员工发生冲突时,你会怎么做 “的问题一样。我要么像个成年人一样和他们一起解决,要么让我们的管理层介入,然后他们帮他们解决。这不是什么英雄叙事,我在事情发生时就已经考虑过了。
不,他们挑选的是那种被问到时能讲述战争故事的人。他们也在挑选那种必须调试一些足够棘手和与众不同的东西,以至于令人难忘的人。
有些人天生就不会讲故事。讲故事并不是软件工程师通常的工作职责–我们不是小说家。拥有难忘的调试经历并不直接等同于拥有一个好故事。
这与我们在大科技公司看到的宣传文化的问题其实是一样的:你最终会提拔那些善于制作宣传资料的人,也就是善于讲述自己工作故事的人。当然,这与那些真正工作出色的人之间有很好的重叠,但这并不是完美的重叠。
就我个人而言,我并不介意,因为我认为自己擅长讲故事。但作为面试官,我绝不会这样对待候选人,因为不是每个人都能讲好故事。
这是一篇非常有趣的文章,不仅就其本身的优点而言,它还引发了许多其他难以调试的故事。
我喜欢从这些故事中汲取的经验教训。
我在大学时曾帮助过一个同学,他的程序从打孔卡牌输入中输出了非常虚假的数字。最后,我建议他打印出程序读取的数字,结果发现字段排列出现了偏差。这成了我调试程序的第一步。
在我攻读电子工程学位期间,我曾在华盛顿州朗维尤的一家纸浆漂白厂实习。他们在漂白塔中安装了各种测量仪器。工程师们讲述了一个关于他们测量流量、温度或酸度的仪器的故事。仪器出现故障,但制造商找不到任何缺陷,于是将仪器运回。如此反复几次,直到一位工程师陪同仪器来到维修实验室。技术人员将仪器侧放,而不是像在工厂的仪器架上那样平放。平放后,错误暴露无遗。
在阅读彼得-塞贝尔(Peter Seibel)所著的《工作中的程序员》(Coders At Work)一书时,另一个错误在我脑海中挥之不去。Guy Steele 讲述了 Bill Gosper 报告的 bignum 库中的一个错误。引起他注意的是一个他不太理解的条件步骤。因为它是基于 Knuth 的除法算法:”在 Knuth 中引起我注意的是一个注释,即这一步很少发生–概率大约只有字的大小的二分之一”。这个错误出现在一段很少执行的代码中。这个教训帮助他找到了类似的错误。
当我们三个人在 Sycor 构建编译器时,我们记了一大本实验笔记,在上面写下了简短的发布说明,并用一行字记录了我们发现并修复的每个错误。
我最近发现的 bug 是一个新的 emacs 片段导致 eval_buf 出错。这毫无道理,所以最终决定清除 .emacs.d 目录,重新开始。有些文件已经有 20 多年的历史了–我只是在新建机器时复制了该目录。
>它与 Google Docs 的发布不符。堆栈跟踪添加的信息很少。用户投诉量并没有出现相关的激增
凡人哪里会投诉谷歌产品?
如果你能找到的话,他们有论坛,我想他们称之为 “社区”,你可以在那里投诉。
然后就会有一个高级别的非雇员 “产品专家 ”来告诉你,这不是一个真正的问题,不要再拿这些小事来烦万能的谷歌了,你的意见并不重要,他们有数百万用户,他们为什么要听你的?
至少,这是我的经验之谈。
无处不在。他们可以在谷歌上搜索按日期过滤的产品投诉。
显然,支付 Google One 费用就可以获得 Google 支持。我不知道这是否有价值,但它确实存在
如果它与支付 Google Fi 或 Youtube Premium 所获得的支持类似,那么它可能毫无价值。
> 有人不小心把 Math.abs() 变成了超级优化级别的标识函数。
我的天啊
恶梦之源. . .
在我看来,如果你能使用调试器,它就不应该自动成为史上最难的关卡。
根据几天前的计算着色器帖子,目前我正在 “调试 “一些被移植到着色器中的非常先进的代码,而唯一的方法就是创建一个 ints 数组,然后在原始代码和着色器代码中都插入数值,看看它们在哪些地方出现了偏差。这并不是最困难的,但相当耗时。
> 我又做了几次。它并不总是在第 20 次迭代时出现,但通常发生在第 10 次和第 40 次迭代之间。有时从未发生过。好吧,这个错误是不确定的。
这是一个不正确的假设。仅仅因为你的测试用例没有可靠地触发错误,并不意味着错误是非决定性的。
这就好比说 “OpenOffice 星期二无法打印 ”是非确定性的,因为你无法每天都重现它。它是确定性的,你只需要找到合适的环境。
https://beza1e1.tuxen.de/lore/print_on_tuesday.html
从文字上看,作者似乎有时找到了一种重现错误的方法,然后每次测试都依赖这种方法。另一种方法是调整他们的测试用例,直到找到一种或多或少会重现错误的情况,试图找到导致错误的阈值,然后继续推导。
“确定性 “是……流动的盛宴。我们普遍认为 “软件是确定的,因为如果你向相同的可执行机器代码提供相同的输入,它将返回相同的值”,这几乎总是正确的,除非有人对你的处理器进行辐照或试图对其进行电压控制。
但是,”相同的输入 “中隐藏了很多东西,因为这包括了操作系统对程序的所有输入。其中包括 “时间”(重现的祸根)、内存布局、多线程代码的执行调度顺序、未初始化内存的值等等。
>另一种方法是调整测试用例,直到发现某种情况或多或少地重现了错误,试图找到导致错误的临界值,然后继续推导。
是的–当在一个巨大的问题空间中处理未知因素时,越热越冷并往上爬会非常有效。
如果我理解正确的话–无论采取何种步骤,Math.Abs() 值在大约一半的时间里都是正值。这似乎肯定是非确定性的。
你不能单独调用 Math.abs(),你需要给它一个数字。不管是正数还是负数,它都应该返回正数(这就是绝对值)。这里的问题是,当给定一个负值时,它返回的是一个负数,这是错误的:
> 我们重新运行重现。我们查看记录的值。输入负值时,Math.abs() 返回的是负值。我们重新加载并再次运行。Math.abs() 在负输入时返回负值。我们重新加载并再次运行。Math.abs()返回的是负输入的负值。
不管怎样,这是题外话。我并没有争论这是否是一个确定性错误,我只是指出作者的结论与前提不符。即使这个错误被证明是非确定性的,他们也没有完成必要的步骤来自信地做出这样的断言。这个错误是非决定性的 “与 ”我还没有确定重现这个错误的条件 “之间存在着巨大的差异。
只要有适当的蝴蝶翅膀扇动,一切都是确定性的。
https://xkcd.com/378/
我的理解是它被替换成了身份函数(例如只返回原始值)。但只有当代码被确定为热点时,它才会被替换。因此,在代码进入紧循环之前,它都能正常工作,而一旦通过一个负数,它就会开始失效。
在某个地方,有人读到这篇文章后得出结论:”谷歌怎么会蠢到雇佣蠢到让 abs() 返回负值的人。”
我喜欢这个故事!周围的世界太复杂了,看似明显错误的事情却通过最不可能的依赖链发生了。
>在某个地方,有一个人读到这篇文章后得出了这样的结论:”谷歌怎么会愚蠢到雇用愚蠢到让 abs() 返回负值的人。”
奇怪的事情在任何地方都可能发生,但我想知道为什么这个问题在进入生产之前没有被测试用例发现?我认为编译器团队应该会对这类常见函数进行底层测试。
我会清空我的日程表的。
这段话中最精彩的一句。
伴随着:
>然后我们找来了我们的技术负责人/经理,他有人类 JavaScript 编译器的美誉。我们向她解释了我们是如何走到这一步的,Math.abs() 返回的是负值,以及她是否能发现我们做错了什么。在说服她我们并没有犯什么大错后,她坐下来看了看代码。她的 CPU 转速达到了 100%,一边盯着代码,一边用俄语嘀咕着解析树什么的,并向调试控制台输入信息。最后,她向后靠了靠,宣布 Math.abs() 绝对是为负输入返回负值。
当我听到 abs 负值时,我的脑海中立刻跳出了 abs(INT.min())… 不过话说回来,JS…
最糟糕的调试问题总是我无法直接访问的东西,而且还很少见。
比如中间的网络设备没有记录日志,或者没有记录到你需要的级别(有时它们无法记录你需要的内容)。
这通常意味着,除了在生产中或非常接近生产的情况下,不可能使用你并不总是能控制的工具进行复制。
令人讨厌的是 “此 http 请求有时很慢”,以及追逐中间的每个方框会显示一个本应透明但却不透明的新方框,或者由于方框以一种有趣的方式交互而出现一些罕见的时间问题。
到目前为止,我的记录是 3 周。这是两个不同的基于 Ebpf 的系统相互竞赛时触发的 hiesenbug。Ebpf 在正确的地方是个很好的工具,但调试起来却很麻烦。
>就是这样:2 天的时间就找到了一个已经被修复的问题,而且无需交互就能解决
我使用 LLVM 工作,我的大部分工作都是修复上游已经修复的 bug
2008 年左右,我们正在开发一个注册收费公路的应用程序,向用户发送电子标签。
在 IE 中失败,结果非常奇怪。我们花了很长时间才意识到浏览器出了问题,于是将其改为
,这样就正常了。
>作为时事通讯的作者,我还能做些什么呢?通常情况下,我喜欢寻找可资借鉴的经验。但经过 2 天的艰苦调试,不知何故,并没有找到任何可借鉴的经验。
在我看来,一个显而易见的教训是:V8 团队没有就 “哎呀,我们的 Math.abs() 可能会返回负数,我们在 X 版本中已经解决了这个问题,请注意 “进行充分的前期沟通。
在 “为从事高性能客户端视图渲染工作的谷歌开发人员提供建议 “之类的每周时事通讯中,V8 应该能够做到这一点。
12 岁时,我刚开始学东西,用 C 语言写了一些东西,但每隔一段时间就会崩溃,无法解释。我把它拿给我 14 岁的叔叔帮忙,他比我更擅长编码。虽然这已经是 40 年前的事了,但我似乎还记得 Borland Turbo C(我仍然喜欢那个蓝色的集成开发环境)有断点调试功能(令人震惊!),最终导致 “咄,你没有处理掉你的指针,正在重复使用它,那里的内存现在是垃圾 “之类的结论。我依稀记得 * 或 * 就在附近。这是我对 RTFM 和调试的第一次入门,真是一次强有力的入门。
我喜欢你的叔叔只比你大两岁。
在大公司中,来自不同组织的不同人员在排除或修复同一个故障时,彼此独立,甚至毫不知情,这种情况经常发生,令人惊讶。有时,直到你实施了一项修复工作,导致与其他人正在进行的修复工作发生合并冲突时,你才会意识到这一点。
写得很好:)
就像这个 https://geek-and-poke.com/geekandpoke/2017/8/13/just-happene… 但实际上是真的,这对心理健康真的很不利 😀
“Math.abs()对负输入返回负值”,伙计,如果这发生在我身上,我一定会去找圣经。事后想想,真是不可思议。
我毫不怀疑 V8 拥有丰富的测试套件,包括对绝对值函数的测试。
但经过优化的生产版本显然包含了不同的代码?在我看来,这似乎是一个系统漏洞
如果这是一个回归,是否可以对签入进行二进制搜索?还是代码过于分散?
我猜 Google 文档团队最初认为这肯定是他们自己代码中的一个 bug,而不是 Chrome 浏览器或 V8 中的 bug,因此将他们自己的代码一分为二是无济于事的。没有人会在开始调试时就责怪编译器。
我的意思是……
>它与 Google Docs 发布的版本不符。堆栈跟踪增加的信息很少。用户投诉并没有出现相关的激增,所以我们甚至不能确定是否真的发生了这种情况,但如果真的发生了,那就非常糟糕了。从特定版本开始,该问题只在 Chrome 浏览器上出现。
这听起来像是 Chrome 浏览器的漏洞。或者,至少是由 Chrome 浏览器的变化引发的错误。当他们的更改导致崩溃时,将你的代码一分为二是愚蠢的,不管这是谁的错误。
如果你的工作是解决这种情况,那就不是。在 Chrome 浏览器中查找问题并不是一件容易的事。
如果你的工作是解决这种情况,那么你最大的希望就是找出是什么变化导致了这种情况;理解这种变化;然后做任何需要做的事情。
在一个复杂的大型应用程序中,环境的变化导致了崩溃,找出环境中的变化并思考其对应用程序的影响,比回溯应用程序的变化看是否能找到问题所在要有意义得多。
一旦找出问题所在,当然可以在应用程序或环境中进行修复,如果环境是 Chrome 浏览器,修复应用程序通常会更容易。但如果 Chrome 浏览器发生了变化,而我的应用程序又被破坏了,这就意味着要先查看 Chrome 浏览器中的变化,然后再从那里入手。
有谁知道 v8 abs val 函数为什么没有对负值进行测试?
我最难调试的 bug 与损坏的驱动程序和无用的供应商有关。我断断续续花了大约两个月的时间来解决这个问题,到最后我都快疯了。
一个新客户来了,我们为他们部署了一个新的 VMware vSphere 私有云平台(第一次使用这种类型的硬件)。没有什么特别的或太花哨的,但有拳头大小的 10G 生产网络。
几周后,集成团队抱怨说,一个随机的虚拟机无法与另一个虚拟机通信,但只能与其他一个特定的虚拟机通信。将 “坏掉的 “虚拟机移到另一个 ESXi 上后,问题解决了,因此我们怀疑是电缆/连接/端口/交换机坏了。各种测试都一无所获,于是我们就等着再次出现问题。
几天后,还是一样。我们又进行了一些调试和数据包捕获,但一无所获。重启 ESXi 解决了问题,所以可能不是电缆/交换机的问题。我们向 VMware 开出了支持单,他们给出了各种无用的 “建议”(更新驱动程序、Firwmare、操作系统等等等等)。
这种情况发生得越来越多,到后来每天都会发生多次–同样,只是特定的虚拟机与其他特定的虚拟机之间发生了这种情况,但总是可以通过 SSH 与其他东西通信,为此我们不得不重启管理程序来解决这个问题。即使有所有日志、时间表等,VMware 也完全没有用。
几周后,客户开始生气。我们说,我们已经尝试了各种调试方法(ESX 上的数据包捕获、交换机、客户操作系统等),但没有任何原因–各种虚拟机、不同的虚拟硬件版本、不同的客户操作系统、不同的虚拟网卡类型、不同的 ESX,我们正在与供应商一起尝试各种方法,可能是软件错误。
一天早上,我决定去阅读其中一个 ESX 上的所有日志,看看是否能发现什么奇怪的问题(早期我们尝试过搜索错误,但结果只是 VMware 的呕吐物,没有任何有用的信息)。日志太多了,我什么也没看到。无奈之下,我在谷歌上搜索了 “VMware””NIC 类型””网络问题 “的各种组合,结果发现英特尔论坛上有很多人抱怨英特尔 X710 网卡的驱动程序坏了,日志中出现了 “检测到恶意驱动程序 “的信息(不是错误),并直接关闭了特定端口上的流量。你知道吗,我们使用的就是这种网卡,而且我们还收到了这些信息。众所周知,这个垃圾驱动程序几个月前就已经无法正常工作了(要么就是整个机器崩溃),但它却在 VMware 的兼容性列表上傲视群雄。当我把这件事告诉 VMware 的技术支持人员时,他们说他们内部已经知道了,但拒绝将其从兼容性列表中删除。但如果我们升级到下一个主要 vSphere 的测试版,就会有一个更新的驱动程序,据说可以修复一切问题。我们照做了,一切终于得到了修复,但也有机器出现了类似的问题,驱动程序在那之后好几年都没有更新。
这件事让我明白,企业厂商甚至对自己的软件都不甚了解,VMware 的支持毫无用处,硬件兼容性列表也毫无用处。因此,你需要知道自己在做什么,而不能依靠支持来拯救你。
只有在特定浏览器上才会出现这种情况,这难道不是一个很大的提示吗?
> 我怎么花了两天时间
这不可能是史上最难的 bug
作者声称这是他们专门调试过的最难的 bug,而不是计算机史上最难的 bug。
最难的问题是你无法重现的问题,通常与网络有关。
> 其次,重现速度很慢。光是加载开发版的编辑器就花了大概 20 秒,触发问题又花了 40 秒。
重现需要 60 秒?慢!?企业软件中的笑声
该死的,我花的时间远不止两天
90 年代早期至中期的 “High C/C++”编译器在其基本数学函数的浮点库中有一个 bug。我最初并不相信这不是我的代码造成的,但它最终确实出现在他们提供的库中。
从最初的线索到最后的解决,我大概花了三天的时间,用的是 486/50 Luggable,内置橙黑单色屏幕。
我曾经花了几年时间才重现了一个问题。它是用 PLC 代码编写的,在触摸屏控制器上运行一个软 PLC,引擎盖下装有 Busybox。这些设备每周 7 天、每天 24 小时都在使用,通常都是防弹的。每隔一段时间,我就会收到这样的评论:有时它们会在启动时崩溃,但电源循环通常会解决这个问题。最后,我终于在车间里解决了这个问题,并放弃了一切尝试找出原因。
最终的原因是在网络初始化中使用了一个网络库,该库是 Linux 插座的纸巾包装。在向设备下载新版本软件时,PLC 会停止运行,但这并不能彻底关闭打开的套接字,套接字会一直处于打开状态,导致网络服务无法启动,直到设备重新启动。于是我做了一件显而易见的事,将套接字句柄写入一个文件。启动时,我会检查文件,如果文件存在,就关闭套接字句柄。这在开发过程中非常有效。
当然,这个文件在电源循环后仍然存在。99% 的情况下什么都不会发生,但偶尔在启动时关闭这个随机套接字句柄会导致软 PLC 运行时发生故障。这太愚蠢了,但在实际操作中却很难发现。
我在这里说过几次我个人最糟糕的情况。这次我要说的是一个叫 Ed 的同事。
在一个嵌入式系统上,我们遇到了一个找不到的错误。它存在了一两个月。我们无法重现甚至无法调试的随机崩溃。我们开始称它为 “幽灵”。
最后埃德说:”我想幽灵是在我们修改了以太网驱动程序后出现的。” 我们把它还原了,错误就消失了。
我们从未在源代码中发现这个错误。但埃德用日历调试了它。
有意思的是,在$dayjob,我们曾经(曾经?)遇到过一个 bug,它会随机地、间歇性地出现故障,大概每 3 个月出现一次。
我为此揪心了一年,没有任何进展/启示。更新了一个设备的驱动程序,之后就再没见过了。
我希望反向日历调试对我有用!
我经常阅读这些关于难以调试的问题的故事,因为我喜欢调试(可以说是对软件真凶的热爱),这是我读到的第一个故事,当作者描述他们需要在哪里寻找罪魁祸首时,我的反应是 “哦,上帝啊,不”。对布局引擎和所有浏览器特定调整的描述让人觉得调试起来绝对是一场乏味的噩梦。
这篇文章写得很好。我认为它给我们上了很好的一课:少出 bug 总比多出 bug 好,而且对于某些用户来说,它仍然会有一个恼人的 bug。
我处理过的最糟糕的 bug 是在一家使用 Clarion 编程语言的公司工作时产生的。
该语言的编译器很可能是由一个从未读过编译相关书籍的人编写的,基本上就像你使用宏编写编译器一样。我不认为它有任何类似优化的功能。再加上它是一种高级语言,这意味着用调试器进行调试是不可行的。即使你找出了问题所在,你也不知道到底是什么原因导致了代码方面的问题,因为大部分代码行都会变成几页汇编文件。不仅如此,我相信调试符号的格式是自定义的,因此只有使用该语言附带的糟糕的调试器才能获得行号信息。Windows 也是一个糟糕的开发环境,因为几乎所有 WinAPI 层面的东西都缺乏良好的文档。
我所开发的应用程序是多线程 Windows 应用程序。并发问题随处可见。解决这些问题有时需要花费数月时间。在很多情况下,解决方法完全没有意义。
集成开发环境(基本上是被迫使用的)漏洞百出。在很多情况下,只要点击速度过快就会导致程序崩溃。在使用该工具 5 年之后,我已经有了一种直觉,知道在什么情况下需要放慢点击速度以防止崩溃。
集成开发环境也在这些二进制块上运行,这些二进制块封装了整个项目。我从未花时间研究过这些 blob 的格式,但不出所料,以集成开发环境的质量,将这些不透明的二进制 blob 置于错误状态是有可能的。你既可以恢复到之前版本的 blob,也可以复制粘贴所有工作(在集成开发环境中无法轻松访问原始文本,因为这种白痴设计的模板功能一直在使用)。如果你的项目处于奇怪的状态,你会收到神秘的编译器错误,错误标识符是一个 32 位整数的十六进制。
在文档或互联网上搜索这些数字,要么一无所获,要么会在论坛或 comp.lang.clarion 上找到数十个无关问题的结果。
这种语言本身就是 pascal 和/或 COBOL 的疯狂变种。它有一些不错的数据库相关功能(因为它实际上是 CRUD 领域专用的),但也仅此而已。如今,你在 GitHub 上可以看到,人们甚至在考虑将 rust 中的 never 类型部分稳定下来之前,就已经讨论了好几个月它的合理性和人体工程学问题。与此同时,在 clarion 中,你得到的是一个半生不熟的文档页面,它充当了语言规范的角色,而你从中得到的是一个半生不熟的功能,它有一半时间都无法正常工作。对于某些功能,文档往往会有重复的页面,为你提供不重叠、有时相互冲突或完全错误的信息。
在处理 WINAPI 时,你需要处理指针类型,有时还需要进行指针类型转换。这种语言不会让你做类似于 `void *p = &foo;`这样的事情(这是 C 语言,与 Clarion 相比其实非常正常)。你必须做相当于 `void *p = 1 ? &foo : NULL;`的操作,这样会神奇地丢失足够多的类型信息,语言才会允许你这么做。文档中没有其他替代方法(有铸造方法,只是在这种情况下不起作用),甚至文档本身也没有说明,这只是挫折和反复试验的结果。
不仅如此,与我一起工作的人都是在用 C 或 C++ 编写纯粹的 winapi 代码的时代进入这种糟糕的专有语言的(哦,等等,我提到过吗,你必须为这狗屎东西支付许可证)。因此,对他们来说,拥有表单编辑器这一事实实在是太神奇了,以至于在接下来的 25 年里,他们从未考虑过其他的选择。因此,当我抱怨使用这种完全荒谬的语言太疯狂时,他们会告诉我,其他的语言更糟糕。
你想在调试时体验地狱般的生活吗?找一家写 Clarion 的公司吧,显然它在美国政府中仍然很流行。