Madeira的FEXBridge如何向xtajit64暴露JIT写偏移:一个救命的细节设计
【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira
想在越狱 iOS 手机上运行 x86-64 Windows 游戏,听起来像科幻?Madeira 做到了:它用 FEX-Emu 指令翻译 + Wine + DXMT,让苹果 A 系列芯片上的"沙盒 iOS"跑起 PC 游戏。其中有一个不起眼的细节,却决定了整套 JIT 能否启动:iOS 宿主端的 FEXBridge 必须在正确的时机,把 JIT 写偏移量交给客端 DLL xtajit64——本文带你拆解这个"救命的细节设计"。
先搞懂背景:iOS 为什么不能直接 JIT
JIT(即时编译)的工作原理是:一边把 x86-64 指令翻译成 ARM64 指令,一边把生成的机器码写入内存并立即执行。这要求内存同时具备"可写 + 可执行"两种属性。
而 iOS 从内核层面就封死了这条路——沙盒应用拿不到 RX(可执行)权限。Madeira 的解法是借助越狱后的调试器(StikDebug)"借壳":由调试器代为分配可执行页,应用再想办法"偷偷"获得写入口。
相关能力封装在 JITAllocator.h 的jit26_prepare_region协议中。
FEXBridge 的"双视图内存池":RX 看,RW 写
FEXBridge.mm 的jit_pool_init实现了一套教科书级的双映射 JIT 池(64MB、16KB 页):
- 向调试器申请 RX 页——拿到只能读、能执行,不能写的"执行视图";
vm_remap镜像——同一块物理内存,再映射出一份 RW(可读可写)视图;- 一致性自检——往 RW 视图写入魔数
0xCAFEBABE,从 RX 视图读回验证,确保两个视图真的是"同一块内存"。
从此 FEXCore 翻译器把生成的 ARM64 代码写进 RW 视图、从 RX 视图执行。写地址与执行地址之间的那段距离,就是JIT 写偏移(WriteOffset):
WriteOffset = RW视图基址 - RX视图基址FEXBridge.mm 会在初始化时把它写入FEXCore::DualMap::WriteOffset,宿主端 FEXCore 从此各归其位。
为什么固定偏移在 iOS 上"失效"
在 Linux 上,FEX 的双映射布局是固定的,RW 别名总挂在 RX 基址上方 256MB 处——双方心照不宣,无需沟通。
但 iOS 上情况完全不同:vm_remap用的是VM_FLAGS_ANYWHERE,由操作系统自行决定 RW 别名落在哪里,RX + 256MB 的约定根本不成立。
偏偏客端还有一份 FEXCore 拷贝:由 build.sh 构建出的xtajit64.dll(ARM64EC 模式运行在 Wine 内部)。它同样需要写偏移来定位自己的 JIT 池,可它拿到的 RX 地址和宿主端是独立分配的——唯一的救命稻草,就是宿主端把"真实的运行时偏移"亲手告诉它。
偏移如何"穿越"到 xtajit64:一条环境变量的接力赛
跨进程传递一个运行时才确定的数值,Madeira 选了一条朴素但可靠的路:环境变量。整个接力分四棒:
| 接力棒 | 位置 | 动作 |
|---|---|---|
| 第 1 棒 | FEXBridge.h | 声明 C APIfex_get_jit_write_offset(),实时计算RW - RX |
| 第 2 棒 | FEXBridge.mm | 实现该函数,池未初始化则返回 0 |
| 第 3 棒 | WineProcessBridge.m | 启动客端前setenv("MADEIRA_JIT_WRITE_OFFSET", "0x...") |
| 第 4 棒 | env_ios.c | Wine 的 ntdll 在环境转换时显式放行该变量,确保它不被丢弃 |
最后 xtajit64.dll 在自己的ProcessInit里用getenv读出这个十六进制偏移,喂给自己的 FEXCore 拷贝。一条0x开头的字符串,就这样把宿主与客端两个 JIT 世界缝在了一起。
那个"救命的细节":setenv 的时机
设计里最烧脑的,不是"传什么",而是"何时传"。
代码里留了一段自述:最初开发者把setenv写在jit_pool_init里——逻辑上也说得通,池子刚建好就公布偏移。但实测发现,xtajit64 的GetEnvironmentVariableW永远读不到它。
原因在于 iOS 宿主端与 Wine 客端的环境快照机制:Wine 只在自己启动时对宿主环境做一次快照。在池子初始化那一刻设置变量,早于快照点,等于对空气说话。
最终方案是把setenv挪到 WineProcessBridge.m 中紧邻SteamAppPath设置的那几行——这正是 Wine 即将做环境快照的"发射窗口"。同时兜底逻辑也没落下:若fex_get_jit_write_offset()返回 0(池子没初始化),日志会大声告警而不是静默带病运行。
一个注释里写清楚"为什么不能放那儿"的决策,往往比十行代码更值钱——这就是本设计被称为"救命细节"的原因。
顺带一提:偏移为 0 也是合法信息
fex_get_jit_write_offset()在池子尚未初始化时返回 0,调用方据此区分"尚未就绪"与"真实偏移"。这种用哨兵值表达状态的小设计,让整条链路无需额外的锁或状态标志,简单到近乎苛刻。
想深入阅读?按这份地图走
- 🏗️ 桥接层全貌:FEXBridge.mm(双映射池、mmap 钩子、FEXCore 初始化)
- 📐 公开 C 接口声明:FEXBridge.h
- 🕵️ 调试器 BRK 协议:JITAllocator.h 与 JITAllocator.c
- 📮 偏移发布点(救命细节现场):WineProcessBridge.m
- 🛂 Wine 侧环境变量放行:env_ios.c
- 📦 xtajit64.dll 构建脚本:build.sh
- 📖 构建与架构背景:BUILDING.md、WOW64.md
结语
Madeira 能让 Windows 游戏跑进 iPhone,靠的不仅是 FEX + Wine + DXMT 的大组合拳,更是这些藏在注释与环境变量里的细节功夫:一个偏移值,两个 FEXCore 世界,一次踩在"发射窗口"上的setenv。理解了这个设计,你也理解了嵌入式跨平台工程的核心美学——在受限的系统里,用最小、最朴素的通道传递最关键的那一个数值。
【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考