这是一篇关于在 PyPy 中查找和修复一个与垃圾回收器(GC)相关的崩溃错误的博客文章,主要内容总结如下:
- 引言:自去年夏天起,作者一直在研究 PyPy 中一个难以重现的崩溃错误,仅在 CI 上出现,症状为在 pytest 的 AST 重写阶段崩溃。几周前又收到两个类似症状的报告,作者决定认真查找该错误,最终发现是 2013 年以来 PyPy GC 中的几个错误,决定写博客记录。博客分为发现错误的过程、技术解释、反思及额外发现等三部分。
发现错误:
- 在 CI 上运行:作者通过修改 nanobind 的 CI 脚本,使用带有全调试信息和更多断言的 PyPy 版本,并添加矩阵变量增加重复次数,使 PyPy 在 AST 重写阶段崩溃时能生成核心转储文件。然后添加
-Xfaulthandler
选项尝试获取 Python 堆栈跟踪,还尝试在 CI 上运行gdb
但未成功,最后通过配置 CI 运行器上传核心转储文件并在本地使用gdb
进行分析,发现是内存损坏导致 Python 对象头部字被破坏,vtable 不可用。 - 在本地重现:明确要在本地重现问题,通过多次运行测试用例,删除字节码编译的重写 AST 的
pyc
文件,使用multitime
程序重复运行测试,最终使测试用例崩溃,排除了即时编译器和 C 扩展等可能的错误源。 - 使用
rr
:由于在gdb
中无法重现错误,使用rr
(反向调试器),通过rr record --chaos
增加重现错误的机会,发现被损坏的对象被垃圾回收器错误收集,GC 错误地将对象标记为不可达并放入空闲列表。 - 使用 GDB 脚本查找真正的错误:使用 GDB Python 脚本 API 编写辅助命令来理解 GC 堆的状态,如打印随机 PyPy 对象的当前活动 GC 标志、对象跟踪器等,最终发现是数组内容在内存复制时未被正确跟踪导致的错误。
- 编写单元测试:为确保理解正确,编写了一个 GC 单元测试,在 RPython 中模拟 GC 行为,测试中展示了一个仍可通过数组访问但被 GC 收集的对象,这种测试方式可以在所有内存都被模拟的环境中进行调试。
- 修复错误:有了单元测试,修复相对简单,之后与 Armin Rigo 讨论并找到代码中的其他错误,还得到 PortaOne 开发者的帮助,他们在服务器上测试了修复,但最终发现他们的崩溃是另一个与对象固定相关的 GC 错误。
- 编写 GC 模糊测试/基于属性的测试:使用
hypothesis
编写基于属性的测试,对 GC 进行随机测试,检查测试的有效性,发现了修复中的角落情况,并计划添加更多 GC 功能进行测试。
- 在 CI 上运行:作者通过修改 nanobind 的 CI 脚本,使用带有全调试信息和更多断言的 PyPy 版本,并添加矩阵变量增加重复次数,使 PyPy 在 AST 重写阶段崩溃时能生成核心转储文件。然后添加
错误的技术细节:
- PyPy 的增量 GC:PyPy 使用增量代标记清除 GC,分为年轻代和老年代收集,使用写屏障检测旧对象到年轻对象的引用,增量 GC 采用三色标记法,在标记阶段维护黑色、灰色和白色对象的不变量,每次收集步骤只追踪有限数量的灰色对象。
- memcopy 的特殊写屏障:数组使用不同的写屏障,因为追踪数组可能耗时,数组写屏障保留关于数组修改部分的信息,memcopy 被特殊处理,GC 有专门的 memcopy 写屏障,在 memcopy 循环前执行 GC 逻辑,然后使用
libc
的常规内存复制实现。 - 错误:错误出现在 memcopy 写屏障中,当实现当前 GC 时,未将之前非增量的 GC 的写屏障代码更新为增量模式,导致在两个代码路径中未将修改的黑色对象重新标记为灰色,修复该问题可停止崩溃。
- 反思:错误的引入方式很典型,代码在假设改变后未及时更新,应更多地为 GC 编写基于属性的测试,思考该错误长期未被发现的原因,如发生错误的前置条件很罕见,以及是否应使用更正式的方法如 B 或 TLA+进行模型检查,但这是昂贵且繁琐的方法。同时,追踪这个错误虽然很烦人,但也学到了如何使用
rr
和 GDB 脚本接口。 - 额外部分:错误的断言:PyPy 的 VM 构建引导过程会将一些堆对象冻结到二进制文件中,这些预构建对象是垃圾回收器的根,需要被追踪,但全部追踪很昂贵,因此有优化措施,当预构建对象第一次被修改时将其添加到根集合中。错误的断言是在增量标记阶段中间预构建对象第一次被修改时触发,由于编码方式不同,不变量检查代码错误地报告了黑色到白色的指针,作者编写了单元测试并修复了该错误。
- 致谢:感谢 Matti Picus、Max Bernstein、Wouter van Heyst 提供反馈,Armin Rigo 审查代码并指出思维漏洞,以及原始错误报告者。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。