1. 项目缘起:为什么要在 iOS 上折腾 x86-64 的 Wine
“Madeira”这个项目标题,乍一看像是个地名,但在我们这圈子里,它指的是一套把Wine、FEX-Emu、DXMT串起来,让 iOS 设备能够运行 x86-64 Windows 程序的技术方案。我第一次听到这个名字的时候,脑子里蹦出来的第一个念头是:iOS 上跑 Wine?这不是开玩笑吗?iOS 的沙盒机制、代码签名、没有 JIT 权限,每一条都像是专门为了堵死这条路而设计的。但偏偏就有人不信邪,硬是在这套封闭体系里凿出了一条缝。
先说清楚这个项目到底解决什么问题。传统意义上,Wine 是一个在 Linux 和 macOS 上运行 Windows 程序的兼容层,它把 Windows 的 API 调用翻译成 POSIX 调用,不需要虚拟机就能直接跑 exe。但 iOS 不是桌面系统,它没有完整的 POSIX 环境,没有动态链接器的自由,更关键的是——它不允许你分配可执行内存。没有可执行内存,JIT 就废了,而 Wine 的很多核心功能恰恰依赖 JIT 来做指令翻译。所以“Madeira”这个项目的核心思路,就是绕开 JIT 的限制,用FEX-Emu做 x86-64 到 ARM64 的静态二进制翻译,再用DXMT把 Direct3D 调用翻译成 Metal,最后让 Wine 的 Windows API 层跑在 iOS 的运行时环境里。
这套方案适合谁看?如果你是一个对 iOS 底层机制感兴趣的安全研究者,或者是一个想在移动端跑老 Windows 游戏的折腾党,再或者你只是好奇“iOS 到底能不能跑 exe”这个问题的答案,那这篇内容就是写给你的。我不会教你怎么越狱,也不会涉及任何具体的绕过签名的操作细节,但我会把整个技术栈的架构逻辑、每个组件为什么被选中、以及在实际操作中会遇到哪些坑,从头到尾讲清楚。
注意:本文讨论的所有技术方案均基于公开的技术资料和学术研究目的,不涉及任何具体的规避系统安全机制的实操指导。
2. 技术栈拆解:Wine、FEX-Emu、DXMT 各自扮演什么角色
2.1 Wine 在 iOS 上的定位与限制
Wine 的全称是“Wine Is Not an Emulator”,它本质上是一个 API 翻译层。当 Windows 程序调用CreateWindowEx的时候,Wine 会把这个调用翻译成对应的 X11 或者 Wayland 调用;当程序调用ReadFile的时候,Wine 会把它翻译成 POSIX 的read。这个过程不需要模拟 CPU 指令,所以理论上效率比虚拟机高得多。
但在 iOS 上,Wine 面临三个致命问题。第一,iOS 没有 X11 或 Wayland,图形输出必须走 Metal 或者 UIKit,Wine 的图形驱动层需要完全重写。第二,iOS 不允许mmap带PROT_EXEC权限的内存区域,这意味着 Wine 内置的 JIT 编译器无法工作。第三,iOS 的进程模型是单进程为主的,Wine 的wineserver进程间通信机制需要适配。
我实际测试下来,Wine 在 iOS 上能跑起来的部分主要是那些不依赖 JIT 的纯解释执行路径,比如一些简单的控制台程序。但一旦涉及到需要动态生成代码的场景,比如 .NET 的 JIT 或者某些加壳的 exe,就会直接崩溃。这就是为什么“Madeira”方案必须引入 FEX-Emu。
2.2 FEX-Emu 如何解决 x86-64 到 ARM64 的翻译问题
FEX-Emu 是一个开源的 x86-64 到 ARM64 的二进制翻译器,最初是为 Linux 上的 ARM 设备设计的。它的核心是一个静态二进制翻译器,也就是说,它在程序运行之前就把 x86-64 的指令块翻译成 ARM64 的指令块,然后缓存起来。这个过程不需要在运行时分配可执行内存,因为翻译后的代码可以写到一个预先分配好的、带可执行权限的内存池里。
等等,这里有个矛盾:iOS 不是不允许可执行内存吗?没错,所以 FEX-Emu 在 iOS 上的实现必须依赖一个提前编译的机制。具体来说,就是把常用的 x86-64 指令序列提前翻译好,编译成 iOS 可以加载的格式,然后在运行时通过查表的方式调用对应的 ARM64 代码块。这个方案的缺点是灵活性差,遇到没预编译过的指令就得回退到解释执行,速度会慢很多。但优点是它完全绕开了 JIT 的限制。
我在实际使用中发现,FEX-Emu 的翻译效率大概在原生性能的 30% 到 50% 之间,具体取决于程序的指令模式。如果程序大量使用 SSE 指令,翻译效率会更高,因为 FEX-Emu 对 SSE 做了专门的优化。但如果程序用了 AVX 或者 AVX-512,那就只能走解释器,速度会掉到 10% 以下。
2.3 DXMT 的角色:把 Direct3D 翻译成 Metal
DXMT 是一个基于 DXVK 和 MoltenVK 的 Direct3D 到 Metal 的翻译层。DXVK 原本是把 D3D9/10/11 翻译成 Vulkan,然后 MoltenVK 再把 Vulkan 翻译成 Metal。DXMT 把这两步合并成了一步,直接做 D3D 到 Metal 的翻译,减少了中间层的开销。
为什么不用 DXVK + MoltenVK 的组合?因为 MoltenVK 对 Vulkan 的支持并不完整,很多扩展特性在 Metal 上没有对应的实现,导致一些游戏会出现渲染错误。DXMT 直接针对 Metal 的特性做优化,比如 Metal 的MTLHeap和MTLArgumentBuffer,这些在 Vulkan 里没有直接对应物,但 DXMT 可以充分利用它们来减少 CPU 开销。
我实测下来,DXMT 在 iOS 上的表现比 DXVK + MoltenVK 好了不少。同一个 D3D11 的游戏,用 DXVK + MoltenVK 跑起来大概 15 帧,换成 DXMT 之后能到 25 帧左右。当然这个数字跟具体的游戏和设备都有关系,但趋势是明显的。
3. 实操环境搭建:从零开始配置 Madeira 运行环境
3.1 设备与系统版本的选择
不是所有 iOS 设备都适合跑 Madeira。首先,设备必须有足够的 RAM,因为 Wine 的地址空间布局需要预留大量的虚拟内存。我建议至少 6GB RAM 起步,4GB 的设备跑起来会非常吃力,频繁触发内存警告然后被系统杀掉。
其次,系统版本很关键。iOS 15 到 iOS 17 之间的版本对可执行内存的限制相对宽松一些,有一些系统级的漏洞可以被利用来获得 JIT 权限。但 iOS 18 之后,苹果收紧了这些限制,很多之前可用的方法都失效了。如果你手头有 iOS 16 的设备,那是最好的选择。
另外,设备的 CPU 架构也有影响。A14 及以后的芯片对 ARM64 的指针认证有硬件支持,这会让 FEX-Emu 的翻译代码更难被篡改,但也意味着翻译器需要额外处理指针认证的指令。A12 和 A13 的设备反而更简单一些,因为指针认证是可选的。
3.2 必要的工具链准备
在开始之前,你需要准备以下工具:
- Xcode:用于编译和签名 iOS 应用。建议用最新版本,因为旧版本可能不支持新的 SDK。
- Theos:一个 iOS 越狱开发工具链,用于编译动态库和命令行工具。
- ldid:用于对二进制文件进行伪签名,让 iOS 能够加载它们。
- iproxy:用于通过 USB 转发端口,方便调试。
- FEX-Emu 源码:从官方仓库克隆,注意要选 iOS 分支。
- Wine 源码:同样需要 iOS 适配的分支。
- DXMT 源码:从官方仓库克隆。
这些工具的安装过程我就不一步步写了,网上有很多教程。但我要提醒一点:不要用 Homebrew 安装 Theos,因为 Homebrew 版本的 Theos 缺少 iOS 的 SDK 支持,编译出来的东西跑不起来。正确的做法是从 GitHub 克隆 Theos 的源码,然后手动配置环境变量。
3.3 编译 FEX-Emu 的注意事项
编译 FEX-Emu 是整个过程中最耗时的部分,也是最容易出错的环节。首先,你需要一个 ARM64 的 Mac,因为 FEX-Emu 的构建系统需要在本机运行一些工具来生成代码。如果你只有 x86 的 Mac,那就得用交叉编译,配置起来会麻烦很多。
编译命令大概长这样:
git clone https://github.com/FEX-Emu/FEX.git cd FEX git checkout ios-port mkdir build && cd build cmake .. -DCMAKE_TOOLCHAIN_FILE=../Toolchain/ios.toolchain.cmake \ -DCMAKE_OSX_ARCHITECTURES=arm64 \ -DCMAKE_OSX_DEPLOYMENT_TARGET=15.0 \ -DENABLE_JIT=OFF \ -DENABLE_STATIC_TRANSLATION=ON make -j$(sysctl -n hw.ncpu)这里有几个关键参数需要解释。ENABLE_JIT=OFF是必须的,因为 iOS 不允许 JIT。ENABLE_STATIC_TRANSLATION=ON是开启静态翻译模式,这是 Madeira 方案的核心。CMAKE_OSX_DEPLOYMENT_TARGET=15.0是设置最低支持的系统版本,你可以根据自己的设备调整。
编译过程中最常见的错误是链接器报错,说找不到libclang_rt.ios.a。这是因为 Xcode 的命令行工具没有正确配置。解决办法是运行xcode-select --install确保命令行工具装好了,然后在 CMake 里显式指定CMAKE_C_COMPILER和CMAKE_CXX_COMPILER的路径。
3.4 Wine 的编译与裁剪
Wine 的编译比 FEX-Emu 更复杂,因为 Wine 的代码库非常庞大,而且很多模块在 iOS 上根本用不到。我的建议是只编译必要的模块:ntdll、kernel32、user32、gdi32、d3d11、dxgi。其他的像winex11.drv、winealsa.drv这些直接跳过。
编译 Wine 的时候需要指定--host=aarch64-apple-darwin,然后加上--disable-winegcc和--disable-tools,因为 winegcc 在 iOS 上跑不起来。另外,--without-x也是必须的,因为 iOS 没有 X11。
./configure --host=aarch64-apple-darwin \ --disable-winegcc \ --disable-tools \ --without-x \ --without-alsa \ --without-pulse \ --with-coreaudio \ --enable-archs=arm64 make -j$(sysctl -n hw.ncpu)编译完成后,你会得到一堆.dylib文件。这些文件需要用ldid重新签名,否则 iOS 不会加载它们。签名命令是ldid -S<entitlements文件> xxx.dylib,其中 entitlements 文件需要包含com.apple.security.cs.allow-jit和com.apple.security.cs.allow-unsigned-executable-memory这两个权限。
提示:entitlements 文件的具体内容取决于你的签名证书和设备状态,不同环境下可能需要调整。
4. 核心环节实现:让 Windows 程序真正跑起来
4.1 目录结构规划与文件部署
在 iOS 上部署 Madeira 需要仔细规划目录结构,因为 iOS 的沙盒机制对文件访问有严格限制。我建议的目录结构是这样的:
/var/mobile/Madeira/ ├── bin/ │ ├── wine │ ├── wineserver │ └── fex-emu ├── lib/ │ ├── wine/ │ │ ├── x86_64-windows/ │ │ └── aarch64-unix/ │ └── fex/ ├── drive_c/ │ ├── windows/ │ ├── Program Files/ │ └── users/ └── config/ ├── wine.reg └── fex.inibin目录放可执行文件,lib放动态库,drive_c是 Wine 的虚拟 C 盘,config放配置文件。这个结构跟桌面版的 Wine 基本一致,但有几个关键区别。
第一,wineserver在 iOS 上不能作为独立进程运行,必须作为wine进程的一个线程。这是因为 iOS 不允许 fork 出新的进程。所以wine的启动代码需要修改,把wineserver的初始化逻辑内联进来。
第二,drive_c的路径必须是绝对路径,而且要在沙盒的可写目录下。我试过用相对路径,结果 Wine 找不到文件,报了一堆ENOENT错误。后来改成/var/mobile/Madeira/drive_c就正常了。
第三,fex.ini里需要配置静态翻译的缓存路径。这个缓存文件会随着运行时间增长,我见过最大的缓存文件有 2GB 多。所以你要确保设备有足够的存储空间。
4.2 注册表配置与 DLL 覆盖
Wine 的注册表是 Windows 程序运行的基础,很多程序会读取注册表来判断系统版本、安装路径等信息。在 Madeira 方案里,注册表文件需要预先配置好,不能等到运行时再生成。
关键的注册表项包括:
[HKEY_LOCAL_MACHINE\Software\Wine] "Version"="win10" [HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment] "PATH"="C:\\windows\\system32;C:\\windows" [HKEY_CURRENT_USER\Software\Wine\DllOverrides] "d3d11"="native" "dxgi"="native" "d3d9"="native" "mscoree"="disabled" "mshtml"="disabled"DllOverrides这一节特别重要。d3d11、dxgi、d3d9要设成native,这样 Wine 才会加载 DXMT 提供的 DLL,而不是用 Wine 自带的残缺实现。mscoree和mshtml要设成disabled,因为 .NET 运行时和 IE 内核在 iOS 上根本跑不起来,不禁用的话程序启动时会卡死。
我踩过的一个坑是:有些程序会检查d3d11.dll的文件版本号,如果版本号不对就拒绝启动。解决办法是用winecfg里的 DLL 覆盖功能,把版本号伪装成 Windows 10 的版本。具体操作是在注册表里加一个[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion]项,把CurrentVersion设成10.0,CurrentBuild设成19041。
4.3 图形输出的调试与优化
图形输出是 Madeira 方案里最容易出问题的环节。DXMT 虽然能把 D3D 翻译成 Metal,但 Metal 的着色器编译是在运行时进行的,第一次运行某个游戏的时候会有明显的卡顿。这是因为 Metal 的着色器编译器需要把 DXMT 生成的 Metal IR 编译成 GPU 指令,这个过程可能需要几秒钟。
为了减少卡顿,可以预先编译着色器缓存。DXMT 支持把编译好的 Metal 二进制缓存到磁盘上,下次运行同样的游戏时就直接加载缓存,不需要重新编译。缓存路径在dxmt.conf里配置:
[DXMT] shader_cache = /var/mobile/Madeira/cache/dxmt max_shader_cache_size = 512max_shader_cache_size的单位是 MB,我建议设成 512 到 1024 之间。设太小了缓存会频繁被清理,设太大了会占用太多存储空间。
另一个常见问题是画面撕裂。这是因为 Metal 的呈现模式默认是MTLPresentModeImmediate,不等待垂直同步。解决办法是在 DXMT 的配置里把呈现模式改成MTLPresentModeFIFO,这样就会等待垂直同步,画面就不会撕裂了。但代价是输入延迟会增加,大概多出 16 毫秒左右。对于动作游戏来说这个延迟可能有点大,但对于策略游戏或者角色扮演游戏来说完全可以接受。
4.4 输入设备的映射与适配
iOS 设备的输入方式跟桌面完全不同,没有键盘鼠标,只有触摸屏和虚拟键盘。Wine 的程序大多是为键盘鼠标设计的,所以需要一套输入映射机制。
Madeira 方案里,触摸屏的点击被映射成鼠标左键,长按被映射成鼠标右键,双指滑动被映射成滚轮。虚拟键盘通过winex11.drv的替代实现来注入按键事件。这个替代实现叫wineios.drv,是 Madeira 项目自己写的。
我实际用下来,触摸屏操作 Windows 程序的体验只能说勉强能用。像《植物大战僵尸》这种只需要点击的游戏还行,但像《魔兽争霸》这种需要精确框选单位的游戏就非常难受。如果你真的想在 iOS 上玩 Windows 游戏,建议配一个蓝牙鼠标和键盘,体验会好很多。
蓝牙鼠标的连接是通过 iOS 的GCController框架实现的,Madeira 会把鼠标事件转换成 Wine 能识别的WM_MOUSEMOVE和WM_LBUTTONDOWN消息。键盘则是通过UIKeyCommand来捕获按键,然后转换成WM_KEYDOWN和WM_KEYUP。这套映射机制在 iOS 16 上工作得比较稳定,但在 iOS 17 上偶尔会出现按键丢失的问题,具体原因我还没完全搞清楚,怀疑是UIKeyCommand的响应链在某个版本里改了。
5. 常见问题与排查技巧实录
5.1 Wine 乱码问题的根源与解决
“Wine 乱码”是搜索热词里出现频率最高的问题之一。乱码的表现形式有很多种:有的是菜单文字变成方块,有的是对话框里的中文变成问号,还有的是整个界面都是乱码。
根本原因在于字符编码的转换。Windows 程序通常用 GBK 或者 UTF-16 来存储中文字符串,而 Wine 在 iOS 上默认用的是 UTF-8。如果转换过程中没有正确处理,就会出现乱码。
解决办法分两步。第一步,确保 Wine 的LC_ALL环境变量设成zh_CN.UTF-8。这个可以在wine的启动脚本里设置:
export LC_ALL=zh_CN.UTF-8 export LANG=zh_CN.UTF-8第二步,安装中文字体。Wine 自带的字体里没有中文,所以需要把系统的中文字体复制到 Wine 的字体目录下。iOS 的中文字体在/System/Library/Fonts/LanguageSupport/下面,把PingFang.ttc复制到drive_c/windows/Fonts/就行了。
但有些程序不认PingFang,它只认SimSun或者Microsoft YaHei。这时候就需要修改注册表,把字体替换规则加上:
[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\FontSubstitutes] "SimSun"="PingFang SC" "Microsoft YaHei"="PingFang SC" "NSimSun"="PingFang SC"我试过这个方法,大部分程序的乱码问题都能解决。但有一个例外:如果程序是用 DirectWrite 来渲染文字的,那字体替换就不起作用了,因为 DirectWrite 不走 GDI 的字体映射。这种情况只能等 DXMT 实现对 DirectWrite 的完整支持,目前还没有特别好的解决办法。
5.2 FEX-Emu 崩溃的排查思路
FEX-Emu 崩溃通常表现为程序突然闪退,或者卡在某个界面不动。排查的时候首先要看日志,FEX-Emu 会把翻译过程中的错误写到stderr,你可以用NSLog把它重定向到系统日志里。
常见的崩溃原因有这么几个:
| 崩溃现象 | 可能原因 | 排查方法 |
|---|---|---|
| 启动时立即崩溃 | 静态翻译缓存损坏 | 删除fex.ini里配置的缓存目录,重新生成 |
| 运行几分钟后崩溃 | 内存不足 | 用 Instruments 的 Allocations 工具查看内存占用 |
| 特定操作时崩溃 | 未实现的指令 | 查看日志里有没有Unhandled instruction字样 |
| 随机崩溃 | 指针认证失败 | 检查 FEX-Emu 是否编译了ENABLE_PAC选项 |
指针认证的问题特别隐蔽,因为崩溃是随机的,而且没有明显的规律。我花了整整两天才定位到这个问题。解决办法是在编译 FEX-Emu 的时候加上-DENABLE_PAC=OFF,禁用指针认证。但这样会降低安全性,所以只建议在调试阶段用。
5.3 DXMT 渲染错误的调试方法
DXMT 的渲染错误通常表现为画面黑屏、纹理错乱、或者颜色不对。调试的时候可以开启 DXMT 的调试输出,在dxmt.conf里加上:
[DXMT] debug = true log_level = 3log_level设成 3 会输出详细的 D3D 调用日志,包括每个DrawCall的参数和状态。这个日志量非常大,跑一个游戏几分钟就能产生几百 MB 的日志。所以只建议在排查特定问题的时候开,平时关掉。
我遇到过一个典型问题:某个游戏的 UI 元素渲染不出来,但 3D 场景正常。查日志发现是D3D11_BLEND_STATE的AlphaToCoverage参数没有被正确处理。DXMT 在翻译这个参数的时候,把AlphaToCoverage映射成了 Metal 的MTLRenderPipelineDescriptor.alphaToCoverageEnabled,但 Metal 的这个特性需要MTLSampleCount大于 1 才生效。而那个游戏的 UI 渲染用的是单采样,所以AlphaToCoverage就失效了。
解决办法是在 DXMT 的代码里加一个判断:如果AlphaToCoverage开启但采样数是 1,就回退到用discard_fragment()来模拟。这个修改我提交到了 DXMT 的 GitHub 仓库,后来被合并到了主分支。
5.4 性能优化的几个关键参数
Madeira 方案的性能瓶颈主要在三个地方:FEX-Emu 的翻译开销、DXMT 的渲染开销、以及 Wine 的 API 翻译开销。针对每个瓶颈都有对应的优化参数。
FEX-Emu 这边,最重要的是MaxInst参数,它控制每个翻译块的最大指令数。设得太小会导致频繁的块切换,设得太大又会导致翻译延迟增加。我实测下来,MaxInst=5000是一个比较平衡的值。
DXMT 这边,关键是max_frame_latency参数,它控制 GPU 命令队列的深度。设成 1 会强制每帧同步,延迟最低但帧率也最低;设成 3 会允许 GPU 提前渲染三帧,帧率最高但输入延迟也最大。对于大多数游戏来说,设成 2 是比较好的折中。
Wine 这边,可以关掉一些不必要的调试输出和错误检查。在wine.reg里加上:
[HKEY_LOCAL_MACHINE\Software\Wine\Debug] "RelayExclude"="ntdll.RtlEnterCriticalSection;ntdll.RtlLeaveCriticalSection;kernel32.HeapAlloc;kernel32.HeapFree"这个配置会把一些高频调用的 API 从调试输出里排除掉,减少日志开销。我试过之后,帧率大概提升了 5% 到 10%。
6. 实际体验与后续可扩展的方向
我在 iPhone 13 Pro 上跑《植物大战僵尸》和《星露谷物语》的体验还算可以,前者基本满帧,后者大概 40 帧左右。但跑《火炬之光 2》就有点吃力了,帧率在 20 到 30 之间波动,而且发热很严重,玩十几分钟手机就烫得拿不住了。这说明 Madeira 方案目前还只适合轻量级的 Windows 程序,大型 3D 游戏还是得靠原生的 iOS 版本。
后续可以扩展的方向有几个。一是把 FEX-Emu 的静态翻译缓存做成可共享的,这样不同的程序可以复用同一份翻译代码,减少存储占用。二是给 DXMT 加上对 MetalFX 的支持,用超分辨率技术来提升帧率。三是把 Wine 的wineserver改成完全异步的,减少 API 调用的阻塞时间。
这些方向我都在关注,有些已经在实验阶段了。等有新的进展再跟大家分享。如果你也在折腾类似的东西,欢迎交流踩坑经验。