OpenCore Legacy Patcher 如何修补运行中的 macOS 系统
【免费下载链接】OpenCore-Legacy-PatcherExperience macOS just like before项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher
OpenCore Legacy Patcher 的 Root Patch(系统补丁)是该项目里最精细的部分:它修改的不是 U 盘或某个分区,而是当前正在运行的 macOS 的只读根卷。仓库里的 PATCHEXPLAIN.md 偏向解释"补丁包含什么",本文换一个视角,带你从源码层面过一遍修补流水线的实际执行路径,重点拆两个容易踩坑的节点:不同 macOS 版本下怎么挂载只读根卷、为什么拷贝完 kext 之后还必须重建内核缓存。目标是让你对照日志就能在机器上验证每一个环节。
全景:一次修补流程怎么跑
先看全局。入口是 sys_patch.py 里PatchSysVolume的start_patch(),事件顺序是:
HardwarePatchsetDetection探测硬件(GPU、无线网卡、USB 1.1 控制器等),决定套用哪些补丁集;- 挂载
PatcherSupportPkg(存放补丁素材的 disk image); - 把根卷挂载到
/System/Volumes/Update/mnt1; _run_sanity_checks()比对挂载卷里SystemVersion.plist的ProductBuildVersion与当前运行系统,不一致就报错退出——那意味着 OTA 更新正在进行中;_execute_patchset():按补丁集字典删除旧文件、拷贝新 kext、执行必要的 shell 命令;_rebuild_root_volume():重建内核缓存 →(仅 Catalina)kcditto更新 preboot 内核缓存 →(仅 Mojave 及更老)重建 dyld shared cache;bless创建新的 APFS 快照,卸载卷。
关键点是:整个过程发生在运行中系统的"旁边"。系统自己是从旧的 sealed 快照启动的,补丁器改的是快照背后的 live volume,新快照要下次启动才生效。这和往 EFI 分区写文件是完全不同的量级。
挂载只读根卷:为什么按 macOS 版本分三个分支
mount.py 的RootVolumeMount._mount_root_volume()按 XNU 主版本号分支,简化后的逻辑是:
if self.xnu_major < os_data.os_data.catalina.value: return "/" # Catalina 之前根卷可写 if self.xnu_major == os_data.os_data.catalina.value: # Catalina:把只读根卷重新挂载为可写 subprocess_wrapper.run_as_root(["/sbin/mount", "-uw", "/"]) return "/" # Big Sur 及更新:把源卷挂到系统自己的挂载点 subprocess_wrapper.run_as_root(["/sbin/mount", "-o", "nobrowse", "-t", "apfs", f"/dev/{self.root_volume_identifier}", "/System/Volumes/Update/mnt1"])Catalina 之前/本身就是可写的,第一支直接返回;Catalina 引入只读根卷,需要mount -uw /重新以读写方式挂载;Big Sur 起系统从 sealed APFS 快照(如/dev/disk3s1s1)启动,快照不可修改,只能把快照背后的源卷挂出来。设备号由_fetch_root_volume_identifier()解析diskutil info -plist /的DeviceIdentifier得到,若确认是快照就直接截掉末尾两位。
为什么偏偏选/System/Volumes/Update/mnt1这个挂载点?因为 Apple 自己的更新机制就把源卷挂在这里。OCLP 复用同一个挂载点,等于和系统自身的快照管理行为对齐,避免两边争抢同一个卷。
拷完 kext 还不够:重建内核缓存
Catalina 起内核不再直接从磁盘加载 kext,而是加载/System/Library/KernelCollections/里的 prelinked 内核缓存。只把 kext 拷进挂载卷的话,它们只是"躺在磁盘上的文件"。rebuild.py 的RebuildKernelCache._rebuild_method()负责按版本分流:Big Sur 及更新走kmutil,Lion 到 Catalina 走 prelinked 流程,更早走mkext。
Big Sur 及更新的kmutil参数在 boot_system.py 里拼装(节选):
args = ["/usr/bin/kmutil"] if self.detected_os >= os_data.os_data.ventura: args.append("create") args.append("--allow-missing-kdk") else: args.append("install") args.append("--volume-root") args.append(self.mount_location) args.append("--update-all")这里有个讲究:Ventura 起用create --allow-missing-kdk,Big Sur/Monterey 用install。sys_patch.py 头部注释(见上文引用)解释了原因——Ventura 起 Apple 移除了部分盘上二进制,kmutil依赖 Kernel Debug Kit(KDK)参与重建。所以修补流程的 preflight 里有_merge_kdk_with_root()这一步:没有 KDK 就先下载再合并进挂载的根卷。补丁日志里出现"下载 KDK"不是可选项,而是重建缓存的硬前提。
如果补丁集往/Library/Extensions放 kext(大多数老 GPU 补丁集都会),Ventura 起还需要 auxiliary kernel collection。这里有个比较"野"的设计决策:Apple 没有公开重建 auxiliary KC 的入口,于是 auxiliary.py 的_force_auxiliary_usage()先用kmutil create --new aux构建新的辅助缓存,然后killall syspolicyd kernelmanagerd,再删掉/private/var/db/SystemPolicyConfiguration/下的KextPolicy、KextPolicy-shm、KextPolicy-wal三个文件——策略数据库没了,系统就不得不按新缓存重新建立策略。本质上是逼着 Apple 的代码走"首次构建"路径,而不是去改 Apple 的代码。
APFS 快照既是提交点,也是回滚点
前面所有步骤都只是"改草稿",真正提交的一步是新建快照。snapshot.py 的create_snapshot():
args = ["/usr/sbin/bless"] if platform.machine() == "arm64" or self._rosetta_status() is True: args += ["--mount", self.mount_path, "--create-snapshot"] else: args += ["--folder", f"{self.mount_path}/System/Library/CoreServices", "--bootefi", "--create-snapshot"]Intel 与 Apple Silicon 的bless参数不同:后者在非系统卷上不能直接--create-snapshot,只能走--folder + --bootefi的路子。快照创建成功后,系统下次启动才会加载打过补丁的内容。
回滚方向同样是快照语义:unpatch 入口start_unpatch()调_unpatch_root_vol(),第一步就是revert_snapshot(),即bless --mount ... --bootefi --last-sealed-snapshot,把启动指回上一个 sealed 快照,之后才清理 SkyLight 插件、non-Metal 强制偏好和残留的辅助缓存。为什么不逐文件恢复?因为补丁改动的文件清单无法穷举(哪些是新增、哪些是覆盖、哪些 kext 被删),而快照回滚是系统级原子操作,Apple 的更新机制自己也依赖它。
sys_patch.py 头部注释里还有一条值得记住的坑:Big Sur 下原始快照会在两三次启动内被系统丢弃,回滚不可靠;Monterey 起系统会保留原始快照,回滚才稳定。
OTA 更新之后怎么自动补补丁
系统补丁不是永久生效的:每次 OTA 更新都会重新 seal 系统快照,补丁内容被新快照覆盖掉。所以_patch_root_vol()在_execute_patchset()跑完后,若为 GUI 版且系统是 Big Sur 及更新,会调 install.py 的install_auto_patcher_launch_agent()写入一组 launch services:
| 服务 | 类型 | 职责 |
|---|---|---|
...auto-patch.plist | LaunchAgent | 登录后检查并重新打补丁 |
...macos-update.plist | LaunchDaemon | 监测 macOS 更新 |
...os-caching.plist | LaunchDaemon | 缓存 KDK / MetallibSupportPkg |
...rsr-monitor.plist | LaunchDaemon | 监测 cryptex,清理 GPU 伴侣包 |
其中 os-caching 和 rsr-monitor 只在满足条件时才安装;写入前还会对本地与系统内已有副本做 sha256 比对,一致就跳过,保证幂等。另有个文档没明说的细节:源码方式运行(launcher_script is not None)时会直接跳过 launch agent 安装——因为守护进程的 plist 写死了特定 app 路径,源码运行的构建没有这个路径,写进去只会得到一堆死掉的守护进程。
PROCESS.md 从用户视角说了同一件事:如果关掉后台进程,每次更新后就得手动打开应用重新打补丁,KDK 也要手动下载。这就是官方强烈建议保持后台进程开启的原因。
🔍 怎么验证系统补丁生效
补丁流程结束后,机器上有三件事可以查:
- 看"收据":
cat /System/Library/CoreServices/OpenCore-Legacy-Patcher.plist。这个 plist 由_write_patchset()在补丁完成后写入,记录了补丁集名称、使用的 KDK 版本和 commit 信息; - 看快照数量:
diskutil apfs listSnapshots /,打完补丁后快照数应比之前多一个; - 看内核缓存的重建时间:
stat -f "%Sm" /System/Library/KernelCollections/SystemKernelExtensions.kc,应接近补丁完成时间。
想验证"重复打补丁会被拒绝",可以直接看detect.py里HardwarePatchsetValidation枚举:已打补丁后再跑修补会命中REPATCHING_NOT_SUPPORTED(root volume dirty),已安装的补丁版本和当前代码版本不一致则报 "Installed patches are from different commit"。所以正确的升级姿势是先 unpatch 再 patch,而不是叠补丁。
⚠️ 避坑提示
- 打完补丁后不要再手动改动系统卷内容,快照 seal 会变成 Broken,校验会直接报 "System volume is tainted, unpatching is required";
- Ventura 及更新在 OTA 之后必须先下载对应构建的 KDK 才能重新修补,看到后台进程在下载 pkg 时别中断;
- auxiliary KC 构建后系统偏好弹出的授权提示可以先忽略,但建议之后在"系统设置 → 隐私与安全性"里允许,否则部分 kext 可能被卡住;
- 快照回滚在 Monterey 起才可靠,Big Sur 上动手前自己留一份备份;
- 若
bless报 "Can't use last-sealed-snapshot or create-snapshot on non system volume",那是 APFS 卷本身建得不干净的已知 bug,按日志提示做 clean install。
【免费下载链接】OpenCore-Legacy-PatcherExperience macOS just like before项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考