本文由 简悦SimpRead 转码,原文地址 blog.quarkslab.com
这是关于macOS内核调试的两篇博文中的第一篇。在这里,我们介绍了什么是内核调试......。
这是两篇关于macOS内核调试的博文中的第一篇。在这里,我们介绍了什么是内核调试,解释了它是如何在macOS内核中实现的,并讨论了它的局限性;在第二篇文章中,我们将介绍我们的解决方案,以获得更好的macOS调试体验。
术语macOS内核、Darwin内核和XNU在整个帖子中可以互换使用。提供了来自macOS 10.14.1 [XNU49032212]的XNU 4903.221.2的参考资料,这是写作时的最新可用来源。
什么是内核调试器?
调试是搜索和纠正可能导致程序行为错误的软件问题的过程。故障包括错误的结果、程序冻结或崩溃,有时甚至包括安全漏洞。为了检查正在运行的应用程序,操作系统提供了userland调试器机制,如trace或异常端口[1];但在内核/驱动程序/操作系统层面工作时,需要更强大的能力。
像macOS或iOS这样的现代操作系统由数百万行代码组成,内核通过这些代码协调数百个线程的执行,操纵成千上万的关键数据结构。这种复杂性有利于引入同样复杂的编程错误,这至少可以导致机器停止或重新启动。即使是在有内核源的情况下,追踪这类错误的根源往往也是非常困难的,尤其是在不知道确切地执行了哪些代码或者寄存器和内存的状态的情况下;同样,分析内核rootkits和安全漏洞的利用也需要对机器的行为进行准确研究。
由于这些原因,操作系统通常会实现一个内核调试器,通常由一个运行在内核内部的简单代理和一个运行在远程机器上的完整调试器组成,前者接收并执行调试命令,后者向内核发送命令并显示结果。内核内部的调试存根一般有以下任务。
- 读和写寄存器。
- 读取和写入内存。
- 在代码中进行单步操作。
- 捕捉CPU的中断。
有了这些能力,也就有可能。
接下来的章节详细描述了XNU是如何实现内核调试的。
调试macOS的内核
正如[XNU49032212]的README中描述的那样,XNU通过实现Kernel Debugging Protocol(KDP,在文章后面描述)支持远程(双机)调试。苹果关于这个主题的文档[DarwinDoc1]已经过时,不再更新,但幸运的是,关于如何设置最近的macOS内核进行远程调试的详细指南可以在互联网上找到[2] [3] [4] [5] 。总之,需要切换到内核的调试版本(作为Kernel Debug Kit的一部分发布,即KDK,后面还将讨论),重建内核扩展(kext)缓存,并在[NVRAM](https: //en. wikipedia.org/wiki/Non-volatile_random-access_memory)中设置为适当的值(见XNU代码kern/debug.h#L419)。在这之后,LLDB(或其他支持KDP的调试器)可以附加到内核。方便的是,也可以用虚拟机代替第二台Mac进行调试[6] [7] [8] 。
为了完整起见,至少有两种其他的内核调试方法在某个时候被支持,用于几个XNU版本。存档的Apple文档建议,当通过KDP调试不可能或有问题时(例如在网络硬件被初始化之前),可以通过串行线使用ddb[DarwinDoc2],但在XNU 1699之后,对这一功能的支持似乎已经放弃。 26.8,因为所有相关文件(即[XNU1699268]中的目录osfmk/ddb/)在下一个版本中被删除。其他文件,如macOS 10.7.3 build 11D50的内核调试工具包的README,暗示了使用/dev/kmem进行有限的自我调试的可能性。
’实时(单机)内核调试是在Mac OS X Leopard中引入的。这允许在当前运行的系统上对内核进行有限的反省。这可以使用正常的内核和这个内核调试工具包中的符号,在你的启动参数中指定kmem=1;DEBUG内核是不需要的。‘
这种方法在最近的macOS构建中仍然有效,前提是系统完整性保护(SIP)被禁用(bsd/dev/mem.c:L225, pexpert/i386/pe_init. c#L354),但较新的KDK没有再提到它,存档的Apple's docs中的一个说明说,对/dev/kmem的支持将在未来完全删除[DarwinDoc3]。
内核调试协议
正如已经介绍过的,为了使远程调试成为可能,XNU实现了内核调试协议,这是一个通过UDP的客户-服务器协议,允许调试器向内核发送命令并接收结果和异常通知。该协议目前的修订版是第12版,大约从macOS 10.6和XNU 1456.1.26开始(见[XNU1456126]中的osfmk/kdp/kdp.c#L100)。
像典型的通信协议一样,KDP数据包由一个普通的头(定义在osfmk/kdp/kdp_protocol.h#L167,其中包括:请求类型;一个用于区分请求和回复的标志;以及一个序列号)和专门的主体(来自osfmk/kdp/kdp_protocol. h#L199开始),用于不同类型的请求,如KDP_READMEM64和KDP_WRITEM64,KDP_READREGS和KDP_WRITEREGS,KDP_BREAKPOINT_SET和KDP_BREAKPOINT_REMOVE。正如大多数调试套件的README中所述,内核和外部调试器之间的通信可以通过火线或以太网进行(如果没有这样的端口,可以使用Thunderbolt适配器);不支持WiFi。内核只在以下情况下监听KDP连接。
- 这是一个开发或DEBUG构建,并且调试启动参数被设置为DB_HALT,在这种情况下,内核在初始启动后停止,等待调试器的连接([osfmk/kern/debug.c#L281](github.com/apple/darwi…
- 它正在一个管理程序上运行,调试启动参数被设置为DB_NMI,并且一个非屏蔽中断(NMI)被触发(osfmk/i386/mp.c#L628/#L638) 。
- Debug boot-arg被设置为任何值(甚至是无效的值)而发生恐慌(osfmk/kern/debug.c#L278/#L290)。
正如预期的那样,XNU 假设在任何时候最多只有一个 KDP 客户端连接到它。通过一个初始的KDP_CONNECT请求,调试器通知内核,当异常发生时,哪个UDP端口的通知应该被发送回来。有兴趣的读者可以从osfmk/kdp/kdp_protocol.h和osfmk/kdp/kdp_udp.c开始深入了解整个KDP的实现。
内核-调试器在KDP上互动的详细说明
对于更加好奇的人来说,这一部分彻底记录了当LLDB通过KDP连接到XNU时发生的事情;阅读后不需要跟随文章的其他部分。为LLDB 8.0.0 [LLDB800]提供了参考。为了提高可读性,在本节中,超链接只显示引用的源代码文件的名称。
假设内核已经为调试进行了正确的设置,并且调试启动参数被设置为DB_HALT,在XNU启动过程中的某个时刻,IOKernelDebugger对象将调用kdp_register_send_receive()在kdp_udp.c#L410。这个例程在解析了调试启动参数后,执行kdp_call()(kdp_udp.c#L472, kdp_machdep.c#L328,在kdp_machdep.c#L330产生一个EXC_BREAKPOINT陷阱,这反过来触发了Trap_from_kernel()的执行(idt64. s#L1282)、kernel_trap()(trap.c#L491和kdp_i386_trap()(trap.c#L686/#L776, kdp_machdep.c#L363。这最后一个函数调用 handle_debugger_trap() (kdp_machdep.c#L456, debug.c#L994) 并最终调用 kdp_raise_exception() (debug.c#L1064,kdp_udp.c#L2275),以启动kdp_debugger_loop()(kdp_udp.c#L2289/#L1323 )。由于还没有调试器连接,内核在kdp_connection_wait()(kdp_udp.c#L1381/#L1156)处停止,打印字符串'Waiting for remote debugger connection. '在kdp_udp.c#L1201,然后等待接收一个KDP_REATTACH请求,然后是一个KDP_CONNECT(kdp_udp.c#L1221)。
在LLDB中,kdp-remote插件(MacOSX-Kernel/)处理连接到远程KDP服务器的逻辑。当用户执行kdp-remote命令时,LLDB通过执行ProcessKDP::DoConnectRemote()在ProcessKDP.cpp#L221启动与指定目标的连接,它依次发送两个初始请求KDP_REATTACH(ProcessKDP.cpp#L256,CommunicationKDP.cpp#L374和KDP_CONNECT(ProcessKDP.cpp#L257,CommunicationKDP.cpp#L342)。
收到这两个请求后,kdp_connection_wait()终止(kdp.c#L233, kdp_udp.c#L1152)并进入kdp_handler()(kdp_udp.c#L1393/#L1068。在这里,来自客户端的请求被接收(kdp_udp.c#L1079),使用调度表进行处理(kdp_udp.c#L1127, kdp.c#L176,并在一个循环中响应(kdp_udp.c#L1147),直到收到KDP_RESUMECPUS或KDP_DISCONNECT请求(kdp.c#L415/#L274。
完成初始握手后,LLDB再发送三个请求(KDP_VERSION在ProcessKDP.cpp#L259和CommunicationKDP.cpp#L408,KDP_HOSTINFO在ProcessKDP.cpp#L264和CommunicationKDP.cpp#L494,KDP_KERNELVERSION在ProcessKDP.cpp#L275和CommunicationKDP.cpp#L524)中提取关于调试器的信息。如果内核版本字符串(一个例子是'Darwin Kernel Version 16.0.0: Mon Aug 29 17:56:21 PDT 2016; root:xnu-3789.1.32~3/DEVELOPMENT_X86_64; UUID=3EC0A137-B163-3D46-A23B-BCC07B747D72; stext=0xffff800e000000')被识别为来自Darwin内核(ProcessKDP.cpp#L322,CommunicationKDP.cpp#L469,然后加载darwin-kernel动态加载器插件(Darwin-kernel/)。此时,与远程目标的连接被建立起来,通过最终实例化上述插件(Process.cpp#L3242/#L3106)完成附加阶段(Process.cpp#L3166, DynamicLoaderDarwinKernel.cpp#L125,它试图定位内核加载地址(DynamicLoaderDarwinKernel.cpp#L169/#L178)和内核镜像(DynamicLoaderDarwinKernel.cpp#L170/#L428)。最后,达尔文内核模块被加载(Process.cpp#L3168, DynamicLoaderDarwinKernel.cpp#L531/#L520/#L989,它首先使用内核的UUID(DynamicLoaderDarwinKernel.cpp#L1036/#L792)在本地文件系统中搜索内核的磁盘文件拷贝,然后最终加载所有内核扩展(DynamicLoaderDarwinKernel.cpp#L1055/#L1391)。
在附加之后,LLDB等待来自用户的命令,这些命令将被翻译成KDP请求并发送给内核。
- 命令寄存器读和寄存器写产生KDP_READREGS(CommunicationKDP.cpp#L1175)和KDP_WRITEREGS(CommunicationKDP.cpp#L1217)请求。
- 命令内存读取和内存写入产生KDP_READMEM(CommunicationKDP.cpp#L554)和KDP_WRITEMEM(CommunicationKDP.cpp#L592)请求(对于64位目标分别为KDP_READMEM64和KDP_WRITEMEM64)。
- 命令breakpoint set和breakpoint delete产生KDP_BREAKPOINT_SET和KDP_BREAKPOINT_REMOVE(CommunicationKDP.cpp#L1258)请求(对于64位目标分别为KDP_BREAKPOINT_SET64和KDP_BREAKPOINT_REMOVE64)。
- 命令继续和步进都会产生KDP_RESUMECPUS(CommunicationKDP.cpp#L1246)请求;在单步进的情况下,RFLAGS寄存器的TRACE位被设置(ProcessKDP.cpp#L447, RegisterContextDarwin_i386.cpp#L953,RegisterContextDarwin_x86_64.cpp#L1061)在恢复之前有一个KDP_WRITEREGS请求,这后来导致CPU在执行下一条指令后提出一个type-1中断。
收到KDP_RESUMECPUS请求后,kdp_handler()和kdp_debugger_loop()终止(kdp.c#L415, kdp_udp.c#L1152/#L1394 ,机器恢复执行。当CPU遇到断点时,会产生一个陷阱,从Trap_from_kernel()开始,会对kdp_debugger_loop()进行新的调用(如上所述)。由于这次调试器被连接,一个KDP_EXCEPTION通知被产生(kdp_udp.c#L1383/#L1273)以通知调试器关于这个事件。在这之后,kdp_handler()在kdp_udp.c#L1393再次被执行,内核已经准备好接收新的命令。
内核调试工具
对于某些macOS版本,苹果还在developer.apple.com发布了相关的内核调试工具包,其中包括。
- 内核的RELEASE、KASAN(偶尔)、DEVELOPMENT和DEBUG版本,最后两个版本是用 "附加断言和错误检查 "编译的。
- DWARF格式的符号和调试信息,适用于每一个内核构建和一些MacOS中包含的苹果kexts。
- ldbmacros(在下一节中描述),一组用于达尔文内核的额外LLDB命令。
KDK对于内核调试来说是非常有价值的,但不幸的是,它们并不是对所有的XNU构建都可用,而且往往是在它们之后的几周或几个月才发布。通过在苹果开发者门户网站搜索macOS 10.14的非beta版本为例,在写这篇文章时,与相应的macOS版本同一天发布的KDK在九个版本中只有三个(18A391、18C54和18E226);一个KDK晚了两周发布(18B75);其他五个版本没有发布KDK(18B2107、18B3094、18D42、18D43和18D109)。从苹果开发者论坛上的一个帖子来看,现在 "要求新的KDK的正确方法是提交一个要求它的错误。[DevForums1]
总结
通过这篇文章,我们试图准确地记录下macOS内核调试的工作方式,希望能在这个话题上创造一个最新的参考。在下一篇文章中,我们将介绍我们的解决方案,以获得更好的macOS调试体验,也是为了克服当前方法的局限性。