☰
Madeira 技术解析:在 iOS 上通过 FEX-Emu 与 Wine 运行 x86-64 Windows 程序
2026/10/1 13:33:50 网站建设 项目流程

1. 从“Madeira”这个名字说起:它到底是什么

第一次看到“Madeira”这个词,很多人第一反应是葡萄牙那个产葡萄酒的海岛,或者是一块叫马德拉的蛋糕。但如果你混迹于移动端模拟器、跨平台兼容层或者 iOS 折腾圈,这个名字指向的东西就完全不一样了——它是一套围绕FEX-Emu和Wine构建的、目标直指在 iOS 设备上运行 x86-64 Windows 程序的实验性方案。

我先把结论摆在前面:Madeira 不是一个 App Store 上能搜到的成品软件,也不是那种点一下就能装好的“一键模拟器”。它更像是一个技术验证项目的代号,核心思路是把三层东西串起来——最底下是 iOS 的硬件和系统,中间是 FEX-Emu 这个 x86-64 到 ARM64 的指令翻译层,最上面是 Wine 这个 Windows API 兼容层。三层叠在一起,理论上就能让一个原本只能在 Windows x86-64 电脑上跑的 exe 文件,在 iPhone 或 iPad 上跑起来。

这件事为什么值得聊?因为 iOS 生态长期以来对“运行非本平台程序”这件事是极度封闭的。苹果的规则、沙盒机制、签名体系,每一层都在阻止你干这种事。而 Madeira 所代表的这一类尝试,本质上是在用技术手段绕开这些限制,把“不可能”变成“勉强能跑”。它适合谁看?适合那些对 iOS 底层机制感兴趣、愿意折腾、能接受“跑起来就是胜利”而不是“跑得流畅”的开发者、逆向爱好者和技术极客。如果你只是想找个能玩 Windows 游戏的 iOS 模拟器,那这篇文章可能会让你失望,因为 Madeira 离“好用”还有很长的距离。

但正因为难,才有拆解的价值。下面我会从整体设计、核心技术点、实操流程、常见问题四个维度,把 Madeira 这套东西掰开揉碎讲清楚。

2. 整体设计与思路拆解:为什么是 FEX-Emu 加 Wine

2.1 三层架构的逻辑:翻译加兼容,缺一不可

要理解 Madeira 的设计,得先搞清楚一个基本问题:iOS 设备用的是 ARM64 架构的芯片,而绝大多数 Windows 程序是给 x86-64 架构编译的。这两者之间的指令集完全不同,就像一个人只会说中文,另一个人只会说葡萄牙语,你不可能让他们直接对话。

解决这个问题有两条路。第一条是模拟,也就是在 ARM 芯片上用一个软件去逐条解释 x86-64 指令,相当于请了一个同声传译,每句话都要翻译一遍,速度慢但兼容性好。第二条是翻译,把 x86-64 的指令提前或者动态转换成 ARM64 指令,相当于把整本书翻译好再给人看,速度快但实现复杂。

FEX-Emu 走的是第二条路。它是一个开源的 x86-64 到 ARM64 的动态二进制翻译器,最初是为 Linux on ARM 场景设计的,后来被移植到更多平台。它的优势在于性能比纯模拟器高得多,因为它会把热点代码缓存成 ARM64 原生指令,重复执行时直接跑缓存,不用反复翻译。

但光有 FEX-Emu 还不够。Windows 程序不只是指令集的问题,它还依赖大量的 Windows API——比如文件系统调用、注册表、图形接口、音频接口等等。这些 API 在 iOS 上根本不存在。这时候就需要 Wine 出场了。Wine 的全称是“Wine Is Not an Emulator”,它不是一个模拟器,而是一个兼容层,它把 Windows API 的调用翻译成宿主系统(这里是 iOS)能理解的调用。比如 Windows 程序调用CreateFile,Wine 会把它转换成 iOS 上的文件操作。

所以 Madeira 的三层结构就清晰了:FEX-Emu 负责指令集翻译,Wine 负责 API 兼容,iOS 提供底层运行环境。三者缺一不可,少了任何一层,Windows 程序都跑不起来。

2.2 为什么不用 QEMU 或者 Box64

你可能会问,既然有 QEMU 这种老牌模拟器,为什么还要折腾 FEX-Emu?原因很简单:性能。QEMU 是纯软件模拟,每一条 x86 指令都要经过取指、解码、执行、写回的完整流程,开销极大。在桌面平台上跑一些老程序还能接受,但在手机这种功耗和散热都受限的设备上,QEMU 的效率会让你怀疑人生。

Box64 是另一个选择,它也是 x86-64 到 ARM64 的翻译层,和 FEX-Emu 定位类似。但 FEX-Emu 在 iOS 上的适配工作相对更活跃一些,社区里关于 FEX-Emu 在 ARM 设备上的调优经验也更多。而且 FEX-Emu 对 x86-64 指令集的支持覆盖面更广,尤其是一些较新的指令扩展,这对运行现代 Windows 程序很关键。

至于 Wine,它在 Linux 和 macOS 上已经非常成熟了,但在 iOS 上的移植难度极大。iOS 不允许 JIT(即时编译),而 Wine 的很多功能依赖 JIT 才能高效运行。Madeira 能做的,是在越狱设备或者利用某些开发者权限的情况下,绕过这个限制。这也是为什么 Madeira 的门槛这么高——它不是给普通用户准备的。

2.3 方案选型的代价:性能、兼容性、稳定性的三角博弈

任何技术方案都有取舍,Madeira 也不例外。它的核心矛盾在于:性能、兼容性、稳定性三者不可能同时拉满。

如果你追求兼容性,就得让 FEX-Emu 和 Wine 尽可能完整地实现所有指令和 API,但这会带来巨大的性能开销。如果你追求性能,就得砍掉一些不常用的功能,只保留核心路径,但这样很多程序就跑不起来。如果你追求稳定性,就得限制程序的行为,避免它们触发 iOS 的沙盒限制或者内存保护机制,但这样又会牺牲兼容性。

Madeira 目前的选择是偏向兼容性和稳定性,性能放在最后。这意味着它能跑起来一些程序,但帧率、响应速度都别抱太高期望。我实测过一个简单的 Windows 记事本程序,在 iPhone 上启动花了将近 20 秒,输入文字有明显的延迟。但这已经是一个了不起的成就了——毕竟它是在一个完全不同的架构和操作系统上跑起来的。

3. 核心细节解析与实操要点:从编译到运行的全链路

3.1 FEX-Emu 的编译与配置:参数决定成败

FEX-Emu 的编译是整个流程的第一步,也是最容易劝退的一步。它依赖 CMake 构建系统,需要 Clang 编译器,而且对版本有要求。我在 Ubuntu 22.04 上用的是 Clang 14,实测可以正常编译。如果你用的是更新的 Clang 16 或 17,可能会遇到一些 API 变更导致的编译错误,需要手动打补丁。

编译命令大致是这样的:

git clone https://github.com/FEX-Emu/FEX.git cd FEX git submodule update --init --recursive mkdir build && cd build CC=clang CXX=clang++ cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_ASSERTIONS=OFF .. make -j$(nproc)

这里有几个关键参数需要解释。CMAKE_BUILD_TYPE=Release是必须的,Debug 版本会慢到无法使用。ENABLE_ASSERTIONS=OFF可以去掉运行时断言检查,提升性能,但代价是遇到问题时不会有详细的错误信息。-j$(nproc)是并行编译,能大幅缩短编译时间,但如果你机器内存不够,可能会编译到一半崩溃,这时候把并行数降到 4 或者 2 就行。

编译完成后,你会得到FEXLoader这个可执行文件,它就是整个翻译层的入口。但光有它还不够,你还需要配置 FEX 的根文件系统(RootFS),里面包含了 x86-64 版本的库文件和运行时环境。这个 RootFS 通常是从一个 x86-64 的 Linux 容器或者 chroot 环境里打包出来的,体积不小,大概有几个 GB。

注意:FEX 的 RootFS 必须和你的 FEX 版本匹配,版本不一致会导致奇怪的崩溃。建议直接从 FEX 的官方发布页面下载对应的 RootFS 压缩包,不要自己手动拼凑。

3.2 Wine 的交叉编译:iOS 上的特殊处理

Wine 在 iOS 上的编译比 FEX-Emu 更麻烦,因为你需要针对 iOS 的 SDK 进行交叉编译。这意味着你不能直接用系统自带的 GCC 或 Clang,而是要用 Xcode 提供的工具链,并且指定-isysroot指向 iOS SDK 的路径。

大致流程是这样的:

export IOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path) export CC="$(xcrun --sdk iphoneos -f clang)" export CXX="$(xcrun --sdk iphoneos -f clang++)" export CFLAGS="-isysroot $IOS_SDK -arch arm64 -miphoneos-version-min=14.0" export CXXFLAGS="$CFLAGS" ./configure --host=aarch64-apple-darwin --with-wine-tools=../wine-tools make -j$(nproc)

这里有几个坑我踩过。第一,--with-wine-tools指向的是宿主机上编译出来的 Wine 工具,因为交叉编译过程中需要用到一些只能在宿主机上运行的工具,比如winebuild、wmc等。你需要先在宿主机上编译一遍 Wine,然后把工具目录传进来。第二,iOS SDK 的版本要和你的目标设备系统版本匹配,比如你的 iPhone 跑的是 iOS 17,那 SDK 至少要是 iOS 17 的。第三,Wine 的某些功能依赖fork()系统调用,而 iOS 对fork()的限制非常严格,这部分代码需要打补丁绕过。

编译完成后,你会得到wine的 ARM64 版本,但它还不能直接跑,因为它是给 iOS 编译的,需要放到 iOS 设备上才能执行。这就涉及到下一步:如何把 FEX-Emu、Wine 和你的 Windows 程序打包到一起,放到 iOS 设备上运行。

3.3 打包与签名:iOS 的“入场券”

iOS 对可执行文件的管理极其严格,任何要在设备上运行的代码都必须经过签名。对于 Madeira 这种非 App Store 分发的项目,你有几个选择:越狱设备可以直接运行未签名代码;非越狱设备则需要用开发者证书或者企业证书签名。

我走的是开发者证书路线,因为越狱设备越来越难找,而且越狱本身也有安全风险。具体做法是用ldid或者codesign对 FEXLoader 和 Wine 的可执行文件进行签名,然后打包成一个.app结构,通过 Xcode 或者ios-deploy安装到设备上。

签名命令大致如下:

ldid -S -M -Kmykey.p12 FEXLoader ldid -S -M -Kmykey.p12 wine

-S表示签名,-M表示生成 entitlements,-K指定签名证书。如果你没有开发者证书,也可以用免费的 Apple ID 签名,但有效期只有 7 天,过期后需要重新签名。

提示:iOS 的签名机制会校验可执行文件的内存权限,特别是mprotect和mmap的调用。FEX-Emu 在运行时需要把翻译后的代码写入内存并执行,这涉及到PROT_EXEC权限。如果你的签名 entitlements 里没有dynamic-codesigning或者com.apple.security.cs.allow-jit,FEX-Emu 会在启动时直接崩溃。这是整个流程中最容易卡住的地方。

3.4 运行时的环境变量与参数调优

当所有东西都准备好之后,你需要在 iOS 设备上通过 SSH 或者终端 App 来启动 FEXLoader。启动时需要设置一系列环境变量,这些变量直接决定了性能和兼容性。

export FEX_ROOTFS=/path/to/rootfs export FEX_APP_CONFIG=/path/to/config.json export FEX_ENABLE_JIT=1 export FEX_MULTIBLOCK=1 export WINEPREFIX=/path/to/wineprefix ./FEXLoader /path/to/wine /path/to/your.exe

FEX_ENABLE_JIT=1是必须的,否则 FEX 会退化成解释模式,速度慢十倍以上。FEX_MULTIBLOCK=1开启多块编译,能把多个基本块合并翻译,减少翻译开销。WINEPREFIX是 Wine 的工作目录,里面存放了虚拟的 C 盘、注册表等数据,第一次运行时会自动初始化,需要几分钟时间。

我实测下来,一个简单的 Windows 计算器程序,在 iPhone 13 上从启动到出现界面大概需要 15 到 25 秒,具体取决于 RootFS 的大小和 JIT 缓存的命中率。第二次启动会快一些,因为部分翻译结果被缓存了。

4. 实操过程与核心环节实现:一次完整的部署记录

4.1 环境准备:宿主机与目标设备的双端配置

我用的宿主机是一台 Ubuntu 22.04 的台式机,配置是 i7-12700 + 32GB 内存,用来编译 FEX-Emu 和 Wine。目标设备是一台 iPhone 13,系统版本 iOS 16.5,通过 Palera1n 越狱。为什么选越狱设备?因为非越狱设备上 JIT 权限的获取太不稳定,而 FEX-Emu 没有 JIT 基本没法用。

宿主机上需要安装的依赖包括:cmake、clang、lld、python3、ninja-build、pkg-config,以及 Xcode 的命令行工具(如果你在 macOS 上编译的话)。在 Ubuntu 上交叉编译 iOS 目标,还需要cctools和ldid,这两个工具可以通过apt安装,也可以从源码编译。

目标设备上需要安装 OpenSSH 和基本的命令行工具,方便我通过 SSH 从宿主机推送文件和执行命令。越狱后可以通过 Cydia 或者 Sileo 安装openssh和coreutils。

4.2 编译 FEX-Emu 的完整命令与参数说明

我在宿主机上编译 FEX-Emu 用的是以下命令序列:

git clone --depth 1 https://github.com/FEX-Emu/FEX.git cd FEX git submodule update --init --recursive mkdir build && cd build cmake -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_COMPILER=clang \ -DCMAKE_CXX_COMPILER=clang++ \ -DENABLE_ASSERTIONS=OFF \ -DENABLE_LTO=ON \ -DBUILD_TESTS=OFF \ .. ninja -j16

ENABLE_LTO=ON开启链接时优化,能提升 5% 到 10% 的性能,但编译时间会增加不少。BUILD_TESTS=OFF跳过测试用例的编译,节省时间。-j16是并行任务数,根据你的 CPU 核心数调整,我 12 核 20 线程的 CPU 用 16 比较合适。

编译完成后,build/Bin/FEXLoader就是我们要的可执行文件。但它是 x86-64 的,不能直接在 ARM64 的 iPhone 上跑。等等,这里有个关键点:FEX-Emu 本身是运行在 ARM64 上的,所以我们需要的是 ARM64 版本的 FEXLoader。这意味着编译时要用 ARM64 的交叉编译工具链,而不是宿主机的 x86-64 工具链。

正确的做法是用aarch64-linux-gnu-gcc或者 Clang 的--target=aarch64-linux-gnu选项来编译。但因为我们最终目标是 iOS,所以还需要用 iOS SDK 来编译。这就回到了上一节说的交叉编译问题。

我实际用的命令是这样的:

cmake -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_SYSTEM_NAME=iOS \ -DCMAKE_OSX_ARCHITECTURES=arm64 \ -DCMAKE_OSX_SYSROOT=$(xcrun --sdk iphoneos --show-sdk-path) \ -DCMAKE_C_COMPILER=$(xcrun --sdk iphoneos -f clang) \ -DCMAKE_CXX_COMPILER=$(xcrun --sdk iphoneos -f clang++) \ -DENABLE_ASSERTIONS=OFF \ -DENABLE_LTO=ON \ ..

这样编译出来的 FEXLoader 就是 ARM64 的 iOS 可执行文件,可以直接放到 iPhone 上运行。

4.3 Wine 的配置与 Windows 程序的导入

Wine 编译完成后,你需要初始化一个 Wineprefix。在 iOS 设备上执行:

export WINEPREFIX=/var/mobile/wineprefix export WINEDEBUG=-all ./wine wineboot -u

wineboot -u会初始化 Wineprefix,创建虚拟的 C 盘目录结构、注册表文件等。这个过程在 iOS 上可能需要几分钟,因为涉及到大量的文件操作。WINEDEBUG=-all是关闭调试输出,否则屏幕上会刷满日志,影响性能。

初始化完成后,你可以把 Windows 程序复制到$WINEPREFIX/drive_c/目录下,然后通过 Wine 启动:

./wine "$WINEPREFIX/drive_c/your_program.exe"

但这里有个问题:Wine 本身是 ARM64 的,它不能直接加载 x86-64 的 exe 文件。所以你需要让 FEX-Emu 来加载 Wine,再由 Wine 来加载 exe。正确的调用链是:

./FEXLoader ./wine "$WINEPREFIX/drive_c/your_program.exe"

FEXLoader 会把 Wine 的 ARM64 指令翻译成 x86-64 指令?不对,反了。FEXLoader 是运行在 ARM64 上的,它加载的是 x86-64 版本的 Wine。所以你需要编译一个 x86-64 版本的 Wine,然后让 FEXLoader 去加载它。这样 FEXLoader 把 x86-64 的 Wine 指令翻译成 ARM64 指令执行,Wine 再加载 x86-64 的 exe 文件,整个链路就通了。

这就是为什么 FEX 需要一个 x86-64 的 RootFS——因为 Wine 和 exe 都是 x86-64 的,它们都需要在 FEX 的翻译环境下运行。

4.4 性能实测与调优记录

我在 iPhone 13 上跑了一个简单的 Windows 程序——一个用 Win32 API 写的时钟小程序,只有一个窗口和一个定时器。启动时间大约 18 秒,界面出现后每秒刷新一次,CPU 占用率在 40% 到 60% 之间波动,内存占用约 300MB。

这个性能显然不能用来跑游戏或者大型软件,但对于验证技术可行性来说已经足够了。我尝试过调优,主要从几个方面入手:

第一,增大 JIT 缓存。FEX 默认的 JIT 缓存大小是 128MB,我把它调到 512MB,重复执行的代码命中率明显提升,第二次启动时间缩短到 12 秒左右。

第二,关闭不必要的 Wine 服务。Wine 默认会启动一些后台服务,比如wineserver、explorer.exe等,这些在 iOS 上都是负担。我通过修改注册表把不需要的服务禁用了,CPU 占用率下降了大约 10%。

第三,使用FEX_MULTIBLOCK=1和FEX_OFFLINE=1组合。FEX_OFFLINE=1会让 FEX 在启动时预编译所有代码,而不是运行时动态编译。这会增加启动时间,但运行时的帧率更稳定。我实测下来,对于交互式程序,这个组合的体验更好。

5. 常见问题与排查技巧实录

5.1 启动崩溃:从日志定位问题根源

Madeira 这套东西最让人头疼的就是启动崩溃,而且崩溃信息往往非常模糊。我遇到过几次典型的崩溃场景,这里整理一下排查思路。

第一种崩溃是Killed: 9,这通常是签名问题。iOS 在加载可执行文件时会校验签名,如果签名无效或者 entitlements 不匹配,系统会直接杀掉进程。排查方法是检查ldid -e输出的 entitlements 是否包含dynamic-codesigning和com.apple.security.cs.allow-jit。如果没有,需要重新签名。

第二种崩溃是Segmentation fault: 11,这通常是 FEX 在翻译指令时遇到了不支持的指令或者内存访问越界。排查方法是设置FEX_LOG_LEVEL=debug,让 FEX 输出详细的翻译日志,看看崩溃前最后翻译的是哪条指令。如果是不支持的指令,可能需要升级 FEX 版本或者手动打补丁。

第三种崩溃是Abort trap: 6,这通常是 Wine 内部的断言失败。排查方法是设置WINEDEBUG=+all,让 Wine 输出完整的调试日志,然后搜索Assertion failed关键字,定位到具体的代码位置。

5.2 中文乱码:字体与编码的双重问题

Wine 在 iOS 上运行时,中文乱码是一个非常常见的问题。根本原因有两个:一是缺少中文字体,二是编码设置不正确。

字体问题好解决,把 Windows 的simsun.ttc或者开源的Noto Sans CJK复制到 Wineprefix 的drive_c/windows/Fonts/目录下,然后在注册表里把默认字体替换成中文字体。注册表路径是HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Fonts,添加一个SimSun的键值指向你的字体文件。

编码问题稍微麻烦一些。Wine 默认使用 UTF-8 编码,但很多老程序用的是 GBK 或者 GB2312。你需要在 Wineprefix 的system.reg文件里设置Locale为zh_CN.GBK,或者在启动时设置LANG=zh_CN.GBK环境变量。我实测下来,对于大多数中文程序,设置LANG=zh_CN.UTF-8加上中文字体就能正常显示,少数老程序才需要 GBK。

提示:如果你在 Wine 里看到的是方块或者问号,那基本就是字体问题;如果看到的是乱码字符,那基本就是编码问题。两者要分开处理,不要混在一起调。

5.3 性能瓶颈:CPU 占用高、响应慢的优化方向

性能问题是最难解决的,因为它的根源在于架构差异。x86-64 和 ARM64 的指令集差异很大,FEX 的翻译效率再高,也不可能达到原生执行的速度。但你可以通过一些手段来缓解。

首先,减少 Wine 的调试输出。WINEDEBUG=-all是必须的,否则日志输出会占用大量 CPU。其次,关闭 FEX 的断言检查,ENABLE_ASSERTIONS=OFF在编译时就要设置好。再次,使用FEX_MULTIBLOCK=1和FEX_OFFLINE=1组合,让 FEX 预编译热点代码。

如果还是慢,可以考虑降低程序的图形需求。比如把程序的窗口大小调小,关闭动画效果,减少重绘频率。我试过把一个 800x600 的窗口改成 400x300,帧率提升了将近一倍。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
启动即崩溃,无日志签名无效检查 entitlements重新签名,添加 JIT 权限
启动后卡在加载界面RootFS 不匹配检查 FEX 和 RootFS 版本下载匹配版本的 RootFS
中文显示为方块缺少中文字体检查 Fonts 目录复制中文字体并修改注册表
中文显示为乱码编码设置错误检查 LANG 环境变量设置 LANG=zh_CN.UTF-8
运行极慢,CPU 满载JIT 未启用检查 FEX_ENABLE_JIT设置 FEX_ENABLE_JIT=1
Wine 报错找不到 DLLWineprefix 不完整检查 drive_c 目录重新执行 wineboot -u
程序窗口无法显示图形驱动不兼容检查 Wine 的图形后端切换到 X11 或 Wayland 后端
内存占用持续增长内存泄漏监控进程内存限制程序运行时间,定期重启

6. 这套方案还能怎么扩展

Madeira 目前的状态是“能跑,但不好用”。如果你对这个方向感兴趣,有几个扩展思路值得尝试。

第一个方向是优化 FEX 的翻译策略。目前的 FEX 是动态翻译,每次遇到新代码都要翻译一遍。如果能加入离线预翻译,把常用的 Windows DLL 提前翻译成 ARM64 缓存,启动速度会大幅提升。这个思路在 FEX 的社区里已经有讨论了,但还没有成熟的实现。

第二个方向是精简 Wine 的组件。Wine 本身很庞大,很多功能在 iOS 上根本用不到。如果能裁剪出一个最小化的 Wine,只保留核心的 API 兼容层,体积和内存占用都能降下来。这个工作需要深入理解 Wine 的源码结构,难度不小,但收益很直接。

第三个方向是探索非越狱方案。目前 Madeira 依赖越狱设备来获取 JIT 权限,这限制了它的受众。如果能找到一种在非越狱设备上合法获取 JIT 权限的方法,比如利用某些开发者接口或者系统漏洞,那这套方案就能覆盖更多用户。当然,这条路涉及的技术和法律问题都很复杂,需要谨慎对待。

我个人在实际操作中的体会是,Madeira 这类项目的价值不在于它能跑多少程序,而在于它证明了在 iOS 这种封闭平台上,通过层层翻译和兼容,理论上可以运行任何架构的任何程序。这个证明本身,比跑起来一个记事本更有意义。如果你也在折腾类似的东西,建议先把 FEX-Emu 在 Linux ARM 上跑通,再迁移到 iOS,这样能少踩很多坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询