A kernel debugger in Python: drgn

原文链接:https://lwn.net/Articles/789641/

一个内核调试器,允许 Python 脚本在运行中的内核中访问数据结构,是Omar Sandoval在2019年Linux存储、文件系统和内存管理峰会(LSFMM)上的主题演讲。在他在Facebook的日常工作中,Sandoval进行了大量的内核调试,但他发现现有的工具还不足够满足他的需求。这促使他开发了drgn,一个内嵌在Python库中的调试器。

Sandoval开始进行了drgn(发音为“dragon”)的快速演示。他登录到一个虚拟机(VM),并使用”drgn -k”命令调用了正在运行的内核的调试器。在python交互界面中,他使用一些简单的Python代码,能够检查根文件系统的超级块,并遍历该超级块中缓存的索引节点(inodes)以及它们的路径。然后,他做了一些“稍微复杂一点的事情”,仅列出了大小大于1MB的文件的索引节点。这显示了一些更大的内核模块、库文件、systemd等。

他主要从事Btrfs和块层的工作,但他也经常调试各种随机的内核问题。Facebook拥有如此多的机器,以至于总会出现“超级稀有、百万分之一”的bug。他经常自愿去查看这些问题。在这个过程中,他习惯了使用GDB、crash和eBPF等工具,但他发现他经常希望能够对内核数据结构进行任意复杂的分析,这就是为什么他最终开发了drgn。

他说GDB有一些很好的特性,包括能够漂亮地打印类型、变量和表达式。但它专注于断点式调试,而在生产系统上他无法这样调试。它有一个脚本接口,但它很笨重,只是对现有的GDB命令进行了包装。

Crash是专门用于内核调试的工具;它了解链表、结构体、进程等等。但是如果你想超越这些东西,你就会遇到困难,Sandoval说。它不是特别灵活;当他使用它时,他经常不得不转储大量的状态,然后进行后处理。

BPF和BCC非常强大,他经常使用它们,但它们局限于在能够实时重现bug的情况下使用。他所看到的很多bug是几个小时前发生的,并导致机器锁死,或者他获得了一个核心转储文件,想要弄清楚发生了什么。BPF并不能真正涵盖这种用例;它更适用于跟踪,而不是一个真正的交互式调试器。

Drgn使得我们可以用一种真正的编程语言来编写实际的程序——当然,这要取决于个人对Python的看法。它比将数据转储到文本文件中,然后使用shell脚本进行处理,或者使用Python绑定的GDB要好得多。他有时将drgn称为“作为库的调试器”,因为它不仅提供一个有限的命令提示符;相反,它神奇地包装了类型、变量等等,让你可以随心所欲地使用它们。上面链接的用户指南和主页是开始了解drgn的好地方,你可以在那里了解它的全部功能。

他进行了另一个演示,展示了drgn的一些强大之处。它有交互和脚本模式。他首先进入了一个交互式会话,查看了一些变量,并指出drgn返回一个表示该变量的对象;该对象还包含额外的信息,如类型(也是一个对象)、地址和当然还有值。但是人们也可以实现列表迭代,他通过从init任务(task_struct结构)开始,沿着其子任务链一直追踪下去来展示这一点。

在演示中,虽然他实时编写了列表迭代的代码,但他指出如果你每次都需要这样做,那会变得很繁琐。Drgn提供了许多辅助函数来完成这些操作。目前,大多数辅助函数是用于文件系统和块层的,但也可以为网络和其他子系统添加更多的辅助函数。

他重新演示了他和同事在一个生产服务器的虚拟机上进行的实际调查,该虚拟机中成功复现了bug。该生产负载是一个用于存储冷数据的存储服务器;在该服务器上,长时间未使用的磁盘会被关闭以节省电力。因此,该服务器的磁盘经常频繁开关,这暴露了内核的bug。冷存储服务在一个容器中运行,有报告称停止该容器有时会花费很长时间

他开始分析这个问题时,意识到容器最终会完成,但需要很长时间。这暗示了可能存在某种泄漏。他展示了从块控制组数据结构逐步深入的过程,并使用Python的Set对象类型来跟踪与块控制组关联的唯一请求队列的数量。他还能够深入研究与用于标识请求队列的ID分配器(IDA)相关联的基数树(radix tree),以检查一些结果。最后,确定请求队列泄漏是由于引用循环引起的。

他提到了另一个案例,他使用drgn调试了Btrfs意外返回ENOSPC的问题。结果发现这是因为为孤立文件预留了额外的元数据空间。一旦确定了原因,很容易找出是哪个应用程序在创建这些孤立文件;可以周期性地重新启动该应用程序,直到对Btrfs进行真正的修复。此外,当他在内核中遇到一个新的子系统时,他通常会使用drgn来弄清楚所有组件是如何连接在一起的。

drgn的核心是一个名为libdrgn的C库。他说,如果你不喜欢Python而喜欢错误处理,你可以直接使用它。它有可插拔的后端,用于读取各种类型的内存,包括用于运行中的内核的/proc/kcore,崩溃转储,或者用于运行中程序的/proc/PID/mem。它使用DWARF来获取类型和符号,这并不是最方便的格式。他花了很多时间优化对DWARF数据的访问。该接口也是可插拔的,但到目前为止,他只实现了DWARF接口。

这些优化工作使得drgn的启动时间大约为半秒,而crash的启动时间约为15秒。由于drgn启动速度快,使用频率会更高;他仍然不喜欢不得不启动crash的情况。

drgn内嵌了一个C解释器的子集。这使得drgn能够正确处理许多边界情况,比如隐式转换和整数提升。虽然有些棘手并需要花费一些精力,但这意味着他在内核中运行的代码在转换后的代码中没有出现任何问题。

他说,最大的缺失功能是回溯支持。目前,你只能访问全局变量,这并不是一个巨大的限制,但他有时不得不使用crash来获取地址和其他信息,然后将它们插入drgn。这是“在drgn中完全可能实现的功能”,但他还没有做到。他希望使用BPF Type Format (BTF)而不是DWARF,因为它更小更简单。但主要限制是BTF不能处理变量;如果BTF能够处理变量,他将使用它。还在筹备中的是一个有用的drgn脚本和工具的存储库。

他一直在纠结如何将drgn与BPF和BCC集成。想法是在某种程度上使用BPF进行实时调试,而用drgn进行事后调试。两者之间有一些重叠,但他还没有完全弄清楚如何统一它们。BPF由于缺乏循环而使用起来有些麻烦,但drgn无法在事件发生时捕捉问题。他有一个“疯狂的疯狂想法”,就是让BPF断点调用一个用户空间的drgn程序,但他不确定这是否可能实现。

 

图片from 陳丁光

Comments are closed.