ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

iOS上跑x86-64 Windows程序:FEX-Emu+Wine+DXMT三层翻译实战

2026/10/3 5:02:05 拓冰建站 浏览量
iOS上跑x86-64 Windows程序:FEX-Emu+Wine+DXMT三层翻译实战 1. 项目缘起为什么我要在 iOS 上折腾 x86-64 的 Windows 程序第一次看到 Madeira 这个代号是在一个折腾跨平台兼容层的群里。有人丢了一张截图一台 iPad 上跑着一个明显是 Windows 桌面时代的 x86-64 程序界面有点糊但确实在跑。底下有人问这是 Wine 吗回答是FEX-Emu Wine DXMT 的组合项目内部叫 Madeira。这个组合一下子勾起了我的兴趣。因为过去几年在 ARM 设备上跑 x86 程序这件事一直是个能做但不好用的领域。Android 上有 ExaGear 那一套Linux 上有 Box86/Box64而 iOS 因为系统封闭一直是块难啃的骨头。Madeira 这个项目本质上是把三件事拼在了一起FEX-Emu 负责把 x86-64 指令翻译成 ARM64 指令Wine 负责把 Windows 的 API 调用翻译成 POSIX 调用DXMT 负责把 Direct3D 翻译成 Metal。三层翻译叠在一起最终让一个 Windows 的 x86-64 程序能在 iOS 的 ARM64 芯片上跑起来。听起来很绕但拆开看逻辑是清晰的。iOS 设备用的是 Apple Silicon指令集是 ARM64。Windows 程序编译出来是 x86-64。这两者之间隔着一道指令集的墙。FEX-Emu 就是拆墙的它做的是动态二进制翻译——程序运行的时候一条一条地把 x86-64 指令翻译成 ARM64 指令再执行。Wine 则是在系统调用层面做翻译Windows 程序调用CreateWindow、MessageBox这些 APIWine 把它们映射到宿主系统的对应实现上。DXMT 更专一它盯着 Direct3D 的调用把它们转成 Metal 的调用因为 iOS 上图形 API 只有 Metal 这一条路。这套东西适合谁我觉得有三类人值得关注。第一类是跨平台兼容层的研究者想搞清楚多层翻译的架构怎么搭。第二类是想在移动设备上跑老 Windows 程序的玩家比如某些没有移动版的老工具、老游戏。第三类是iOS 开发者想了解在封闭系统上做兼容层会遇到哪些坑。如果你只是想找个能跑 Windows 程序的 App那这个项目现阶段还不适合你它的价值更多在技术探索层面。我花了大概两周时间把 Madeira 这套组合在自己的设备上跑通中间踩的坑比想象中多。下面我把整个思路、关键细节、实操过程和排查经验完整梳理一遍尽量让有基础的人能照着复现让没基础的人也能看懂每一层在干什么。2. 整体架构拆解三层翻译是怎么叠起来的2.1 为什么是 FEX-Emu 而不是别的翻译器在 ARM 上跑 x86 程序翻译器的选择其实不少。QEMU 是最老牌的功能全但性能一般而且它的用户态模拟在移动设备上开销偏大。Box64 在 Linux 上表现很好但它对 iOS 的支持一直不完整因为 iOS 不允许 JIT即时编译之外的一些内存操作方式。FEX-Emu 的优势在于它从一开始就是为 ARM64 设计的而且对 JIT 的依赖方式更符合 iOS 的限制。这里要解释一个关键概念动态二进制翻译有两种模式一种是解释执行一种是 JIT 编译。解释执行就是读一条 x86 指令翻译一条执行一条慢但简单。JIT 是把一段 x86 代码整体翻译成 ARM64 代码块缓存起来下次直接执行翻译后的代码快但需要可执行内存权限。iOS 对可执行内存管得很严普通 App 拿不到mmap带PROT_EXEC的权限除非用MAP_JIT标志。FEX-Emu 在设计上就考虑了这个它的 JIT 后端能适配 iOS 的 JIT 限制这是它能跑起来的前提。提示iOS 上开 JIT 权限通常需要配合调试模式或者特定的签名方式。普通 App Store 分发的应用是拿不到这个权限的这也是为什么这类项目大多停留在开发者自签或者越狱环境。FEX-Emu 还有一个好处是它对 x86-64 指令集的覆盖比较全包括 SSE、AVX 这些 SIMD 指令。很多老程序依赖这些指令做浮点运算和图形处理如果翻译器不支持程序直接就崩了。我在测试一个老版本的图像处理工具时就遇到过 AVX2 指令不被支持导致崩溃的情况换成 FEX-Emu 的较新版本后问题消失。2.2 Wine 在中间扮演的角色Wine 这个名字是 Wine Is Not an Emulator 的递归缩写它不做指令翻译做的是API 翻译。Windows 程序运行时会调用大量的系统 DLL比如kernel32.dll、user32.dll、gdi32.dll。Wine 提供了这些 DLL 的开源实现把里面的函数调用映射到宿主系统的对应功能上。在 Madeira 这套组合里Wine 跑在 FEX-Emu 之上。也就是说Wine 本身也是被翻译执行的 x86-64 代码。这就有个有意思的地方Wine 的 DLL 是 x86-64 的Windows 程序也是 x86-64 的它们之间的调用不需要跨架构FEX-Emu 只需要翻译这一整坨 x86-64 代码。如果 Wine 是 ARM64 原生编译的那 Windows 程序调用 Wine 的 DLL 时就要跨架构反而更麻烦。所以这里选择让 Wine 也走翻译层是合理的。Wine 的配置是个细活。默认的 Wine 前缀prefix里有一堆 DLL 覆盖设置哪些用原生、哪些用内置直接影响程序能不能跑。比如mscoree.dll如果设成原生程序会去找 .NET Framework而 iOS 上根本没有直接报错。设成内置Wine 会用 Mono 来顶替虽然不完美但至少能跑。我在配置一个需要 .NET 的老工具时就是靠把mscoree设成内置才跑起来的。2.3 DXMT 为什么是图形翻译的关键Direct3D 是 Windows 上游戏和图形程序的主力 API。在 iOS 上图形 API 只有 Metal。DXMT 的作用就是把 D3D 的调用翻译成 Metal 的调用。它支持 D3D 11 和部分 D3D 12 的特性通过把着色器shader从 DXBC/DXIL 转换成 Metal Shading Language 来实现。这里的技术难点在于着色器转换。D3D 的着色器是编译成字节码的Metal 的着色器是另一种语言。DXMT 需要把字节码反编译、优化、再编译成 Metal 能吃的格式。这个过程如果出错表现就是画面花屏、黑屏或者直接崩溃。我在跑一个 D3D 11 的小游戏时就遇到过着色器编译失败导致画面全黑的情况后来查日志发现是某个纹理格式不被支持换了个 DXMT 的版本才解决。DXMT 和 Wine 的配合方式是Wine 提供d3d11.dll的接口DXMT 实现这个接口内部再调 Metal。所以配置的时候要把 Wine 的d3d11设成原生让它加载 DXMT 的实现而不是 Wine 自带的那个基于 OpenGL 的版本。这一点如果搞错程序要么跑不起来要么性能差得离谱。2.4 三层翻译的性能账怎么算三层翻译叠在一起性能损失是必然的。我实测下来一个简单的 x86-64 程序经过 FEX-Emu 翻译后性能大概是原生的 40% 到 60%。如果再叠加 Wine 的 API 翻译开销可能降到 30% 到 50%。图形部分如果走 DXMT还要再打折扣具体取决于着色器转换的复杂度。但这个账不能只看绝对性能。对于很多老程序来说它们本来就不需要很高的性能。一个 2005 年的文本编辑器在现在的 ARM 芯片上就算打三折也比你当年的奔腾 4 快。真正吃性能的是游戏和图形密集型应用这类程序在 Madeira 上跑体验就取决于具体场景了。2D 游戏一般没问题3D 游戏就要看 DXMT 的转换效率。注意不要指望在 iOS 上跑最新的 3A 大作。这套组合的目标是让老程序能跑而不是让新程序跑得快。心态摆正体验会好很多。3. 核心细节解析每一层的配置要点和坑3.1 FEX-Emu 的 rootfs 和配置FEX-Emu 在 iOS 上跑需要一个 rootfs根文件系统里面放着 x86-64 的库和 Wine 的二进制。这个 rootfs 通常是一个 squashfs 镜像或者一个目录里面包含了 Ubuntu 或者 Debian 的 x86-64 用户态环境。为什么需要这个因为 Wine 依赖大量的系统库比如libc、libX11、libfreetype这些库必须是 x86-64 的才能被 FEX-Emu 翻译执行。rootfs 的构建是个体力活。我用的方法是在一台 x86-64 的 Linux 机器上用debootstrap搭一个最小化的 Debian 环境然后往里装 Wine 和相关的依赖。装完之后打包成镜像传到 iOS 设备上。这个过程要注意库的版本匹配Wine 对libc的版本有要求太新太旧都可能出问题。FEX-Emu 的配置文件里有几个参数值得关注。RootFS指向 rootfs 的路径ThunkHostLibs和ThunkGuestLibs控制宿主库和客户库的映射。如果程序需要调用宿主系统的某些功能比如音频输出就要通过 thunk 机制来桥接。我一开始没配 thunk结果程序跑起来没声音查了半天才发现是音频库没映射过去。3.2 Wine 前缀的初始化与 DLL 覆盖Wine 的前缀prefix是它模拟的 Windows 环境里面有一个虚拟的 C 盘还有注册表文件。初始化前缀用wineboot命令它会创建目录结构和默认的注册表。在 Madeira 里这个命令要通过 FEX-Emu 来执行因为wineboot本身是 x86-64 的。DLL 覆盖是 Wine 配置里最容易出错的地方。Wine 自带了很多 DLL 的实现但有些程序需要原生的 DLL 才能跑。比如某些程序依赖msvcp140.dllWine 自带的版本可能不完整就需要把原生的 DLL 拷到前缀的system32目录里然后在winecfg里把对应的库设成原生。反过来像kernel32、user32这些核心库必须用 Wine 内置的设成原生会直接崩。我整理了一个常用 DLL 的覆盖建议表供参考DLL 名称建议设置原因kernel32内置核心系统库原生版本依赖 Windows 内核user32内置窗口管理原生版本无法在 iOS 上工作d3d11原生需要加载 DXMT 的实现dxgi原生配合 DXMT 使用mscoree内置用 Mono 顶替 .NET Frameworkmsvcp140视情况程序自带则用原生否则用内置vcruntime140视情况同上这个表不是绝对的具体还要看程序的需求。我的经验是先全用内置跑一遍看报什么错再针对性地换原生。3.3 DXMT 的安装与着色器缓存DXMT 的安装相对简单把编译好的d3d11.dll、dxgi.dll放到 Wine 前缀的system32目录然后在winecfg里把d3d11和dxgi设成原生就行。但着色器缓存是个容易被忽略的点。DXMT 在第一次遇到某个着色器时会做转换这个过程比较慢。转换后的结果会缓存起来下次直接用。如果缓存目录没有写权限每次都要重新转换体验会很差。缓存目录一般在~/Library/Caches下面具体路径取决于 DXMT 的版本。我建议在配置的时候显式指定缓存路径并确保这个路径可写。另外如果程序更新了着色器缓存可能会失效需要手动清理。我遇到过一次画面异常清理缓存后恢复正常所以养成定期清理的习惯没坏处。3.4 iOS 侧的限制与绕行方案iOS 对这类项目的限制主要有三个JIT 权限、文件系统访问、后台运行。JIT 权限前面提过需要特殊签名。文件系统访问方面iOS 的沙盒机制让程序只能访问自己的容器目录Wine 前缀必须放在这个目录里。后台运行方面iOS 会随时挂起后台应用如果程序在后台需要继续跑就要申请后台任务权限但时间有限。绕行方案主要是靠开发者签名或者企业签名拿到更多的权限。另外有些项目会用altstore或者sideloadly这类工具来侧载配合开发者模式能拿到 JIT 权限。这部分操作涉及具体的工具链不同版本差异较大建议参考项目的最新文档。提示iOS 的开发者模式需要在设置里手动开启而且设备重启后会重置。如果发现 JIT 突然不工作了先检查开发者模式是不是关了。4. 实操过程从零跑通一个 Windows 程序4.1 环境准备与依赖安装我用的环境是一台 iPad ProM1 芯片系统版本是 iPadOS 16。首先要在设备上装一个能跑 FEX-Emu 的宿主 App这个 App 通常由项目方提供或者自己用 Xcode 编译。编译的时候要注意签名配置用免费的开发者账号签名有效期只有 7 天到期要重新签。用付费账号可以延长到一年。宿主 App 装好后把 rootfs 镜像和 FEX-Emu 的配置文件传进去。传输方式可以用iTunes的文件共享或者用scp如果设备开了 SSH。我用的方法是把文件打包成一个 zip通过文件 App 拷进去再在宿主 App 里解压。依赖方面rootfs 里需要装这些包wine、wine32如果需要 32 位支持、winetricks、cabextract。winetricks是个脚本工具能帮你装一些常用的 Windows 组件比如vcrun2019、dotnet48。不过winetricks在 FEX-Emu 环境下跑有时候会因为网络问题失败建议提前把需要的组件下载好。4.2 rootfs 构建的详细步骤构建 rootfs 我是在一台 Ubuntu 22.04 的 x86-64 机器上做的。步骤如下# 安装 debootstrap sudo apt install debootstrap # 创建一个最小化的 Debian 环境 sudo debootstrap --archamd64 bullseye ./rootfs http://deb.debian.org/debian/ # chroot 进去 sudo chroot ./rootfs # 配置 apt 源安装 Wine apt update apt install wine wine64 winetricks cabextract # 退出 chroot exit # 打包成 squashfs 镜像 sudo mksquashfs ./rootfs rootfs.squashfs -comp zstd这里用bullseye而不是更新的版本是因为 Wine 在某些新版本上会有兼容性问题。zstd压缩是为了减小镜像体积同时解压速度快。打包好的镜像大概 1.5GB 左右传到 iPad 上需要一点时间。4.3 Wine 前缀初始化与程序安装在 iOS 设备上通过宿主 App 的终端界面执行以下命令# 设置环境变量 export FEX_ROOTFS/path/to/rootfs export WINEPREFIX/path/to/prefix # 初始化 Wine 前缀 FEXBash -c wineboot -u # 安装程序 FEXBash -c wine /path/to/setup.exeFEXBash是 FEX-Emu 提供的一个包装脚本它会在 FEX-Emu 的环境里执行后面的命令。wineboot -u会创建前缀目录和注册表。安装程序的时候如果安装界面出不来可能是图形驱动没配好检查 DXMT 是否加载。我装的是一个老版本的音频编辑工具安装过程很顺利但启动的时候报错说找不到d3d9.dll。这个程序用的是 D3D9而 DXMT 主要支持 D3D11。解决办法是用 Wine 自带的d3d9实现它基于 OpenGL虽然性能一般但能用。在winecfg里把d3d9设成内置问题解决。4.4 图形与音频的调试图形调试主要看日志。DXMT 会输出转换日志如果某个着色器转换失败日志里会有明确的错误信息。我遇到过一次画面闪烁的问题日志显示是某个纹理格式不支持后来在 DXMT 的配置里加了一个格式映射才解决。音频方面Wine 默认用winealsa或者winepulse在 iOS 上这两个都不直接可用。需要配置 thunk把音频调用桥接到 iOS 的 CoreAudio。FEX-Emu 的 thunk 配置里有一个libasound的映射把它指向宿主系统的音频库就行。我配好之后声音正常输出延迟也在可接受范围内。注意音频 thunk 配置错误会导致程序启动时卡死因为它在等音频设备初始化。如果程序卡在启动画面先检查音频配置。4.5 性能调优的几个手段性能调优我试过几个手段。第一是调整 FEX-Emu 的 JIT 缓存大小默认值偏小跑大程序容易频繁重编译。在配置里把JITCacheSize调大能减少重编译开销。第二是关闭不必要的 Wine 调试输出WINEDEBUG-all能减少日志开销。第三是给 DXMT 开着色器预编译虽然启动慢一点但运行更流畅。还有一个手段是用taskset把进程绑到性能核上。iOS 的芯片是大小核架构FEX-Emu 的翻译线程如果跑到小核上性能会明显下降。不过 iOS 上能不能用taskset取决于系统权限我这边测试是可以的但不确定所有版本都支持。5. 常见问题与排查技巧实录5.1 程序启动就崩没有任何提示这是最常见的问题原因通常有三个DLL 缺失、指令集不支持、内存权限不足。排查方法是开WINEDEBUGloaddll看加载了哪些 DLL哪个加载失败。如果是指令集问题FEX-Emu 的日志里会有 unsupported instruction 的字样。内存权限问题一般表现为SIGSEGV需要检查 JIT 权限是否正常。我遇到过一次启动即崩查了半天发现是 rootfs 里的libc版本和 Wine 不匹配。Wine 编译时链接的libc版本比 rootfs 里的新导致符号找不到。解决办法是重新构建 rootfs用匹配的libc版本。5.2 界面乱码或字体显示异常Wine 的字体问题是个老话题。默认情况下Wine 会用宿主系统的字体但 iOS 的字体路径和 Linux 不一样Wine 找不到就会显示方块或者乱码。解决办法是把字体文件拷到 Wine 前缀的drive_c/windows/Fonts目录然后在注册表里设置字体替换。我一般会拷几个常用的字体进去simsun.ttc宋体、msyh.ttc微软雅黑、arial.ttf。然后在winecfg的字体选项卡里把这些字体设为默认。这样大部分程序的界面都能正常显示。5.3 图形程序黑屏或花屏黑屏通常是着色器转换失败花屏通常是纹理格式不支持。排查方法是看 DXMT 的日志找到失败的着色器或纹理格式。如果是着色器问题可以尝试换一个 DXMT 版本或者用dxvk替代如果项目支持。如果是纹理格式问题可以在 DXMT 的配置里加格式映射。我遇到过一次花屏日志显示是DXGI_FORMAT_BC7不支持。BC7 是一种压缩纹理格式Metal 对它的支持有限。解决办法是在 DXMT 配置里把 BC7 映射到 BC3虽然画质有损失但至少能看。5.4 音频无声或爆音无声通常是 thunk 没配好爆音通常是缓冲区大小不合适。检查 thunk 配置里libasound的映射路径是否正确。缓冲区大小可以在 Wine 的音频设置里调默认值偏小调大一点能减少爆音。我遇到过一次爆音调大缓冲区后解决。但缓冲区太大又会导致延迟所以要在延迟和音质之间找个平衡。我的经验是设成 1024 个采样点大部分场景够用。5.5 常见问题速查表问题现象可能原因排查方法解决方案启动即崩DLL 缺失WINEDEBUGloaddll补全缺失的 DLL启动即崩指令集不支持看 FEX-Emu 日志换 FEX-Emu 版本界面乱码字体缺失看 Wine 日志拷贝字体并设置替换黑屏着色器转换失败看 DXMT 日志换 DXMT 版本花屏纹理格式不支持看 DXMT 日志加格式映射无声thunk 未配置检查 thunk 配置配置音频 thunk爆音缓冲区过小听感判断调大缓冲区卡顿JIT 缓存过小看 FEX-Emu 日志调大 JITCacheSize5.6 几个独家避坑技巧第一个技巧是先用最小化程序测试。不要一上来就跑大程序先用notepad.exe这种简单程序验证环境是否正常。如果notepad都跑不起来那肯定是环境问题不用往下折腾。第二个技巧是保留多个 rootfs 版本。不同程序对库版本的要求不一样保留几个不同版本的 rootfs遇到兼容性问题可以快速切换。我一般会保留一个 Debian 11 的和一个 Debian 12 的。第三个技巧是日志分级。Wine 和 FEX-Emu 的日志都很啰嗦全开的话输出量巨大。我一般先用WINEDEBUG-all跑一遍看能不能起来起不来再逐步开日志。FEX-Emu 的日志可以用FEX_LOG_LEVEL控制设成warn只输出警告和错误。第四个技巧是注意文件路径的大小写。iOS 的文件系统默认是大小写敏感的而 Windows 程序经常不区分大小写。如果程序访问一个文件报 file not found但文件明明存在很可能是大小写问题。解决办法是在 Wine 的配置里开启大小写不敏感模式或者手动把文件名改成程序期望的大小写。6. 这套组合还能怎么扩展跑通基础功能之后我试了一些扩展玩法。一个是多程序共存在同一个前缀里装多个程序共享 DLL 和注册表。这样做的好处是省空间坏处是程序之间可能冲突。我的做法是给每个程序单独建前缀虽然占空间但隔离性好出问题好排查。另一个扩展是接外设。iOS 支持蓝牙键鼠和手柄Wine 程序可以通过 thunk 拿到这些输入事件。我试过用蓝牙手柄玩一个老游戏延迟可以接受但按键映射需要手动配。这部分配置在 Wine 的注册表里具体键值可以参考 Wine 的文档。还有一个方向是性能分析。FEX-Emu 有内置的性能计数器能看到翻译开销、JIT 命中率这些指标。DXMT 也有类似的统计。把这些数据收集起来能帮你找到性能瓶颈。我分析过一次发现瓶颈在着色器转换上后来通过预编译缓存解决了。最后说个我个人的体会这套东西的乐趣不在于它能跑多少程序而在于每一层翻译背后的设计取舍。FEX-Emu 怎么处理自修改代码Wine 怎么模拟 Windows 的窗口消息循环DXMT 怎么把 D3D 的状态机映射到 Metal 的渲染管线这些细节比能跑起来本身更有意思。如果你也是喜欢刨根问底的人建议从 FEX-Emu 的源码入手它的文档写得还算清楚配合调试日志看能学到不少东西。至于后续还能怎么玩我打算试试把 Vulkan 也接进来用 MoltenVK 做后端这样一些用 Vulkan 的程序也能跑。不过这是另一个大坑了等踩完再分享。