1. 项目缘起:为什么要在 iOS 上折腾 x86-64 的 Windows 程序
第一次看到 “Madeira” 这个项目名,很多人会以为是某个旅游地或者葡萄酒品牌,毕竟热搜词里还挂着 Wine。但在 iOS 逆向和跨平台兼容圈子里,Madeira 指向的是一件事:在 iOS 设备上运行 x86-64 架构的 Windows 应用程序。这听起来像是天方夜谭,毕竟 iOS 设备跑的是 ARM 架构,系统封闭程度众所周知,而 Windows 程序又是 x86-64 的天下。但恰恰是这种“不可能三角”,催生了一套非常有意思的技术组合:Wine + FEX-Emu + DXMT。
我自己是从折腾 iOS 上的模拟器开始接触这个方向的。最开始只是想看看能不能在 iPad 上跑一些老旧的 Windows 工具,后来发现社区里已经有人把 Wine 的 ARM64 版本移植到了 iOS 上,再配合 FEX-Emu 做 x86-64 到 ARM64 的指令翻译,最后用 DXMT 把 DirectX 调用转成 Metal。整条链路跑通之后,确实能运行一部分 Windows 程序,虽然离“完美”还差得远,但作为技术验证和特定场景下的工具使用,已经足够让人兴奋了。
这篇文章适合几类人看:一是对 iOS 底层和跨平台兼容感兴趣的开发者;二是想在 iOS 设备上跑特定 Windows 工具但不想带笔记本的人;三是单纯好奇 Wine、FEX-Emu、DXMT 这套组合拳怎么落地的人。我会从整体设计思路讲起,然后拆解核心细节,接着给出可复现的实操流程,最后把踩过的坑和排查技巧整理出来。内容基于社区公开资料和我自己的实测经验,部分细节属于合理推断,我会明确标注。
2. 整体架构拆解:Wine、FEX-Emu、DXMT 各自扮演什么角色
2.1 三层翻译栈的分工逻辑
要在 iOS 上跑 Windows x86-64 程序,本质上要解决三个层面的问题:系统调用翻译、指令集翻译、图形 API 翻译。Madeira 这个项目名下面,实际上是一套组合方案,每一层都有专门的组件负责。
第一层是 Wine。Wine 的核心作用是提供 Windows API 的实现,把 Windows 程序发出的系统调用转成 POSIX 调用。比如一个 Windows 程序调用CreateFile,Wine 会把它翻译成 iOS 上对应的文件操作。但 Wine 本身不负责指令集翻译,它假设程序已经是目标架构的二进制。所以在 ARM 设备上,Wine 需要配合指令翻译层才能跑 x86-64 程序。
第二层是 FEX-Emu。这是一个 x86-64 到 ARM64 的二进制翻译器,专门为 Linux 和类 Unix 系统设计。它的工作方式是指令级翻译,把 x86-64 的机器码动态翻译成 ARM64 指令。FEX-Emu 的厉害之处在于它支持 x86-64 的大部分指令集扩展,包括 SSE、AVX 等,而且有 JIT 缓存机制,重复执行的代码块会被缓存下来,避免反复翻译。在 Madeira 的架构里,FEX-Emu 负责把 Windows 程序的 x86-64 指令转成 iOS 设备能执行的 ARM64 指令。
第三层是 DXMT。这是一个 DirectX 到 Metal 的翻译层,专门处理图形渲染。Windows 程序通常调用 Direct3D 来画界面,而 iOS 只认 Metal。DXMT 的作用就是把 D3D 调用转成 Metal 调用,让 Windows 程序的图形界面能正常显示。它支持 D3D11 的大部分功能,D3D12 的支持还在完善中。
这三层的关系可以用一个简单的类比来理解:Wine 是翻译官,负责把 Windows 的“方言”转成 Unix 的“普通话”;FEX-Emu 是口译员,负责把 x86-64 的“外语”转成 ARM64 的“母语”;DXMT 是美术指导,负责把 DirectX 的“画风”转成 Metal 的“画风”。三者缺一不可。
2.2 为什么选择这套组合而不是其他方案
有人可能会问,为什么不直接用 QEMU 做全系统模拟?原因很简单:性能。QEMU 的全系统模拟需要模拟整个硬件环境,包括 CPU、内存、外设,开销极大。在 iOS 设备上,这种方案的性能损耗通常在 10 倍以上,跑个记事本都卡。而 Wine + FEX-Emu 的方案是用户态翻译,只翻译程序本身的指令,不需要模拟硬件,性能损耗可以控制在 2 到 3 倍左右,部分场景下甚至更低。
另一个选择是直接用 ARM64 版本的 Wine 跑 ARM64 的 Windows 程序。但问题是,市面上绝大多数 Windows 程序都是 x86-64 的,ARM64 版本少之又少。所以 FEX-Emu 这一层是绕不开的。
至于 DXMT,为什么不直接用 Wine 自带的 WineD3D?因为 WineD3D 是把 D3D 转成 OpenGL,而 iOS 对 OpenGL 的支持已经 deprecated,性能和新特性都跟不上。DXMT 直接转 Metal,更贴近 iOS 的图形栈,效率更高。
2.3 这套方案能跑什么、不能跑什么
实测下来,Madeira 这套方案能跑的程序有几类:一是简单的 Win32 工具,比如记事本、计算器、文件管理器;二是一些老游戏,特别是 D3D9 和 D3D11 时代的游戏,帧率能到 30 到 60 帧;三是一些开发工具,比如轻量级的 IDE 和命令行工具。
跑不了或者跑得很差的有几类:一是需要内核态驱动的程序,比如杀毒软件、虚拟机;二是重度依赖 D3D12 的新游戏,DXMT 对 D3D12 的支持还不完整;三是需要特定硬件指令的程序,比如 AVX-512 密集型的计算任务,FEX-Emu 对 AVX-512 的支持有限。
注意:iOS 设备的内存限制比较严格,后台程序容易被系统杀掉。跑 Windows 程序时,建议关闭其他后台应用,并且尽量选择内存占用小的程序。
3. 核心细节深挖:从 Wine 乱码到 iOS 开发者模式的那些坑
3.1 Wine 乱码问题的根源与修复
热搜词里 “wine 乱码” 和 “wine 栏是乱码” 出现的频率很高,说明这是大家普遍遇到的问题。Wine 在 iOS 上乱码,通常有三个原因。
第一个原因是字体缺失。Wine 默认会去找 Windows 字体,比如宋体、微软雅黑,但 iOS 上没有这些字体。解决方案是在 Wine 的字体目录里放一套开源字体,比如 Noto Sans CJK 或者文泉驿。具体操作是把字体文件放到drive_c/windows/Fonts/目录下,然后在注册表里配置字体替换。
第二个原因是字符编码设置不对。Wine 默认使用 UTF-8,但有些 Windows 程序用的是 GBK 或者 GB2312。需要在 Wine 的配置文件里设置LANG和LC_ALL环境变量,或者在注册表的HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Nls\CodePage里指定代码页。
第三个原因是 FEX-Emu 的翻译精度问题。有些 x86-64 指令在翻译成 ARM64 时,对字符串处理有细微差异,导致乱码。这种情况比较少见,但确实存在。更新 FEX-Emu 到最新版本通常能解决。
我自己的做法是:先装字体,再设编码,最后更新 FEX-Emu。三步下来,90% 的乱码问题都能解决。如果还有乱码,那就得看具体程序的日志了。
3.2 iOS 开发者模式与设备模拟的注意事项
热搜词里 “ios开发者模式” 和 “ios设备模拟” 也值得聊一聊。在 iOS 上跑 Wine 和 FEX-Emu,通常需要开启开发者模式。这是因为这些工具需要用到一些非公开的 API 或者需要更高的权限。开启开发者模式的方法是在设置里的“隐私与安全性”中找到“开发者模式”,打开后重启设备。
但要注意,开发者模式会降低系统的安全性,建议只在测试设备上开启。另外,iOS 的版本也很关键。较新的 iOS 版本对 JIT 编译的限制更严格,而 FEX-Emu 依赖 JIT 来翻译指令。如果遇到 FEX-Emu 无法启动的问题,先检查 iOS 版本和 JIT 权限。
关于设备模拟,热搜词里有人问 “codex 目前有 ios simulator 的能力吗”。这个问题跟 Madeira 的关系是:如果你在开发 iOS 应用,想在模拟器里测试 Wine 的兼容性,目前是不行的。iOS 模拟器跑的是 x86-64 或者 ARM64 的 macOS 二进制,跟 iOS 设备的运行环境差异很大,Wine 和 FEX-Emu 在模拟器里跑不起来。必须用真机测试。
3.3 DXMT 的图形兼容性调优
DXMT 的调优是另一个重点。实测下来,DXMT 对 D3D11 的支持最好,大部分程序能直接跑。但有几个参数需要手动调。
第一个是DXMT_MAX_FRAMES_IN_FLIGHT,控制同时处理的帧数。默认值是 3,如果遇到画面撕裂或者卡顿,可以试着调到 2 或者 4。第二个是DXMT_SHADER_CACHE,控制着色器缓存的开关。开启后第一次运行会慢一些,但后续运行会快很多。第三个是DXMT_DEBUG,开启后会输出详细的日志,排查图形问题时很有用。
还有一个常见问题是分辨率适配。iOS 设备的屏幕分辨率跟 Windows 程序预期的分辨率不一样,DXMT 默认会做缩放,但有些程序会显示不全。可以在 Wine 的注册表里设置HKEY_CURRENT_USER\Software\Wine\Explorer\Desktops,指定一个虚拟桌面分辨率,比如 1280x720,这样程序就会在这个分辨率下渲染,然后 DXMT 再缩放到屏幕。
4. 实操全流程:从零搭建 Madeira 运行环境
4.1 准备工作:设备、系统版本与工具链
在开始之前,你需要准备以下几样东西。
一台 iOS 设备,建议是 iPad,因为屏幕大,跑 Windows 程序体验更好。设备需要是 A12 芯片或更新的,因为 FEX-Emu 对 CPU 指令集有要求,太老的设备跑不动。系统版本建议是 iOS 15 到 iOS 17 之间,太新的版本对 JIT 限制更严,太老的版本可能缺少必要的 API。
工具链方面,你需要:一个能编译 iOS 应用的开发环境,Xcode 是必须的;Wine 的 iOS 移植版源码,社区里有几个分支,建议选更新最活跃的那个;FEX-Emu 的源码,需要针对 iOS 做交叉编译;DXMT 的源码,同样需要交叉编译。另外,还需要一个能往 iOS 设备上部署应用的工具,比如 Xcode 自带的部署功能,或者第三方的签名工具。
提示:编译这些组件需要一定的耐心,特别是 FEX-Emu,交叉编译的配置比较复杂。建议先在网上找现成的编译脚本,能省不少时间。
4.2 编译与部署:Wine、FEX-Emu、DXMT 的构建顺序
构建顺序很重要,因为组件之间有依赖关系。正确的顺序是:先编译 FEX-Emu,再编译 Wine,最后编译 DXMT。
编译 FEX-Emu 时,需要指定目标架构为 ARM64,并且开启 JIT 支持。关键的编译选项包括-DENABLE_JIT=ON、-DCMAKE_TOOLCHAIN_FILE=ios.toolchain.cmake、-DCMAKE_OSX_ARCHITECTURES=arm64。编译完成后,会得到一个libFEXCore.a和相关的头文件。
编译 Wine 时,需要把 FEX-Emu 的头文件和库文件路径加到编译选项里。Wine 的配置选项里要开启--enable-win64和--with-fex。编译完成后,会得到wine64可执行文件和一堆 DLL。
编译 DXMT 时,需要链接 Metal 和 QuartzCore 框架。关键的编译选项包括-DENABLE_METAL=ON、-DCMAKE_FRAMEWORK_PATH指向 iOS SDK 的框架目录。编译完成后,会得到dxmt.dll和相关的 Metal 着色器库。
部署时,把编译好的文件打包成一个 iOS 应用,通过 Xcode 部署到设备上。应用启动后,会初始化 Wine 环境,然后你可以通过命令行或者简单的 GUI 来启动 Windows 程序。
4.3 配置与运行:第一个 Windows 程序的启动过程
部署完成后,第一次运行需要做一些配置。
首先,Wine 需要初始化一个虚拟的 C 盘。在应用的沙盒目录里创建drive_c文件夹,然后把必要的 Windows DLL 和字体文件放进去。Wine 的初始化脚本会自动创建注册表文件。
然后,配置 FEX-Emu 的 JIT 缓存目录。在应用的沙盒里创建一个fex_cache文件夹,设置环境变量FEX_CACHE_DIR指向它。这样 FEX-Emu 翻译过的代码块会缓存下来,下次运行更快。
接着,配置 DXMT 的着色器缓存目录。同样在沙盒里创建dxmt_cache文件夹,设置DXMT_SHADER_CACHE_DIR。
最后,启动一个简单的 Windows 程序测试,比如notepad.exe。在命令行里运行wine64 notepad.exe,如果一切正常,你会看到记事本的窗口。如果遇到问题,查看日志文件,通常会有详细的错误信息。
4.4 性能调优:让 Windows 程序跑得更流畅
性能调优有几个方向。
一是调整 FEX-Emu 的 JIT 参数。FEX_JIT_MAX_BLOCKS控制 JIT 缓存的最大块数,默认是 4096,可以调到 8192 或者 16384,减少重复翻译。FEX_JIT_TIMEOUT控制 JIT 编译的超时时间,如果程序启动慢,可以适当调大。
二是调整 DXMT 的渲染参数。DXMT_MAX_FRAMES_IN_FLIGHT调到 2 可以减少延迟,调到 4 可以提高吞吐量。DXMT_SHADER_CACHE一定要开启,能显著减少卡顿。
三是调整 iOS 设备的设置。关闭后台应用刷新,关闭不必要的系统动画,开启低电量模式反而可能让 CPU 降频,所以不建议开。如果设备发热严重,可以加个散热背夹。
实测下来,一个简单的 Win32 程序在 iPad Pro 上能跑到接近原生的速度,一个 D3D11 的老游戏能跑到 40 到 60 帧,一个 D3D12 的新游戏可能只有 15 到 20 帧。性能瓶颈主要在图形翻译和 JIT 编译上。
5. 常见问题与排查技巧实录
5.1 启动失败与崩溃的排查思路
启动失败是最常见的问题,表现是程序闪退或者卡在启动画面。排查思路如下。
第一步,看日志。Wine 和 FEX-Emu 都会输出日志,通常在应用的沙盒目录下的logs文件夹里。日志里会显示程序加载了哪些 DLL,执行了哪些指令,在哪里崩溃的。
第二步,检查依赖。很多 Windows 程序依赖特定的 DLL,比如msvcp140.dll、vcruntime140.dll。这些 DLL 需要手动放到 Wine 的system32目录下。可以用wine64自带的winedump工具查看程序的依赖列表。
第三步,检查指令集支持。有些程序用了 FEX-Emu 不支持的指令,比如 AVX-512 的某些扩展。这种情况只能等 FEX-Emu 更新,或者找替代程序。
第四步,检查内存。iOS 设备的内存有限,如果程序需要的内存超过设备可用内存,会被系统杀掉。可以在 Xcode 的 Devices 窗口里查看设备的内存使用情况。
5.2 图形显示异常的快速定位
图形显示异常包括黑屏、花屏、画面撕裂、文字模糊等。排查思路如下。
黑屏通常是 DXMT 没有正确初始化。检查dxmt.dll是否在 Wine 的system32目录下,检查 Metal 框架是否链接正确。如果日志里显示Failed to create Metal device,说明 Metal 初始化失败,可能是设备不支持或者权限问题。
花屏通常是着色器编译错误。开启DXMT_DEBUG后,日志里会显示哪个着色器编译失败了。这种情况可以试着更新 DXMT 到最新版本,或者手动修改着色器代码。
画面撕裂通常是帧率不匹配。调整DXMT_MAX_FRAMES_IN_FLIGHT参数,或者开启垂直同步。
文字模糊通常是分辨率缩放问题。在 Wine 的注册表里设置虚拟桌面分辨率,让程序在原生分辨率下渲染。
5.3 输入法与网络连接的配置要点
输入法问题在中文用户里很常见。Wine 默认不支持 iOS 的原生输入法,需要用一个桥接方案。社区里有人做了一个简单的输入法桥,通过剪贴板来传递文字。具体操作是:在 iOS 上复制文字,然后在 Wine 程序里粘贴。虽然麻烦,但能用。
网络连接方面,Wine 会使用 iOS 的网络栈,大部分情况下能直接联网。但有些程序会检查网络适配器的类型,如果发现不是 Windows 的网络适配器,会拒绝联网。这种情况可以在 Wine 的注册表里模拟一个 Windows 网络适配器。
注意:iOS 对后台网络活动有限制,如果 Wine 程序需要在后台保持网络连接,可能会被系统断开。建议在使用网络功能时保持应用在前台。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 程序闪退 | 缺少 DLL | 查看日志中的依赖加载记录 | 补充缺失的 DLL 到 system32 |
| 启动卡死 | JIT 编译超时 | 查看 FEX-Emu 日志 | 调大 FEX_JIT_TIMEOUT |
| 黑屏 | DXMT 初始化失败 | 查看 DXMT 日志 | 检查 Metal 链接和权限 |
| 花屏 | 着色器编译错误 | 开启 DXMT_DEBUG | 更新 DXMT 或修改着色器 |
| 文字乱码 | 字体缺失或编码错误 | 检查字体目录和注册表 | 安装字体并设置代码页 |
| 画面撕裂 | 帧率不匹配 | 观察帧率变化 | 调整 MAX_FRAMES_IN_FLIGHT |
| 无法联网 | 网络适配器检查 | 查看程序日志 | 注册表模拟 Windows 适配器 |
| 输入法无法使用 | 缺少桥接 | 测试剪贴板粘贴 | 使用剪贴板桥接方案 |
6. 个人实操心得与后续扩展方向
6.1 几个让我少走弯路的经验
第一个经验是:不要追求一次跑通所有程序。我一开始想一口气把常用的 Windows 工具都跑起来,结果每个都出问题,排查起来很乱。后来改成一次只跑一个程序,跑通了再跑下一个,效率反而高很多。
第二个经验是:日志是你的好朋友。Wine、FEX-Emu、DXMT 都有详细的日志输出,遇到问题先看日志,比瞎猜快得多。我习惯在启动程序时加上WINEDEBUG=+all和FEX_LOG_LEVEL=debug,虽然日志量大,但信息全。
第三个经验是:社区版本比官方版本更实用。Wine 官方对 iOS 的支持很有限,社区里有几个分支专门针对 iOS 做了优化,比如增加了对 Metal 的支持、修复了 JIT 权限问题。用这些分支能省很多事。
第四个经验是:性能调优要有取舍。想要帧率高,就得牺牲画质;想要画质好,就得接受帧率低。在 iOS 设备上,我通常优先保证流畅度,把画质调到中等,这样体验最好。
6.2 这套方案还能怎么扩展
Madeira 这套方案目前主要跑 Windows 程序,但它的架构其实可以扩展到其他场景。
一个方向是跑 Linux 程序。Wine 本身就是从 Linux 上发展起来的,FEX-Emu 也支持 Linux 的 ELF 格式。如果把 Wine 换成 Linux 的运行时,理论上可以在 iOS 上跑 Linux 的 x86-64 程序。社区里已经有人在尝试这个方向。
另一个方向是跑老游戏机模拟器。很多老游戏机的模拟器是 x86-64 的 Windows 程序,比如 PCSX2、Dolphin。用 Madeira 的方案,可以在 iOS 上跑这些模拟器,然后再跑游戏。虽然多了一层翻译,但性能还能接受。
还有一个方向是开发工具链。有些 Windows 上的开发工具,比如 Keil、IAR,只有 Windows 版本。用 Madeira 的方案,可以在 iPad 上跑这些工具,配合外接键盘和鼠标,能当个轻量级的开发终端用。
6.3 最后分享一个小技巧
如果你在 iOS 上跑 Wine 程序时遇到随机崩溃,可以试试关闭 iOS 的“后台应用刷新”和“自动更新”。这两个功能会在后台占用 CPU 和内存,导致 Wine 的 JIT 编译被中断。关闭后,稳定性会明显提升。另外,把设备调成“飞行模式”再打开 Wi-Fi,能减少一些系统服务的干扰,对性能也有帮助。
这个项目我还在持续折腾,后面如果遇到新的坑或者发现新的调优参数,我会继续更新。如果你也在玩这套方案,欢迎交流你的经验。