xnu内核调试

5,330 阅读5分钟

对于iOS/Mac底层开发人员来说,研究xnu内核实现原理,最好的方式就是调试源码跟踪其实现过程。因此,有必要进行xnu内核的调试,本文主要梳理内核调试环境搭建过程,开始吧骚年

概述

苹果提供了内核调试协议KDP(Kernel Debug Protocol)来支持远程调试,该协议基于UDP协议允许调试器将命令发送到内核,并接收返回的结果和异常通知。 本文将基于最新的系统版本OS X10.15.6,来构建起内核调试环境便于跟踪内核过程,具体内核版本如下:

准备工作

因调试内核难免会遇到内核“恐慌”情况,因此最好的方式是通过虚拟机运行系统,并且虚拟机支持快照管理,方便保存环境。常用的VMWareParalles,不过笔者使用Paralles搭建环境过程中存在一些问题(后面会解释),所以,建议使用VMWare来安装系统。

安装虚拟机

下载最新版本的MacOS镜像,可通过App Store来获取,具体的安装过程在此不赘述,自行google。

下载工具包并安装

工具包包括LLDB+KDK(Kernel Debug Kit),其中KDK下载地址:developer.apple.com/download/mo…,具体的下载版本应匹配最新的系统版本要求,可通过如下命令查看系统版本

sw_vers |grep BuildVersion

如下图所示: 或者通过“系统报告->软件”来查看,如下图: 但是官网未找到完全匹配的版本,不过已验证使用最新的工具包即可使用,具体版本为:Kernel Debug Kit 10.15.6 build 19G73

调试主机及虚拟机都需要安装该调试工具包,安装完成后会在/Library/Developer/KDKs/KDK_10.15.6_19G73.kdk/System/Library/Kernels目录下看到内核的二进制文件、Xcode调试符号文件.dSYM以及kernel.development内核调试文件。

下载xnu源码

通过uname -v命令查看10.15.6版本为xnu-6153.141.1,因此到苹果源码下载对应的源码,用于后续xnu源码调试。

关闭SIP

调试主机及虚拟机都需要关闭SIP系统完整性保护,具体关闭方式如下:

  • 系统启动时,快速Command+R直至出现苹果图标;
  • 打开实用工具的终端,输入csrutil disable来禁用并重启系统;

系统重启后可通过csrutil status查看关闭状态,正确为如下:

调试

内核替换

虚拟机需要将位于KDK安装目录下的内核调试版本赋值到/System/Library/Kernels

cp /Library/Developer/KDKs/KDK_10.15.6_19G73.kdk/System/Library/Kernels/kernel.development /System/Library/Kernels

不过Catalina版本后硬盘被分为只读部分和数据部分,因内核目录位于只读目录,需要修改文件权限,通过简单的sudo chmod是无法修改的,可通过mount工具来临时修改权限(重启后会恢复权限)完成,具体如下:

sudo mount -uw /

设置NVRAM启动参数boot-args

为了将虚拟机设置成调试模式,需要使用nvram设置boot-args,命令如下

nvram boot-args="debug=0x141 kext-dev-mode=1 kcsuffix=development pmuflags=1 -v"

使用Paralles安装OS X 10.15.6在本身SIP关闭情况,仍然无法修改NVRAM,总是报nvram: Error setting variable - 'boot-args': (iokit/common) general error错误,尝试通过恢复模式设置也无效。

debug标识位如下图所示

具体可以参考苹果官方文档Building and Debugging Kernels

  • debug=0x145,其中0x01为系统启动后会等待调试器挂载,0x04为通过按键触发NMI (non-maskable interrupt)非屏蔽中断来触发进入调试状态,这样可以随时进入调试状态。但默认的虚拟机组合键来触发非屏蔽中断为Command-Option-Control-Shift-Escape太繁长,可以通过VMWare的按键映射来修改,如下图所示:
  • kext-dev-mode=1允许加载未签名kext
  • kcsuffix=development指定加载上面拷贝的kernel.development
  • pmuflags=1关闭看门狗定时器
  • -v显示内核加载信息

网络配置坑点:

  • 如果您通过物理FireWire端口(较旧的Mac机器)使用FireWire电缆的话,可参考Debugging macOS Kernel For Fun
  • 如果虚拟机设置两个网卡的话,比如host-only以及默认的NAT,则需要通过kdp_match_name选项来指定网卡的方式,具体可参考Mac内核扩展开发

清除kext缓存

命令如下:

sudo kextcache -invalidate /

让虚拟机系统的kext cache无效,使用新的内核调试并重启系统,就会进入调试模式,如下图:

LLDB挂载调试

调试主机启动LLDB并将目标设置为位于KDK目录中的内核调试文件/Library/Developer/KDKs/KDK_10.15.6_19G73.kdk/System/Library/Kernels/kernel.development,如下:

$lldb
(lldb)target create /Library/Developer/KDKs/KDK_10.15.6_19G73.kdk/System/Library/Kernels/kernel.development

如果遇到以下问题:

按照提示执行如下指令:

command script import "/Library/Developer/KDKs/KDK_10.15.6_19G73.kdk/System/Library/Kernels/kernel.development.dSYM/Contents/Resources/Python/kernel.py"

遇到如下错误: 说明python版本不对,需要改为python 2,按照提示输入如下命令:

defaults write com.apple.dt.lldb DefaultPythonVersion 2

注意:该命令不是在lldb中执行

成功如下:

连接虚拟机

kdp-remote xxx //xxx为虚拟机ip地址

成功后如下图所示:

xnu源码调试

调试时,lldb 会去 /Library/Caches/com.apple.xbs/Sources/xnu/(没有该目录则创建)对应版本的xnu-xxx目录寻找内核源码,所以前面下载的源码放这个目录就可以支持xnu源码调试。

实战

跟踪应用崩溃抛出Mach异常转换为Signal信号流程,具体的Mach异常与信号转换流程如下:

测试代码如下:

#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include <string.h>

#define BUF_LEN 1024

static void sig_int(int signo) {
    printf("handler signo:%d \n", signo);
}

int main(int argc, char **argv) {
    char buf[BUF_LEN] = {0};
    //处理信号
    //ctrl+c 进程终止
    if (signal(SIGINT, sig_int) == SIG_ERR) {
        printf("signal error:%s \n", strerror(errno));
    }

	//产生硬件异常
    intptr_t *ptr = NULL;
    *ptr = 1;

    return 0;
}

可通过breakpoint set -n xxx设置需要断点的内核函数,如下图:

函数调用栈如下图所示:

我们可以配合xnu源码来进一步跟踪调用流程,如threadsignal函数如下图所示:

详细的调用流程过程在此不详述,主要是清晰如何通过内核调试来跟踪xnu的源码实现过程。