对于iOS/Mac底层开发人员来说,研究xnu内核实现原理,最好的方式就是调试源码跟踪其实现过程。因此,有必要进行xnu内核的调试,本文主要梳理内核调试环境搭建过程,开始吧
骚年
概述
苹果提供了内核调试协议KDP(Kernel Debug Protocol)
来支持远程调试,该协议基于UDP
协议允许调试器将命令发送到内核,并接收返回的结果和异常通知。
本文将基于最新的系统版本OS X10.15.6
,来构建起内核调试环境便于跟踪内核过程,具体内核版本如下:
准备工作
因调试内核难免会遇到内核“恐慌”情况,因此最好的方式是通过虚拟机运行系统,并且虚拟机支持快照管理,方便保存环境。常用的VMWare
及Paralles
,不过笔者使用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
的源码实现过程。