debug!耗时 3 个月改写一行代码,刚毕业程序员小哥成功消灭潜伏 7 年的 Bug!

作为脑力工作者,如何评估程序员的个人贡献和价值?不可否认,有些公司依赖代码量、需求完成量等指标来衡量程序员的工作价值。然而,根据 CSDN《2024 中国开发者调查报告》数据显示,只有 7% 的开发者每天有超过 70% 以上的时间在写代码,超过四成的开发者每天编写的代码量集中在 100-300 行,其余时间不是在开会就是在对需求、Debug。

开发者每天写代码的时间 图源:CSDN《2024 开发者调查报告》

那么,这些数据是否足以准确衡量程序员的价值呢?最近,有开发者 ch00f 在讨论论坛 Lemmy.World 发表了一篇题为《花费 3 个月调查一个 7 年前的 Bug,并用 1 行代码修复它》的长文,分享了自己多年前修复 Bug 的一次经历,引发了不少程序员关于生产力、Debug 的讨论。

发现隐藏多年的 Bug

多年前,ch00f 正在为 OG iPad(由苹果公司在 2010 年发布的第一代 iPad)开发一个硬件配件。

这款配件主要是通过 USB 接口连接到 iPad,提供 MIDI 输入/输出(Musical Instrument Digital Interface,音乐仪器数字接口,用于电子乐器、计算机和其他相关设备之间的通信和同步)和音频输入/输出,适合音乐家在 Garage Band(一款由苹果公司开发的数字音频工作站软件,主要用于音乐创作和录音)中铺设音轨,录制一些曲目。

这款产品之所以能够获得成功,是因为它的核心是基于公司在为 PC 端制作的一款很常见的 USB 产品,公司在这方面已经有了十多年的技术积累。现如今,只需要基于一个小型的微控制器,就能让 iPad 进入 USB 主机模式(当时 iPad 使用的是 30 针连接器),然后将其连接到几乎已经完成的产品上,带来了极大的便利。

不过,当时由于这款配件产品已经非常老旧,团队中没有人知道如何重新编写它的程序代码。当需要让它正常与 iPad 连接以及工作时,开发者们不得不直接手动编辑二进制文件,修改 USB 配置描述符,使其能够匹配新开发产品的名称,以便确保 iPad 正确识别和处理这款连接的设备。

此外,ch00f 还要确保他们所开发的固件从 iPad 的 USB 端口获取的电流小于 10mA(原来的设备是由端口供电的,但即使是自供电的情况下, iPad 如果请求超过 10mA 的电流,也会报错)。这一点特别麻烦,因为原始产品的名称只有 4 个字符,而新产品的名称却有 7 个字符。ch00f 无法为额外的字节腾出空间,所以只能把名字截短,以便将其适配到二进制文件中,同时又不破坏任何功能。

总之,产品出厂面市后,ch00f 及其背后的开发团队发现了这款配件存在一个问题:每隔一段时间,就会丢失一条 MIDI 信息。

对于不太了解的人来说,可能并不懂 MIDI 是什么。稍作解释——MIDI 是用于控制乐器、合成器和其他音乐设备的标准通信协议,它是用来传输音符的,这些音符可以由处理器或音色模块转化为音频。

图源:维基百科

MIDI 信息包括各种数据类型,用于控制和传输音乐信息,例如:

  • 音符信息,这是最基本和常见的 MIDI 信息类型之一,用于表示音符的开始和结束。每个音符由其音高(例如 A、B、升 F 等)和时长(持续时间)来定义。
  • 力度信息:也称为“音量信息”或“速度信息”,用于指定音符的强弱或键的按压力度。
  • 开键和放键信息:用于表示键盘上的键是按下还是释放。按键被按下时发送“按下”信息,释放时发送“释放”信息。这种信息对于模拟键盘和其他具有类似操作的设备非常重要,以确保正确地模拟音乐家的演奏动作。

从最后一种类型来看,按下和松开键会产生两条不同的信息。

事实上,偶尔丢失一个音符信息一般不会有什么大问题,但对于那些音色具有无限延音的乐器(如管风琴)来说就不同了。如果在使用 ch00f 背后公司开发的设备时选择了管风琴音色,它可能会收到“按下”的信息,但不会收到“释放键”的信息。

这样 iPad 就会误以为你无限期地按住琴键。这就导致了声音会一直持续下去,直到你手动停止它。这个问题对于某些乐器音色来说影响很大。

实际上没有官方规范说明,如果在没有接收到「按下键」信息的情况下再次接收到相同音符的按键信息该怎么办,但苹果以最糟糕的方式处理了这个问题。iPad 只会在「按键开启」和「释放按键」的数量相等时才认为按键被释放。因此,唯一的解决方案就是祈祷和希望它跳过后续的键按下消息,然后最终接收键释放的消息。

然而,发生这种情况的概率几乎为 0%,所以大多数用户不得不强制退出应用程序。

针对这个问题,很多消费者在公司留言板上猜测其中原因?有人说是新的 iOS 系统更新导致和这个配件不兼容?也有人称应该关闭其他所有应用程序,再来试试?各种各样的理论铺天盖地,但没人能给出确切的解释。

用 Python 编写脚本,复现 Bug

那时,ch00f 是公司的新员工,刚从大学毕业,所以他起初接到的任务就是把这个问题搞清楚。

接下这个需求之后,ch00f 第一步是找到产生这个 Bug 的方法。于是,他写了一个 Python 脚本,反复对公司的产品进行音阶测试,并监听是否有按键被卡住。如今回忆过去,ch00f 表示,「到现在都还记得那种仿佛一头“发了疯”的大象在键盘上猛砸几个小时的噪音」。

最后,ch00f 做到了大概可以每 10 分钟就能复现一次这个 Bug。他还注意到,这个问题只在同时按下多个键时发生。一次只按一个键不会产生这个问题。

随即,ch00f 使用苹果硬件开发人员才能使用的高级电缆,对他们的产品和 iPad 之间的 USB 通信进行了检查。

经过大量的排查(USB 调试器只能采样一小部分数据,所以 ch00f 必须在听到卡住的音符时按下触发器),ch00f 终于发现,「释放音键」的声音从未传到 iPad 上。所以这撇清了苹果公司的责任,问题就出现在了他们研发的配件没有及时传递 MIDI 信息。

接下来,便是 Debug 的过程了。首先,ch00f 要做的就是让源代码编译起来。在 ch00f 的脑海中,他记得依赖于一款“hex3bin”(将十六进制文件转换为二进制文件的工具),和一些 Perl 脚本。

「我猜这些工具在七年前写固件时是广泛使用的,但现在需要费点功夫才能找到。我对 Perl 不了解,但我还是让它运行起来了」,ch00f 说道。

固件编译好后,ch00f  插入一些指令,让特定的 LED 在固件的某些点闪烁(设备内部有几个调试用的 LED,用户看不到)。这个设备的 8 位处理器没有实时调试器,所以这就是 ch00f 唯一的调试手段。

问题最终排查出来了,归根结底是一个时序问题。处理器需要处理音频和 MIDI 流量。当处理音频数据包时,它会暂停正在进行的操作。MIDI 流量是缓冲的,所以如果在处理音频时收到按键或放键信息,它会在音频处理完后立即处理。

但它只有一个缓冲区。因此,如果在处理音频时收到第二个 MIDI 信息,第二个音符会覆盖第一个,导致第一个音符永远丢失。通过 USB 传输 MIDI 音符的速度是有限的,但刚好比处理音频的速度稍快。所以如果第一个音符在处理器开始处理音频之后进来,下一个音符可能会在处理器处理完音频之前进来。

解决方案

现在是解决方案。ch00f 表示,虽然其对 USB 音频处理知之甚少,但他在大学时用过 8 位 8051 处理器,所以知道哪些函数运行会比较慢。ch00f 用 Ctrl+F 搜索“%”,在音频处理代码中发现了一个 16  位调制器。

这个 16 位调制器起到的只是一个最终检查作用,它用来确保发送的字节或位数是正确的(预计余数为零),因此每次的除数是固定的。代码的编写方式让编译器认为每次的除数可能不同,所以它在后台包含了一个完整的函数来处理 8 位处理器上的 16 位取模操作。

最终,ch00f 在 Google 上搜索了「优化取模」,很快学到对于固定的除数,任何 16 位取模都可以重写为三个 8 位取模。

ch00f 尝试实现了这个单行更改,音频处理器的每个数据包处理时间迅速从 90 微秒降到了 20 微秒。这个改动 100% 解决了这个 bug。

为什么存在 7 年的 Bug 无人识?

不幸的是,固件无法进行现场升级,所以这对客户服务来说仍然是个麻烦。

至于为什么这个 bug 在之前 7 年的 USB 版本产品销售中从未出现过,ch00f表示,可能是因为大多数用户只将设备用作音频记录器或 MIDI 记录器。如果只启用 MIDI,就不会处理音频,也就不会出现这个 Bug。

然而,iPad 会一直启用所有功能,所以这个 bug 始终存在。只是没有人注意到。另外,许多 MIDI 应用不像苹果公司那样要求按键开/关事件相匹配。因此,如果某个键卡住了,再按一次就能松开。

ch00f 表示,「三个月听“魔鬼”在管风琴上砸键盘,最终通过修改一行代码解决了一个七年的 bug。」

简而言之:8 位处理器上的 16 位调制解调器速度太慢,导致数据包丢失。

Debug 究竟有多痛苦?

对于 ch00f 的这段经历,也引发了网友的热议。有网友评论道,「当你找到解决办法时,公司一定非常满意!干得好!」

ch00f 透露称,“事情确实如此。他们告诉我,当天剩下的时间可以休息,这对于我 22 岁来说,是闻所未闻的。”

Debug 远不及大家想得那么简单,ch00f 的这段经历也让不少开发者们感同身受:

  • 同样,我花了 6 周时间研究内核令牌环驱动程序间歇性初始化问题。这需要反复重启内核才能观察到问题。断点毫无用处,因为它们掩盖了问题。结果发现在特定步骤中的初始化不是同步的,读取状态是一个竞赛条件。我们花了几个星期的时间盯着看,开着玩笑,思考,胡思乱想,然后突然,瞧。改变代码顺序,成功了。
  • 在错过了多个 deadline 后,我多次用头撞到桌子上,然后突然有了一个清醒的头脑,比如“这给了我 X 的感觉,但如果真的如此,那就太疯狂了”,然后我快速进行字符串搜索,问题真的就出现了。

也有人认为:

  • 你必须承认,这种情况并不常见。如果一个开发人员经常要花 3 个月的时间来修复每一个 bug,那么这些 bug 最好都是令人讨厌的异生兽,因为开发人员更有可能只是速度太慢。
  • 我可以想象,几乎所有的开发经理都会在发现 Bug 的第 2 周或第 1 个月后说,这个问题的优先级不够高,不需要投入这么多时间。
  • 解决 Bug 的挑战有一半不是修复代码,而是发现到底发生了什么。

那么,你在 Debug 过程中又发生哪些难忘的事情,欢迎分享。

来源:

https://news.ycombinator.com/item?id=40749624

https://lemmy.world/post/16763534


已发布

分类

,

来自

宁ICP备09000617号-1