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 阶段需要指定--host=aarch64-apple-darwin,并且要关掉一些 iOS 上不支持的模块,比如 DirectX 相关的组件。FEX-Emu 的编译更麻烦一些,它依赖一些底层的汇编代码,需要针对 ARM64 做适配。
提示:编译过程中如果遇到
undefined symbol的错误,大概率是某个系统库在 iOS 上不存在。这时候需要找到对应的 Wine 模块,把它禁用掉,或者写一个 stub 函数顶上去。
3.2 编译与打包流程
编译 Wine 的流程大致是这样的:
# 配置编译选项 ./configure --host=aarch64-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_TYPE=Release \ -DENABLE_JIT=ON # 编译 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环境变量控制,建议先用WINEDEBUG=+all把所有日志打开,看看卡在哪一步。
常见的首次运行问题包括:
- 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 程序时遇到莫名其妙的崩溃,可以先试试把程序的图形界面关掉,用命令行模式跑。很多崩溃其实是图形层的问题,命令行模式下能排除掉这个变量,更快定位到核心问题。这个思路在排查兼容性问题时特别管用,我在好几个程序上都用这招找到了根因。