Bug Archeology:借助LLM解开一个十年的Swift/C++谜题
摘要
一位开发者讲述了如何利用LLM解决一个Swift/C++跨平台音乐应用中存在十年的Bug,展示了AI如何协助调试复杂问题。
<p><a href="https://lobste.rs/s/aoatq6/bug_archeology_solving_decade_old_swift_c">评论</a></p>
查看缓存全文
缓存时间: 2026/05/15 17:02
# 错误考古:借助 LLM 解开一个十年之久的 Swift/C++ 谜题
来源:https://samkhawase.com/blog/bug-archeology-using-LLM/
> *“当你排除了所有不可能的情况,剩下的,无论多么难以置信,都必然是真相。”* —— 阿瑟·柯南·道尔,《四签名》
很多年前,我参与了一个非常酷的项目——Juke (https://de.wikipedia.org/wiki/JUKE_(Musikstreamingdienst))。它是欧盟早期的音乐应用先驱之一,拥有 5700 万首歌曲的曲库。这个应用采用了一套非常独特的技术栈,由中国 CTO Guillaume 开创。
这是我档案里的一张截图。
Juke 应用
## 快速回顾架构
该应用使用了一套手工打造的工具链,基于 Dropbox 的 Djinni 框架¹ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fn-1)。思路很大胆:采用 MVVM 架构,用 Java/Objective-C 编写 UI 层,用 C++ 编写公共业务逻辑。ViewModel 以及所有核心业务逻辑(如网络、音频、存储、加密等)都用 C++ 编写。应用通过薄封装层调用平台组件(如网络或 CoreData)。
Djinni 架构
我主导了将 iOS 部分从 Objective-C 迁移到 Swift 的工作。虽然我能熟练写 Objective-C,但用 Swift 写 ViewController 比 Objective-C 轻松得多。C++/Java 开发者也觉得编写 Swift 代码更容易。我还协助搭建了构建系统和 CI,以便将 Swift 集成进去。最终,构建链包括了一批多样化的工具,如 Carthage、Gradle、Conan、Cucumber,每个 CI 构建在顺利时也要运行 45 分钟。编写和调试跨平台 C++ 非常繁琐,容易踩坑。有一次我被 C++ 搞得很烦,甚至探索过 Swift on Android² (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fn-2) 和 Rust³ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fn-3) 作为替代方案。
## 该功能
除了清晰的音质外,该应用最受欢迎的功能之一就是**离线存储歌曲**。上面的截图展示了这个功能。Android 用户甚至可以将歌曲保存到 SD 卡。以跨平台方式设计这个功能是一个有趣的挑战。细节有点模糊,但我记得我们使用了加盐哈希和 nonce 来加密歌曲,然后再存储到本地。UI 层会调用核心的 C++ 加密/解密函数来完成工作。
## 这个 bug
2017年9月,苹果发布了 iOS 11。一个周五的早上,我们收到了一位愤怒用户的 bug 报告:在升级到 iOS 11.0.1 后,用户无法播放他们 ~1GB 的离线歌曲。在 iOS 11 上调试了一段时间无果后,我意识到工具链在系统升级过程中出了问题。我立即在一台 iOS 10 设备上保存了一首歌曲,然后升级到 iOS 11。果然,升级后这首歌无法播放。我找到了“真凶”!
既然找到了复现 bug 的方法,我面前摆着一项艰巨的任务。我需要在 XCode 中手动逐步调试,沿着一条从 Swift -> Objective-C -> Objective-C++ -> C++ 再返回的链路走下去。bug 可能出现在这个链中的任意位置。
第二个问题是时间紧迫。不仅因为周五晚上,苹果的发布节奏也让修复 bug 的截止日期更加紧张。苹果通常只发布两个 `.ipsw`⁴ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fn-4) 更新:当前版本和上一个版本。他们当时已经发布了 iOS 11.0.1 的更新,并且随时会停止对 iOS 10.3.3(最后一个 iOS 10 版本)的签名。我别无选择,只能周末去办公室,并希望苹果那个周末不会发布新的 `.ipsw`⁵ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fn-5)。
于是,我开始了这场史诗般的马拉松式调试与修复。
1. 降级到 iOS 10.3.3
2. 下载一首歌,看能否播放
3. 仔细逐步调试,记下每个细微细节
4. 升级到 iOS 11.0.1
5. 检查下载的歌曲能否播放
6. 重复步骤 3
顺便说一下,这是 Xcode 调试窗口的样子。想象一下面对这些符号时的“滚动盲视”。图片来自网络。
Xcode 调试会话 —— 也就是大海捞针
你可能会想,为什么我要走这么昂贵的排查路径?因为我完全不知道 bug 出现在链的哪个环节。日志没有帮助,只能通过暴力方式复现 bug。尽管如此,这并不愉快。降级和升级非常耗时,而且我不能漏掉任何一步。最糟糕的是,我甚至不确定是四种语言中的哪一种导致了 bug。在 Swift/ObjC/ObjC++/C++ 混搭的代码堆里找针简直是噩梦。尝试了几次后,我放弃了,沿着施普雷河⁶ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fn-6) 走了一圈。
在闲逛时,我想通了我走错了路 —— 过于依赖暴力方式。我之前一步步地操作,记录输出,检查变量,把值得注意的东西都记在日记里。与其从 ViewController 开始逐层调试,我为什么不直接从加密和解密入手,看看在哪里失败呢?我跑回办公室,开始了新的排查,在这些区域加了大量断点和日志。很快(也就是几次升级/降级之后),我中了大奖。
细致的记录和日志最终帮了大忙。Swift 加密/解密函数中的一个参数是 `inout` 参数,而在 iOS 11.0.1 中它是 null!正是这个参数对解密歌曲至关重要。iOS 11 中肯定有什么变化导致了这个问题。掌握这个信息后,我创建了一个小型 playground 项目来模拟该行为。bug 可以复现,我可以精确定位到 `inout` 限定符。最终,我在 iOS 应用中移除了 `inout` 限定符,测试了几次。一切顺利。
我立即发布了应用更新,没有进一步拖延,因为苹果需要好几天才能批准应用。我记下周一跟进,关闭了案例,然后回家了。但是,在周末花在这个 bug 上之后,我没有耐心去查阅 Swift 的更新日志和 RFC 来找出根本原因。我修复了 bug,但没有抓住肇事者。
## 2026 年的考古发现
我们回到了 2026 年。LLM 和 AI 编码代理以其看似无限的韧性翻遍复杂代码库,彻底颠覆了软件行业。到目前为止,我在旅途中修复了许多 bug (https://samkhawase.com/blog/zig-kindle-gdb),但不知何故,我一直没有放下这个十年之久的 bug。最近,我告诉一位学员,记录工作中遇到的所有古怪事情是开始写博客的最好方法。我突然有了一个想法:如果用 LLM 回到过去,弄清楚当时到底发生了什么,会怎么样?我迅速将我记得的细节输入提示,然后启动了 Claude Opus 4.6。
``
几年前,我在开发一个复杂的 iOS 项目时修复了一个奇怪的 bug。我需要帮助来识别并重现这个 bug。
--- 我记得的故事 ---
我们使用 Android 和 iOS 构建了一个跨平台移动应用。Android 部分用 Java 编写,iOS 部分用 Swift 编写,公共逻辑(ViewModel 等)用 C++(我想是 14)编写,使用了 Dropbox 的 Djinni 框架 (https://djinni.xlcpp.dev/)。这是一个音乐应用 (Juke),它从 Juke 和 Saturn-MediaMarkt 服务器流式播放音乐。还有一个功能可以将曲目离线保存。这些曲目使用加盐哈希或其他方式加密,这对于这个 bug 很重要。时间大约是 2016-2018 年,在德国。
bug:
一个用户保存了大约 1GB 的离线曲目。在一次 iOS 升级后,用户无法听这些歌曲了。我发现问题出在解密上。控制流是 swift -> objC -> Objc++ -> C++ 再返回。我必须通过降级 iOS 版本、再升级,重复多次,一步一步调试才能找出问题所在。我记得问题出在解密函数的 Swift `inout` 参数上。我当时的时间窗口很有限,因为旧版 iOS 一旦新版本发布就不再可用。
--
你能帮我识别并解决这个 bug 吗?
``
Claude Opus 进行了几次网络搜索,返回的结果让我震惊。
你的时间范围 (2016-2018) 对应以下关键过渡:
| iOS 版本 | Swift 版本 | 关键变化 |
|-----------|-------------|-----------------|
| iOS 10 | Swift 3.0 (2016年9月) | SE-0107: UnsafeRawPointer 引入,严格别名规则正式化 |
| iOS 11 | Swift 4.0 (2017年9月) | SE-0176: 强制独占内存访问。inout 语义发生根本变化 |
| iOS 12 | Swift 4.2 (2018年9月) | 独占性强制在运行时完全启用 |
最可能的 Bug:SE-0176 改变了 inout 语义
Opus 正确识别出 Swift 4 中的变化是导致这个 bug 的原因。更有趣的是,Opus 还识别出我并未指定的其他细节。
1. 解密使用了一个 `inout` 参数,该参数读取加密字节,并将解密后的字节写回同一个缓冲区。
2. 我通过使用普通的 var 而不是 `inout` 参数来修复了这个 bug。
在阅读了 Claude 的总结并查阅文档后,我终于能连起所有线索了。
Swift 3.0 使用 copy-in/copy-out 语义,但编译器在 Swift 3.0 中并未强制独占访问。根据 SE-0176⁷ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fn-7),Swift 4.0 强制执行了独占性原则,这意味着对同一个 `inout`⁸ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fn-8) 变量的两次访问是真正互斥的。
当跨越 Swift -> ObjC -> C++ 边界时,编译器会物化一个缓冲区的临时副本,将指向临时缓冲区的指针传递给 ObjC/C++ 端,然后再复制回来。在 iOS 10/Swift 3 下,它直接传递指针(按引用传递优化)。当 Swift 将桥接调用的方式从按引用传递改为 copy-in/copy-out 时,函数从副本(全零或未初始化)中读取数据,而不是从实际的加密头部读取,从而生成错误的解密密钥。我记得在 iOS 11 中发现该参数为 `NULL`,这使得这个理论是合理的。
以下是 bug 的可视化表示。
Bug 可视化
#### 结论
发现这个洞见后,我感觉像一位老警探终于破获了职业生涯中最令人费解的案件。也许我应该重新审视 2008 年遇到的 iSeries 或 Java SwingWorker⁹ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fn-9) 的 bug!
LLM 已经在颠覆我们编写软件的方式,但发现它能用来调试几十年前的旧 bug,绝对令人震撼。我为我十年前修复的最难的 bug 找到了答案,但这也凸显了在遇到问题时把它们写下来是多么重要。我迫不及待地想看看,借助这些未来主义工具,我们将继续编织出怎样的魔法,以及我们将在这个新发现的宇宙中展开怎样的捉虫之旅。
1. https://djinni.xlcpp.dev/ ↩ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fnref-1)
2. https://gist.github.com/samkhawase/5e7c5a7a6d70d21c71416bd0c046f1e5 ↩ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fnref-2)
3. https://github.com/samkhawase/c-interface-experiments ↩ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fnref-3)
4. https://ipsw.me/iPhone10,1/ ↩ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fnref-4)
5. 苹果于 2017 年 10 月 11 日发布了下一版本 11.0.3,但我当时无法知道他们的发布节奏。 ↩ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fnref-5)
6. 我的办公室在 Wallstraße (https://de.wikipedia.org/wiki/Wallstra%C3%9Fe_(Berlin)),有趣的是,这个词与纽约华尔街是同一个词。 ↩ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fnref-6)
7. https://github.com/swiftlang/swift-evolution/blob/main/proposals/0176-enforce-exclusive-access-to-memory.md ↩ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fnref-7)
8. https://docs.swift.org/swift-book/documentation/the-swift-programming-language/declarations/#In-Out-Parameters ↩ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fnref-8)
9. https://docs.oracle.com/javase/8/docs/api/javax/swing/SwingWorker.html ↩ (https://samkhawase.com/blog/bug-archeology-using-LLM/#user-content-fnref-9)
相似文章
用 Swift 训练大语言模型,第一部分:将矩阵乘法从 Gflop/s 提升到 Tflop/s
作者详细介绍了在 Apple Silicon 上优化 Swift 自定义矩阵乘法内核以训练大语言模型的过程,旨在通过利用 CPU、SIMD、AMX 和 GPU 能力,实现超越 C 实现的性能。
我们使用 LLM 分析代码库中的每一个文件。所有人都认为这是出于成本考虑的一个愚蠢想法,但事实并非如此。
一项基准研究表明,使用 LLM 分析整个代码库具有成本效益。DeepSeek V4 Flash 因其低成本以及与 Claude Opus 等高端选项相当的准确率,被确定为最佳默认模型。
Apple~Silicon 平台上的波兰语模型跨模型族系推测解码:基于扩展 UAG 的 MLX-LM 对 Bielik~11B 的经验评估
本文首次系统评估了 Apple~Silicon 上波兰语大语言模型的跨模型族系推测解码技术,通过在 MLX-LM 中扩展 UAG 实现跨分词器解码。研究发现,上下文感知的词元翻译能够提升接受率,但统一内存的带宽限制阻碍了理论加速比的摊销,在结构化文本场景下最佳吞吐量增益达 1.7 倍。
自回归大语言模型正式与鱼共眠(Yann LeCun是对的)
CETI项目使用大语言模型的架构解码抹香鲸的咔嗒声,揭示了其语音字母表,但也凸显出AI的统计模式匹配缺乏真正的理解。文章认为,AGI需要具身化、多模态的根基,而不仅仅是基于文本的模型扩展。
Metal-Sci:用于 Apple Silicon 上 LLM 驱动演化内核搜索的科学计算基准
Metal-Sci 推出了一项包含 10 个任务的基准测试,用于优化 Apple Silicon 上的科学计算内核,并配套了由大语言模型驱动的演化搜索框架。该研究评估了 Claude Opus 4.7、Gemini 3.1 Pro 和 GPT 5.5 等模型,在实现显著加速的同时,利用分布外测试来捕获静默的性能退化问题。