Madeira 内存管理三大秘密:W^X 双映射、NO_FOOTPRINT 与地址空间预留,iPhone 跑 Windows 游戏的关键
【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira
Madeira 内存管理指南来了!Madeira 是一个让 x86-64 Windows PC 游戏通过 FEX-Emu + Wine + DXMT 在越狱前的 iOS 上运行的开源项目。iPhone 的 iOS 系统对内存有着极其严苛的限制:代码页不能又可写又可执行、内存超额度会被 Jetsam 直接杀掉进程、关键低地址区间随时可能被运行时占碎。本文将带你拆解 JITAllocator.c 与 JITAllocator.h 中破解这三大限制的核心技巧。
为什么 iPhone 上跑 Windows 游戏这么难?
在理解三大秘密之前,先看一下 Madeira 的分层架构:
| 层 | 作用 |
|---|---|
| FEX-Emu | 实时把游戏的 x86/x86-64 代码翻译成 ARM64(这就是 JIT) |
| Wine 11.4 | 提供 Windows 环境,本身以 ARM64EC 原生运行 |
| DXMT / madeira-d3d12 | 用 Metal 绘制 Direct3D 画面 |
JIT 翻译器是性能的核心——它生成的 ARM64 机器码必须被执行,而生成的过程必须写入。iOS 却同时用三道关卡拦着这条路:
- W^X 策略:同一块物理页不能同时具备"可写"和"可执行"权限
- Jetsam 内存配额:计入
phys_footprint的内存超过额度,进程直接被杀 - 地址空间碎片化:JIT 代码池和主镜像需要的低地址区间,每次启动都被运行时乱序占碎
下面逐一破解。🔧
秘密一:W^X 双映射——同一块内存,两副面孔
问题:iOS 不允许 RWX
iOS 强制实施 W^X(Write XOR Execute)策略:一页内存要么可写、要么可执行,不能两者兼得。而 JIT 编译器天然需要"先写入代码、再执行代码"。
解法:具名内存对象的两个视图
Madeira 借用了名为 MeloNX 的双映射方案,核心实现在 JITAllocator.c:
- 创建具名内存对象(named memory entry),最大权限设为 RWX
- 映射第一个视图(RW):FEX 在这里写入生成的 ARM64 代码
- 映射第二个视图(RX):FEX 从这里执行代码
两个视图指向同一块物理内存——你在 RW 视图写入的指令,立刻在 RX 视图中可执行。API 封装非常直观,见 jit_region_create / jit_region_rw_ptr / jit_region_rx_ptr:
写入走
rw_ptr,执行走rx_ptr,jit_region_write还会自动处理 ARM64 的指令缓存失效。🎯
iOS 26 之后 JIT 还必须在调试器附加时才允许,Madeira 通过 StikJITHelper.swift 中的BRK #0xf00d协议让调试器帮忙分配 RX 页,再把 RW 别名vm_remap上去,生产环境的巨型代码池就是这么建成的。
秘密二:NO_FOOTPRINT——让巨型代码池"隐形"
问题:896MB 的代码池会触发 Jetsam
FEX 的 JIT 代码池非常大——项目历史中曾尝试 896MB 甚至 1024MB。iOS 的 Jetsam 机制按phys_footprint(物理内存占用)杀进程,如果池子里每一页都计入,游戏启动阶段就会被"Terminated due to memory issue"。
解法:私有的 Mach 记账豁免 API
在 JITAllocator.c 中,Madeira 使用了两个私有 Mach 常量:
VM_LEDGER_FLAG_NO_FOOTPRINT:告诉系统"这块内存不算进 app 的内存配额"mach_memory_entry_ownership:把具名内存对象的记账所有权转移出去
标记为 NO_FOOTPRINT 后: phys_footprint 不再计入该内存池 → Jetsam 额度留给游戏本身(纹理、音频、Wine 堆)jit_make_region_no_footprint函数(JITAllocator.h 第 29 行)还有个精妙之处:由于这是 beta 系统上的私有 API,每次调用前后都会采样phys_footprint,用差值证明豁免是否真的生效——调用成功但差值≈0,就说明内核接受了调用却仍在记账。这种"用日志自证结果"的调试思路非常值得学习。📊
秘密三:地址空间预留——抢在所有人之前占坑
问题:低地址区间是稀缺资源
iPhone 18 Pro 上,有一条约 1.4GB 的低地址间隙,同时被两样东西需要:
0x140000000处的 128MB固定窗口(不可重定位的主镜像必须落在这里)- FEX 的RX JIT 代码池(调试器按 first-fit 放置,无地址提示)
而每次启动,应用自身的运行时分配都会以不同方式切进这个间隙:
启动 A: 505MB | 窗口 | 628MB | 261MB → 池可用 608MB 启动 B: 476MB | 窗口 | 604MB | 286MB → 池可用 592MB 启动 C: 463MB | 窗口 | 349MB | 181MB | 329MB → 池只有 448MB (主镜像 397MB 就把它填满了 → 加载失败,进程死亡)解法:构造器级抢占 + PROT_NONE 占位
答案藏在 JITAllocator.c 第 837-901 行 的madeira_early_va_claim:
- 它是一个
__attribute__((constructor(101)))构造器,在main之前就运行,比 SwiftUI、Metal、日志系统都早 - 先用
vm_allocate + VM_FLAGS_FIXED固定占用0x140000000窗口(128MB) - 再从上往下尝试,占住窗口上方最大的连续空闲段(1GB → 256MB,16MB 步长)
- 占住的区间全部设为
VM_PROT_NONE:只占虚拟地址,不消耗任何物理内存
等到 StikJITHelper.swift 真正请调试器分配 RX 池时,才释放占位。如果占坑失败,JITAllocator.h 第 101-102 行 定义的intruder变量会记录"是谁、多大、什么权限的映射"挡了路,让日志能直接说出原因——排查地址碎片问题的利器。🕵️
三大秘密总结
| 秘密 | 对抗的限制 | 核心技术 | 关键位置 |
|---|---|---|---|
| W^X 双映射 | 页权限互斥 | 具名内存对象 + RW/RX 双视图 | JITAllocator.c |
| NO_FOOTPRINT | Jetsam 内存配额 | 私有 Mach 记账豁免 API | JITAllocator.c |
| 地址空间预留 | VA 碎片化 | 构造器抢占 +VM_PROT_NONE占位 | JITAllocator.c |
延伸阅读
- 完整的构建与原理文档:docs/BUILDING.md
- 32 位游戏如何运行(也依赖低地址布局):docs/WOW64.md
- 3D 图形栈实现:madeira-d3d12/README.md
- 调试用的 JIT 脚本:app/Madeira/madeira-jit.js
这三招本质上是同一套思路:iOS 不给的空间,就用系统自身的机制"借"出来——权限分离借给双视图,内存额度借给私有记账 API,地址空间借给提前占坑。读懂 JITAllocator.c,你就能看懂 iPhone 模拟器内存工程的全貌。🎮
【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考