Madeira 的 StikDebug 协议深度解析:BRK #0xf00d 命令分派完全解读
【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira
本文带你完整解读Madeira的StikDebug 协议:它让 Windows PC 游戏(x86/x86-64)通过 FEX-Emu + Wine + DXMT 在未越狱的 iPhone 上运行,而协议的核心BRK #0xf00d指令正是 iOS 限制下"借调试器之手执行 JIT 代码"的命令分派桥梁——一条中断指令,就是一次系统调用。
一、为什么 iOS 上的 PC 模拟器要"借调试器的手"?
iOS 有严格的 W^X(写异或执行)内存策略:应用不能自己创建可执行内存,唯一例外是——当调试器附加、进程带上CS_DEBUGGED标志时,JIT 才被允许。Madeira 恰好重度依赖 JIT:FEX 动态翻译器要在运行时把游戏的 x86-64 指令实时编译成 ARM64 机器码。
于是 Madeira 设计了这套"握手协议":
- 发送端:app 自身(
JITAllocator.c里的内联汇编) - 应答端:StikDebug 调试器,或内置的 StikJIT 辅助进程,由一份捆绑的 JavaScript 脚本 madeira-jit.js 驱动
- 信道:ARM64 的
BRK #0xf00d中断指令
启用入口在 StikJITHelper.swift:通过stikdebug://enable-jitURL 把 bundle ID、当前 PID 和 base64 编码的脚本传给调试器,最多等待 90 秒,轮询直到CS_DEBUGGED置位且调试器真正在线。
二、协议总览:一条 BRK 指令 = 一次"系统调用"
🎯 协议约定非常简单——寄存器就是"接口":
| 寄存器 | 用途 |
|---|---|
x16 | 命令号(分派键) |
x0 | 参数 1(通常是地址;0 表示"帮我分配") |
x1 | 参数 2(通常是大小) |
x0 | 返回值(调试器写回的结果) |
发送端在 JITAllocator.c 中只需两行汇编。以申请 JIT 内存为例:
__asm__ volatile( "mov x16, #1\n" "brk #0xf00d\n" : "+r"(x0) : "r"(x1) : "x16", "memory" );执行到brk时,进程触发陷阱,控制权交给调试器。脚本读出寄存器、完成操作、把结果写回x0,再把 PC 前移 4 字节后继续运行——对游戏来说,这就是一次"透明的系统调用"。
三、x16 命令分派表:完整解读 🎯
madeira-jit.js 是协议的"内核态"。它只认两个 BRK 立即数:0xf00d(通用协议)和0x69(旧版协议),其余 BRK 一律跳过并把x0清零。
x16 = 0:CMD_DETACH 断开调试器
发送D命令让调试器脱附。注意CS_DEBUGGED是粘性标志——脱附后依然保持置位,JIT 能力不会失效,所以游戏加载 PE 依赖项完成后就可以断开,释放调试会话。
x16 = 1:CMD_PREPARE_REGION 准备可执行内存
这是最核心的命令,流程见 madeira-jit.js:
- 若
x0 == 0(请求自动分配),脚本向 StikDebug 发_M<size>,rx,让调试器分配一块 RX(只读可执行)内存 - 调用
prepare_memory_region(addr, size)把该区域标记为可执行 - 把最终地址写回
x0,游戏端拿到的就是"开箱即用"的代码池基址
x16 = 3:CMD_MAP_PAGE_ZERO 在地址 0 放 TEB
Windows 的线程环境块(TEB)按惯例映射在地址 0,但 iOS 内核拒绝应用自己映射页 0。脚本借调试器的更高权限,从 app 内存读出 TEB 数据,直接写入地址 0(约 16KB),x0返回 0/1 表示成败。Wine 侧的发起点见 signal_arm64_ios.c。
x16 = 0x69:兼容旧协议
对旧版 StikDebug 的兼容路径,直接用x0作为地址与大小调用prepare_memory_region。
四、内存摆放:一次必须"掷"对骰子的分配 🎲
拿到调试器后,StikJITHelper.swift 的allocatePool()才是重头戏:它要申请一块默认 128MB 的 JIT 池(RX),再用vm_remap在其上叠一层 RW 别名视图——写代码走 RW,跑代码走 RX,这就是 W^X 下的"双映射"区域。
难点在于 iOS 内核第一匹配分配、不接受地址提示,池子落在哪里全凭运气。代码中用三个"红线区间"校验每次分配:
| 红线 | 地址范围 | 踩中后果 |
|---|---|---|
| 下限 | < 0x119000000 | FEX 的位置相关编码 bug 被触发,分派器跳进零内存 |
| 来宾 64G 窗口 | [0x7000000000, 0x8000000000) | Wine 在此打包 PE 镜像,池代码会被误判为来宾地址,首帧黑屏 |
| 可执行窗口 | [0x140000000, +128MB) | 吞掉无重定位表游戏(如 RDR2.exe)的固定基址,TLS 指针全部失效 |
分配不合格就释放(或钉住)重试,最多 3 次;实在摆不下就宁可缩小池子甚至失败退出,也不让游戏带病运行——详细逻辑见 StikJITHelper.swift。池子就绪后还会调用jit_make_region_no_footprint()申请 jetsam 豁免,避免大块内存写入被系统按物理占用直接杀掉。
五、安全网:没人应答 BRK 时会发生什么 🛡️
协议最脆弱的一环是"BRK 发出去却没人接"。项目为每一种失败都准备了明确的退路:
- app 侧:JITAllocator.c 安装 SIGTRAP 兜底处理器。没有调试器在线时,BRK 被处理器吞掉、
x0置 0,应用得到"请求失败"的返回值,而不是崩溃 - Wine 侧:信号处理器中的
case 0xf00d专门识别游离的协议 BRK 并跳过(见 signal_arm64_ios.c),不会把协议中断当成崩溃上报 - 脚本侧:只服务 BRK。其他真实故障(缺页、非法指令等)被还原为对应的 Unix 信号交还给 Wine 的
sigaction处理器;同一故障 8 次无法投递时直接杀死进程——"可见的死亡好过一个永远挂起的线程" - UI 侧:
ready状态要求CS_DEBUGGED置位且(调试器在线 或 本次运行的池已建成),防止"标志还在、调试器已走"的假就绪(见 StikJITHelper.swift)
六、关键文件速查表
| 文件 | 角色 |
|---|---|
| app/Madeira/JITAllocator.c | BRK 发送端:jit26_prepare_region/jit26_detach |
| app/Madeira/JITAllocator.h | 协议 API 与 iOS 26 BRK 协议注释 |
| app/Madeira/madeira-jit.js | 调试器侧主循环与 x16 命令分派 |
| app/Madeira/StikJITHelper.swift | 启用入口(URL Scheme)与 JIT 池摆放策略 |
| app/MadeiraJITHelper/MadeiraJITHelper.swift | 内置 StikJIT 辅助进程(XPC 转发 PID 与脚本) |
| build/ntdll-unix/signal_arm64_ios.c | Wine 侧 0xf00d 兜底与页 0 TEB 请求 |
| docs/JIT.md | 官方 JIT 配置指南(StikDebug / 内置 StikJIT) |
💡 延伸阅读:JIT 环境的完整配置流程(StikDebug 配对、LocalDevVPN、内置配对)见 docs/JIT.md,本文协议正是由其中的"Enable JIT"按钮触发。
七、一句话总结
BRK #0xf00d是 Madeira 在 iOS 铁幕上凿出的一个"合规通道":app 用寄存器当参数发起请求,调试器脚本按x16分派三个命令——分配可执行内存、修补页 0、脱附——再写回结果。配合精心设计的地址红线、重试策略和多层兜底,这套不足 400 行脚本的轻量协议,撑起了整个 Windows PC 游戏在 iPhone 上的实时动态翻译。
【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考