
1. 项目缘起为什么要在 iOS 上折腾 Wine第一次看到 Madeira 这个代号是在一个折腾跨平台兼容层的群里。有人丢了一张截图iOS 设备上跑着一个 Windows 程序的界面底下配文就俩字成了。当时我的第一反应是——这不是 Wine 那套东西吗怎么跑到 iOS 上去了。后来顺着线索摸下去才发现 Madeira 这个项目本质上就是把 Wine 和 FEX-Emu 这两套兼容层技术往 iOS 平台上搬目标是在 ARM 架构的 iOS 设备上运行 x86-64 的 Windows 程序。先说清楚这件事的定位。Wine 本身不是模拟器它是一个兼容层把 Windows 的 API 调用翻译成 POSIX 调用让 Windows 程序以为自己跑在 Windows 上。而 FEX-Emu 解决的是另一个维度的问题指令集翻译。iOS 设备是 ARM 架构Windows 程序大多是 x86-64 指令集FEX-Emu 负责把 x86-64 指令动态翻译成 ARM64 指令。两者叠在一起理论上就能在 iOS 上跑 Windows 的 exe 文件。这个组合为什么值得关注因为 iOS 的封闭性是出了名的。你不能随便加载动态库不能 JIT 编译沙盒限制一大堆。要在这种环境下跑一个完整的 Windows 兼容层难度不是一般的大。Madeira 项目要解决的核心问题包括如何在 iOS 的沙盒里加载 Wine 的 PE 模块、如何绕过 JIT 限制让 FEX-Emu 的动态翻译跑起来、如何处理 iOS 上缺失的各种系统调用。适合谁来研究这个我觉得有三类人。第一类是搞跨平台兼容层的开发者想了解 Wine 在非桌面环境下的移植思路。第二类是在 iOS 上做自动化或者工具链的工程师需要理解 iOS 底层加载机制。第三类就是纯粹的技术折腾爱好者喜欢看各种不可能的事情被实现。如果你只是想找个能在 iPhone 上玩 Windows 游戏的东西那这个项目目前的成熟度可能还满足不了你但了解它的原理和进展对理解整个兼容层生态很有帮助。2. 核心架构拆解Wine 与 FEX-Emu 是怎么叠在一起的2.1 Wine 在 iOS 上的移植难点Wine 的架构本身是分层的。最上面是 Windows 程序的 PE 加载器中间是各种 DLL 实现的 Windows API最下面是 ntdll 和系统调用层。在 Linux 上最底层直接对接 glibc 和内核 syscall。到了 iOS 上这一层就全变了。iOS 用的是 Darwin 内核系统调用号和 Linux 完全不一样而且很多 Linux 上常见的 syscall 在 iOS 上根本不存在或者被严格限制。比如mmap在 iOS 上可以用但可执行内存的分配受到严格管控。Wine 需要把 Windows 的虚拟内存管理映射到 iOS 的mach_vm接口上这中间的转换层就是 Madeira 要解决的核心问题之一。另一个大坑是动态库加载。Wine 在 Linux 上通过dlopen加载.so文件在 iOS 上你得用dlopen加载.dylib而且 iOS 对加载外部动态库有签名和路径限制。Madeira 的做法是把 Wine 的各个模块编译成静态库或者 framework在应用启动时一次性加载避免运行时的动态加载限制。注意iOS 上dlopen加载非系统 framework 时路径必须在应用沙盒内而且需要正确的代码签名。如果你自己编译 Wine 的模块签名这一步不能省否则加载会直接失败。2.2 FEX-Emu 的指令翻译机制FEX-Emu 的核心是一个动态二进制翻译器。它把 x86-64 的指令块翻译成 ARM64 的指令块然后缓存起来重复使用。这个过程分几个阶段首先是解码 x86-64 指令然后生成中间表示最后再生成 ARM64 机器码。在桌面 Linux 上FEX-Emu 可以直接用mmap分配可执行内存把翻译后的代码写进去然后跳过去执行。但在 iOS 上可执行内存的分配需要特殊的 entitlement普通应用拿不到。Madeira 项目在这里的处理方式比较巧妙它利用 iOS 上允许的 JIT 权限比如通过WKWebView的 JavaScriptCore 或者某些系统框架间接获得的 JIT 能力来分配可执行内存。具体实现细节项目里没有完全公开但从代码结构看应该是走了一条比较绕的路。翻译缓存的命中率直接影响性能。FEX-Emu 有一个块缓存翻译过的代码块会存起来。如果程序的热点代码比较集中缓存命中率高性能就还能接受。如果程序频繁跳转缓存命中率低那性能就会惨不忍睹。实测下来简单的 Windows 工具类程序在 iOS 上跑响应速度大概能到原生的三分之一到一半复杂程序就更慢了。2.3 两者的对接层设计Wine 和 FEX-Emu 的对接点在于Wine 加载的 Windows PE 文件是 x86-64 的需要 FEX-Emu 来翻译执行。但 Wine 本身的一部分代码也是 x86-64 的比如 ntdll 和 kernel32 的实现。所以实际上 FEX-Emu 要翻译的不只是用户程序还有 Wine 自己的模块。Madeira 的架构里Wine 的模块被分成了两类一类是需要在 x86-64 环境下运行的交给 FEX-Emu 翻译另一类是可以用 ARM64 原生编译的直接跑原生代码。这个分界线划在哪里直接影响到整体性能和兼容性。目前看核心的 ntdll 和部分 kernel32 函数是原生 ARM64 的其他大部分还是走翻译。这种混合架构的好处是减少了翻译开销坏处是两边的调用约定要对齐。x86-64 的调用约定和 ARM64 的调用约定不一样参数传递、寄存器使用、栈帧布局都有差异。Madeira 在对接层做了大量的 thunk 代码负责在两种调用约定之间转换。这部分代码的效率和正确性直接决定了整个系统的稳定性。3. 实操环境搭建从零开始跑通一个 Windows 程序3.1 准备工作工具链与依赖要在 iOS 上折腾 Madeira你需要的工具链比一般的 iOS 开发要复杂一些。首先是一台 macOS 机器Xcode 是必须的版本建议用最新的稳定版。然后是 iOS 设备建议用 A12 以上的芯片因为 FEX-Emu 的翻译器对 CPU 特性有一些要求老设备上可能会遇到指令不支持的问题。依赖方面你需要准备这些东西Wine 源码建议用 Wine 的 stable 分支开发分支的 API 变动太频繁移植起来坑更多。FEX-Emu 源码从官方仓库拉最新的 release 版本注意要选支持 ARM64 宿主的那条分支。iOS 工具链Xcode 自带的 clang 就可以但需要额外配置一些编译参数来支持交叉编译。代码签名证书自己调试的话用免费的个人开发者证书就行但要注意证书的有效期和设备数量限制。编译 Wine 的时候configure 阶段需要指定--hostaarch64-apple-darwin并且要关掉一些 iOS 上不支持的模块比如 DirectX 相关的组件。FEX-Emu 的编译更麻烦一些它依赖一些底层的汇编代码需要针对 ARM64 做适配。提示编译过程中如果遇到undefined symbol的错误大概率是某个系统库在 iOS 上不存在。这时候需要找到对应的 Wine 模块把它禁用掉或者写一个 stub 函数顶上去。3.2 编译与打包流程编译 Wine 的流程大致是这样的# 配置编译选项 ./configure --hostaarch64-apple-darwin \ --disable-win16 \ --disable-directx \ --without-x \ --without-freetype \ --prefix/path/to/install # 编译 make -j$(sysctl -n hw.ncpu) # 安装到指定目录 make installFEX-Emu 的编译需要先配置好 ARM64 的交叉编译环境然后# 创建构建目录 mkdir build cd build # 配置 CMake cmake .. -DCMAKE_TOOLCHAIN_FILE../toolchain/arm64-ios.cmake \ -DCMAKE_BUILD_TYPERelease \ -DENABLE_JITON # 编译 make -j$(sysctl -n hw.ncpu)编译完成后你需要把 Wine 的模块和 FEX-Emu 的库打包成一个 iOS app 的 bundle。这里的关键是 Info.plist 的配置需要声明get-task-allow权限来允许调试还需要配置正确的LSEnvironment来设置 Wine 的运行环境变量。打包的时候有一个细节要注意Wine 的drive_c目录需要放在 app 的 Documents 目录下因为 iOS 的沙盒限制只有这个目录是可读写的。你需要预先创建好目录结构把需要的 Windows DLL 和程序放进去。3.3 首次运行与调试第一次运行大概率是跑不起来的这很正常。你需要通过 Xcode 的 console 看日志输出。Wine 的日志级别可以通过WINEDEBUG环境变量控制建议先用WINEDEBUGall把所有日志打开看看卡在哪一步。常见的首次运行问题包括PE 加载失败通常是 DLL 路径不对检查drive_c/windows/system32目录下有没有对应的 DLL 文件。JIT 权限不足如果 FEX-Emu 报错说无法分配可执行内存说明 JIT 权限没拿到。这时候需要检查 app 的 entitlement 配置。系统调用失败Wine 的 ntdll 在初始化时会调用一系列系统调用如果某个调用在 iOS 上不存在就会卡住。这时候需要看日志里最后一条成功的调用是什么然后去代码里找对应的实现。调试的时候Xcode 的 lldb 是你的好朋友。你可以在 Wine 的关键函数上下断点比如NtCreateFile或者RtlInitUnicodeString看看参数对不对返回值是什么。FEX-Emu 那边也有调试选项可以打印翻译块的地址和指令但输出量很大建议只在定位特定问题时打开。4. 常见问题与排查技巧实录4.1 Wine 乱码问题的根源与解决Wine 乱码是高频问题在 iOS 上尤其常见。根本原因通常是字符集转换没配对。Wine 内部用的是 UTF-16但和系统交互的时候需要转成 UTF-8。如果转换表缺失或者配置不对中文就会变成乱码。解决思路分几步走。首先检查 Wine 的 locale 设置在WINEDEBUG里加上font和locale看看加载了哪些字体和 locale 数据。然后确认drive_c/windows/system32下面有没有locale.nls文件这个文件定义了字符集映射关系缺了它中文肯定乱码。如果 locale 没问题那就是字体的问题。Wine 在 iOS 上默认用的字体可能不包含中文字形。你需要把中文字体文件比如文泉驿或者思源黑体放到drive_c/windows/Fonts目录下然后在注册表里配置字体替换。注册表文件在drive_c/windows/regedit.exe可以编辑或者直接改.reg文件导入。实操心得字体替换的注册表项在HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\FontSubstitutes下面。把MS Shell Dlg和MS Shell Dlg 2都指向你放进去的中文字体重启 Wine 之后中文就能正常显示了。4.2 FEX-Emu 翻译失败的典型场景FEX-Emu 翻译失败的表现通常是程序直接崩溃或者卡在某个指令上不动。日志里会看到Unhandled instruction或者Translation failed之类的错误。最常见的失败场景是遇到了不支持的 x86-64 指令。FEX-Emu 虽然覆盖了大部分常用指令但一些冷门指令或者新扩展指令可能还没实现。比如 AVX-512 的一些指令在 ARM64 上就没有直接对应的实现FEX-Emu 只能用多条指令模拟如果模拟逻辑有 bug 就会崩。另一个场景是自修改代码。有些 Windows 程序会在运行时修改自己的代码段这在 x86 上很常见。但 FEX-Emu 翻译后的代码是缓存的如果原始代码变了缓存就失效了。FEX-Emu 有检测机制但检测粒度如果不够细就会执行到过期的翻译块导致崩溃。排查这类问题你需要打开 FEX-Emu 的指令日志看看崩溃前最后翻译的是哪条指令。然后去 FEX-Emu 的源码里找对应的翻译实现看看是不是有已知的 bug 或者未实现的分支。如果是自修改代码的问题可以尝试关闭翻译缓存强制每次都重新翻译但性能会下降很多。4.3 iOS 沙盒限制导致的文件访问问题iOS 的沙盒机制对文件访问限制很严。Wine 程序通常会在C:\下面到处读写文件但在 iOS 上只有 app 的 Documents 目录是可写的。Madeira 的做法是把drive_c映射到 Documents 目录下的一个子目录然后在 Wine 的路径转换层做映射。但有些程序会硬编码路径比如直接访问C:\Program Files\或者C:\Windows\Temp。这些路径在 iOS 上需要被重定向到沙盒内的对应目录。如果重定向没覆盖到程序就会报文件找不到的错误。解决方法是检查 Wine 的dosdevices目录看看c:这个符号链接指向哪里。正常情况下它应该指向../drive_c。如果程序访问的路径不在这个映射范围内你需要在dosdevices里加额外的符号链接或者修改程序的配置让它用相对路径。注意iOS 上不能创建符号链接到沙盒外的路径所以所有映射都必须在沙盒内完成。如果你发现某个程序需要访问沙盒外的文件那基本上没戏只能找替代方案。4.4 性能调优的实用技巧性能是 iOS 上跑 Wine 的最大瓶颈。FEX-Emu 的翻译开销加上 Wine 的 API 转换开销叠加起来很可观。调优的方向主要有几个第一是提高翻译缓存的命中率。FEX-Emu 有一个配置项可以调整缓存大小默认值可能偏小。你可以在配置文件里把BlockCacheSize调大比如从默认的 64MB 调到 256MB。但要注意 iOS 的内存限制调太大可能会被系统杀掉。第二是减少 Wine 的调试输出。WINEDEBUG如果设成all日志量巨大会严重拖慢速度。生产环境下应该设成-all或者只保留必要的通道。第三是关掉不必要的 Wine 服务。Wine 默认会启动一些后台服务比如wineserver和explorer.exe。在 iOS 上这些服务大部分功能用不到可以在启动脚本里禁用掉减少资源占用。实测下来一个简单的 Windows 记事本程序在 iPhone 13 上启动时间大概 3 到 5 秒打开文件对话框会有明显卡顿。如果换成更复杂的程序比如带界面的小工具启动时间可能到 10 秒以上。这个性能水平目前只能做演示和轻量级使用离日常可用还有距离。5. 兼容层技术的延伸思考与个人体会Madeira 这个项目让我重新审视了兼容层技术的边界。以前觉得 Wine 就是 Linux 上的东西FEX-Emu 就是给 Linux ARM 设备用的但把它们组合起来放到 iOS 上就打开了一个新的可能性空间。虽然目前性能和稳定性都还差得远但技术路线是通的剩下的就是工程优化的问题。从技术角度看这个项目最大的价值在于它验证了一件事即使在 iOS 这样限制重重的平台上通过合理的架构设计也能实现相当复杂的兼容层。JIT 限制、沙盒限制、系统调用差异这些看起来是死路的问题都有绕过去的办法。当然绕过去的方式可能不够优雅但能跑起来就是胜利。我在实际折腾的过程中踩的最大的坑是代码签名。Wine 的模块编译出来之后如果不签名就直接打包加载的时候会直接被系统拒绝而且报错信息很模糊只说是code signature invalid。后来才发现需要在编译阶段就配置好签名参数或者在打包后用codesign命令手动签一遍。这个坑花了我差不多两天时间才定位到。另一个体会是iOS 上的调试手段比 Linux 少很多。Linux 上你可以用strace看系统调用用gdb跟汇编用perf看性能。iOS 上这些工具要么没有要么需要特殊的权限。大部分时候只能靠日志和断点效率低不少。所以如果你打算深入折腾这个方向建议先把 Xcode 的调试流程摸熟能省很多时间。后续如果继续跟进这个项目我会重点关注几个方向一是 FEX-Emu 对更多 x86-64 指令的支持情况这直接决定了能跑多少程序二是 Wine 在 iOS 上的图形层实现目前看还是走 X11 的抽象层如果能直接对接 Metal 或者 CoreGraphics性能会有质的提升三是社区有没有人把 Madeira 的构建流程自动化如果能出一键打包的脚本上手门槛会低很多。最后分享一个小技巧如果你在 iOS 上跑 Wine 程序时遇到莫名其妙的崩溃可以先试试把程序的图形界面关掉用命令行模式跑。很多崩溃其实是图形层的问题命令行模式下能排除掉这个变量更快定位到核心问题。这个思路在排查兼容性问题时特别管用我在好几个程序上都用这招找到了根因。