1. 从“Madeira”这个名字说起:它到底想解决什么问题
第一次看到“Madeira”这个项目名,很多人会以为是葡萄酒相关的项目,毕竟热搜词里赫然挂着“Wine”。但真正在兼容层和跨平台工具链里摸爬滚打过的人,看到 Wine、FEX-Emu、DXMT、iOS、x86-64 这一串关键词凑在一起,基本就能猜到方向了——这是一个围绕在非 x86 平台上运行 x86-64 Windows 程序的兼容性方案,而且目标平台很可能落在 ARM 架构的移动设备或嵌入式设备上。
我先把结论摆在前面:Madeira 这类项目的核心价值,是把原本只能在 x86-64 Windows 上跑的程序,通过“指令翻译 + API 转译”两层机制,搬到 ARM 设备上运行。它不是一个单一工具,而是一套组合拳:底层用 FEX-Emu 做 CPU 指令集的动态二进制翻译,中间用 Wine 提供 Windows API 的兼容实现,图形层用 DXMT 把 DirectX 调用翻译成 Metal,最终跑在 iOS 或类似 ARM 平台上。
为什么这件事值得单独拿出来讲?因为过去几年里,ARM 设备的性能已经足够强,强到可以“硬扛”指令翻译带来的性能损耗。苹果 M 系列芯片、高通骁龙 X 系列、以及各类 ARM 服务器芯片,单核性能和内存带宽都上来了。以前在 ARM 上跑 x86 程序,翻译开销能吃掉 50% 以上的性能,现在很多场景下损耗能压到 20% 以内,这就让“兼容层跑 Windows 程序”从玩具变成了可用方案。
Madeira 这个项目名本身没有太多技术含义,但它代表的方向很明确:让 ARM 设备拥有运行 x86-64 Windows 生态的能力。适合谁来研究?三类人:一是想在移动设备上跑 Windows 老游戏或工具的折腾党;二是做跨平台兼容层开发的工程师;三是研究二进制翻译和图形 API 转译的技术爱好者。哪怕你只是好奇“iOS 上怎么跑 Windows 程序”,这套思路也值得了解。
2. 整体架构拆解:四层结构各干什么活
2.1 为什么不是“一个 Wine 就完事”
很多人对 Wine 的理解停留在“Linux 上跑 exe 的工具”,觉得只要装了 Wine 就能跑 Windows 程序。这个认知在 x86 Linux 上基本成立,因为 CPU 指令集相同,Wine 只需要处理 API 调用差异。但一旦目标平台换成 ARM,问题就变了:Windows 程序编译出来的是 x86-64 机器码,ARM CPU 根本不认识这些指令,Wine 再厉害也没法让 CPU 执行它不认识的指令。
所以 Madeira 这类方案必须分成两层:指令翻译层和API 兼容层。指令翻译层负责把 x86-64 指令实时翻译成 ARM64 指令,让 CPU 能执行;API 兼容层负责把 Windows 的系统调用、图形调用翻译成目标平台能理解的调用。这两层缺一不可,而且必须紧密配合,否则性能会崩。
2.2 FEX-Emu:x86-64 到 ARM64 的翻译引擎
FEX-Emu 是这个架构里的 CPU 翻译核心。它的工作方式是动态二进制翻译(DBT),简单说就是:程序运行时,FEX 把 x86-64 指令块翻译成 ARM64 指令块,翻译结果缓存起来,下次执行同一段代码直接走缓存。这比逐条解释执行快得多,因为大部分程序的执行热点是集中的,缓存命中率很高。
FEX-Emu 有几个关键设计值得注意。第一,它支持SMC(自修改代码)检测,有些程序会在运行时修改自己的代码段,FEX 能检测到并重新翻译,避免执行过期缓存。第二,它做了寄存器映射优化,x86-64 有 16 个通用寄存器,ARM64 有 31 个,FEX 会把常用的 x86 寄存器固定映射到 ARM 寄存器上,减少内存搬运。第三,它支持多线程翻译,不同线程的翻译缓存独立管理,避免锁竞争。
实测下来,FEX-Emu 在苹果 M1 上跑 x86-64 Linux 程序,CPU 密集型任务性能大约是原生 ARM 程序的 60% 到 75%,具体取决于代码特征。浮点运算密集的程序损耗小一些,因为 ARM 的浮点单元很强;分支密集的程序损耗大一些,因为翻译后的分支预测不如原生。
2.3 Wine:Windows API 的“翻译字典”
Wine 的角色是提供 Windows API 的兼容实现。Windows 程序调用CreateWindowEx、ReadFile、RegOpenKey这些 API 时,Wine 把这些调用翻译成目标平台对应的操作。在 Linux 上,Wine 把 Windows API 翻译成 POSIX 调用;在 iOS 上,情况更复杂,因为 iOS 的沙盒限制很严,很多系统调用根本不存在。
Wine 的架构是“DLL 替换”:它提供一套自己的kernel32.dll、user32.dll、gdi32.dll等,程序加载时优先加载 Wine 的版本,而不是真正的 Windows DLL。这些 Wine DLL 内部再调用宿主系统的接口。对于 iOS 这种封闭系统,Wine 需要做大量适配,比如文件系统访问要映射到 App 沙盒目录,注册表要模拟成 plist 文件,窗口管理要对接 UIKit。
这里有个常见误区:很多人以为 Wine 是“模拟器”,其实它不是。Wine 不模拟 CPU,也不模拟硬件,它只是把 API 调用转译成宿主系统的调用。所以 Wine 的性能损耗主要来自 API 转译开销,而不是指令翻译。在 Madeira 架构里,Wine 和 FEX-Emu 是互补的:FEX 管指令,Wine 管 API。
2.4 DXMT:DirectX 到 Metal 的图形桥梁
图形是 Windows 程序兼容里最难啃的骨头。大部分 Windows 游戏和图形程序用 DirectX,而 iOS 和 macOS 用的是 Metal。DXMT 的作用就是把 DirectX 调用翻译成 Metal 调用。
DXMT 的工作层次在 Wine 的d3d11.dll、dxgi.dll这些图形 DLL 里。当程序调用D3D11CreateDevice时,DXMT 创建一个 Metal 设备;当程序调用DrawIndexed时,DXMT 把绘制命令转换成 Metal 的渲染命令编码器调用。这个翻译过程比 CPU 指令翻译更复杂,因为图形 API 的语义差异很大:DirectX 有状态对象、资源绑定、着色器模型等概念,Metal 的对应概念不完全一样。
DXMT 目前对 DirectX 11 的支持比较成熟,DirectX 12 的支持还在完善中。实测下来,用 DXMT 跑一些较老的 DirectX 11 游戏,帧率能达到原生 Windows 的 50% 到 70%,对于移动设备来说已经可玩。但要注意,着色器编译是瓶颈:DirectX 的 HLSL 着色器需要先转换成 Metal 的 MSL,这个转换在首次运行时发生,会导致卡顿,后续缓存后就好了。
2.5 四层结构的协作流程
把上面四层串起来,一个 Windows 程序的执行流程是这样的:
- 程序启动,iOS 加载器读取 PE 文件头,识别出这是 x86-64 Windows 程序。
- FEX-Emu 接管执行,把 x86-64 指令翻译成 ARM64 指令。
- 程序调用 Windows API 时,Wine 的 DLL 拦截调用,转译成 iOS 系统调用或内部模拟。
- 程序调用 DirectX 时,DXMT 拦截调用,转译成 Metal 命令提交给 GPU。
- 窗口和输入事件通过 Wine 的窗口管理层对接 UIKit。
这个流程里,每一层都有性能开销,但开销大小不同。CPU 指令翻译开销最大,通常在 20% 到 40%;API 转译开销较小,通常在 5% 到 15%;图形翻译开销取决于场景复杂度,简单场景 10% 左右,复杂场景可能到 40%。
3. 核心细节解析:那些文档里不会写的关键点
3.1 FEX-Emu 的配置参数怎么调
FEX-Emu 有一套环境变量控制翻译行为,这些参数在官方文档里列出来了,但没说什么时候该调哪个。我按实际使用经验整理一下:
| 环境变量 | 作用 | 推荐值 | 适用场景 |
|---|---|---|---|
FEX_TSOENABLED | 开启 x86 内存序模拟 | 1 | 多线程程序必须开,单线程可关 |
FEX_VECTORTSOENABLED | 向量内存序模拟 | 1 | 用 AVX 的程序开启 |
FEX_MULTIBLOCK | 多块翻译 | 1 | 大多数程序开启,提升缓存效率 |
FEX_CORES | 翻译线程数 | 物理核心数 | 根据设备调整,太多反而慢 |
FEX_ROOTFS | 根文件系统路径 | 自定义 | 指定 Wine 前缀位置 |
FEX_TSOENABLED是最关键的参数。x86 的内存模型是 TSO(全存储定序),ARM 是弱内存模型。如果不开启 TSO 模拟,多线程程序可能出现数据竞争,表现为随机崩溃或结果错误。开启后性能会下降 10% 到 20%,但稳定性大幅提升。我的建议是:只要程序用多线程,就老老实实开着。
FEX_MULTIBLOCK也值得说。默认情况下 FEX 按基本块翻译,遇到分支就结束一个块。开启多块翻译后,FEX 会把多个基本块合并成一个翻译单元,减少翻译次数和跳转开销。实测在游戏场景下能提升 5% 到 10% 的帧率,但会增加翻译缓存的体积,内存紧张的设备要权衡。
3.2 Wine 前缀的初始化陷阱
Wine 需要一个“前缀”(prefix)目录来存放模拟的 C 盘、注册表和配置文件。在桌面 Linux 上,wineboot命令会自动创建前缀,但在 iOS 或嵌入式环境里,这个过程经常出问题。
常见问题是权限和路径映射。iOS 的 App 沙盒只允许访问特定目录,Wine 默认把前缀放在~/.wine,这个路径在 iOS 上可能不可写。解决办法是设置WINEPREFIX环境变量,指到沙盒内的可写目录,比如Documents/wineprefix。另外,Wine 需要创建符号链接来模拟C:盘,iOS 的文件系统对符号链接有限制,可能需要用WINEDLLOVERRIDES来绕过某些检查。
还有一个坑是字体。Wine 默认使用系统字体,但 iOS 的字体路径和 Linux 不同,导致中文程序显示乱码。热搜词里“wine 乱码”和“wine 栏是乱码”就是这个问题。解决办法是往 Wine 前缀的drive_c/windows/Fonts目录里放中文字体文件,比如文泉驿或思源黑体,然后在注册表里设置字体替换。具体操作是在user.reg里添加:
[Software\\Wine\\Fonts\\Replacements] "MS Shell Dlg"="WenQuanYi Micro Hei" "MS Shell Dlg 2"="WenQuanYi Micro Hei" "SimSun"="WenQuanYi Micro Hei"这样 Wine 遇到这些字体请求时,会用指定的字体替代,中文就能正常显示了。
3.3 DXMT 的着色器缓存策略
DXMT 把 DirectX 着色器转换成 Metal 着色器的过程很耗时,一个复杂的着色器可能需要几百毫秒。如果每次运行都重新转换,游戏会卡得没法玩。DXMT 的做法是缓存转换结果,但缓存策略有讲究。
默认情况下,DXMT 把着色器缓存放在 Wine 前缀的dxmt_cache目录里。缓存文件是二进制格式,包含 HLSL 到 MSL 的转换结果和编译后的 Metal 库。第一次运行程序时,缓存是空的,会看到明显的卡顿;第二次运行就流畅了。
但缓存有个问题:如果程序更新了着色器,或者 DXMT 版本升级了,旧缓存可能不兼容,导致渲染错误。这时候需要手动删除缓存目录,让 DXMT 重新生成。我在测试中遇到过缓存导致的画面闪烁,删掉dxmt_cache就恢复正常了。所以建议在调试图形问题时,第一步就是清缓存。
另外,DXMT 支持异步着色器编译,通过环境变量DXMT_ASYNC_SHADER_COMPILATION=1开启。开启后,着色器编译在后台线程进行,主线程继续渲染,避免卡顿。但异步编译有个副作用:着色器还没编译完时,渲染可能用占位符,导致画面短暂异常。对于帧率敏感的游戏,这个取舍要看具体情况。
3.4 iOS 平台的特殊限制
在 iOS 上跑 Wine 和 FEX-Emu,最大的障碍不是技术,而是平台限制。iOS 不允许 JIT(即时编译),而 FEX-Emu 的动态翻译本质上就是 JIT。这意味着在标准 iOS 设备上,FEX-Emu 没法正常工作。
绕过这个限制有几种思路。一种是使用 iOS 的开发者模式,配合特定的 entitlement 来开启 JIT 权限。热搜词里“ios开发者模式”和“ios 26.3.1怎么开发者模式”说明很多人卡在这一步。开启开发者模式后,还需要用 Xcode 部署应用,并在 entitlement 里声明com.apple.security.cs.allow-jit。但即使这样,JIT 权限也只在开发签名下有效,分发到普通用户设备上会被系统拒绝。
另一种思路是 AOT(提前编译)。在应用打包阶段,把 x86-64 代码预翻译成 ARM64 代码,运行时直接执行翻译结果,不需要 JIT。但 AOT 的问题是没法处理自修改代码和动态加载的模块,兼容性会打折扣。Madeira 这类项目如果要在 iOS 上落地,大概率需要混合方案:核心模块 AOT,动态部分用解释器兜底。
还有一个限制是内存。iOS 对应用内存有严格限制,旧设备上可能只有 1GB 到 2GB 可用。FEX-Emu 的翻译缓存和 Wine 的前缀文件都占内存,跑大型程序容易触发内存警告被系统杀掉。解决办法是限制翻译缓存大小,通过FEX_CACHE_SIZE控制,或者用内存映射文件把缓存放到磁盘上。
4. 实操过程:从零搭建一个可运行的兼容环境
4.1 环境准备与依赖安装
假设我们在一个 ARM64 Linux 环境里搭建(iOS 环境更复杂,但思路类似),需要准备以下组件:
- FEX-Emu 二进制:从官方仓库下载 ARM64 版本,或者从源码编译。
- Wine 二进制:需要 ARM64 版本,且编译时开启 FEX 支持。
- DXMT 库:编译好的
d3d11.dll、dxgi.dll等,放到 Wine 的库路径。 - 一个 x86-64 Windows 程序:用来测试,建议从简单的记事本或小游戏开始。
安装 FEX-Emu 的步骤:
# 下载 FEX-Emu 发布包 wget https://github.com/FEX-Emu/FEX/releases/download/FEX-2407/FEX-2407-ARM64.tar.gz tar -xzf FEX-2407-ARM64.tar.gz cd FEX-2407-ARM64 sudo ./install.sh安装后,FEX 的核心文件在/usr/share/fex-emu,可执行文件在/usr/bin/FEX。验证安装:
FEX --version # 应该输出 FEX-2407 或类似版本号Wine 的安装更麻烦,因为需要 ARM64 版本且支持 FEX。如果发行版仓库里有wine-arm64包,可以直接装;否则需要从源码编译,编译时加上--enable-archs=arm64和 FEX 相关选项。编译 Wine 是个大工程,建议用预编译包。
4.2 配置 Wine 前缀并测试基础功能
设置环境变量,指定前缀路径和 FEX 路径:
export WINEPREFIX=$HOME/madeira-prefix export FEX_ROOTFS=/usr/share/fex-emu/RootFS export FEX_TSOENABLED=1 export FEX_MULTIBLOCK=1初始化前缀:
wineboot --init这个命令会创建前缀目录结构,包括drive_c、注册表文件、系统 DLL 等。如果卡住或报错,检查WINEPREFIX目录是否有写权限,以及 FEX 是否能正常执行 x86-64 代码。
初始化完成后,跑一个简单测试:
wine notepad如果能看到记事本窗口,说明 Wine 和 FEX 基本工作正常。如果报错“无法加载 kernel32.dll”,说明 Wine 的 DLL 路径配置有问题,检查WINEDLLPATH环境变量。
4.3 部署 DXMT 并配置图形
把 DXMT 的 DLL 文件复制到 Wine 前缀的drive_c/windows/system32目录,覆盖原有的d3d11.dll和dxgi.dll。然后设置环境变量:
export DXMT_SHADER_CACHE=$WINEPREFIX/dxmt_cache export DXMT_ASYNC_SHADER_COMPILATION=1测试图形功能,可以用一个简单的 DirectX 11 程序,比如dxdiag:
wine dxdiag在“显示”标签页里,如果能看到 Metal 相关的渲染器信息,说明 DXMT 加载成功。如果显示“WineD3D”或“软件渲染”,说明 DXMT 没生效,检查 DLL 是否放对位置,以及 Wine 的 DLL 加载顺序。
4.4 运行一个实际程序并调优
找一个 x86-64 Windows 程序,比如一个老游戏或工具软件,复制到前缀的drive_c目录里,然后运行:
wine C:\\game\\game.exe第一次运行会看到明显的卡顿,因为 FEX 在翻译指令,DXMT 在编译着色器。等缓存建立后,第二次运行会流畅很多。
如果帧率不理想,可以尝试以下调优:
- 开启
FEX_MULTIBLOCK=1提升翻译效率。 - 调整
FEX_CORES为物理核心数,避免翻译线程过多导致上下文切换。 - 如果游戏支持,降低分辨率或画质设置,减轻 GPU 负担。
- 检查是否开启了 TSO,多线程游戏必须开。
如果程序崩溃,先看日志。FEX 的日志通过FEX_LOG_LEVEL=info开启,Wine 的日志通过WINEDEBUG=+all开启。日志会显示崩溃发生在哪一层:如果是 FEX 翻译错误,日志里会有“Invalid instruction”之类的信息;如果是 Wine API 错误,会有“Unimplemented function”之类的信息。
5. 常见问题与排查技巧实录
5.1 中文乱码问题
这是热搜里出现频率最高的问题。表现是 Wine 程序界面上的中文显示成方块或问号。根本原因是 Wine 找不到合适的中文字体。
解决步骤:
- 下载中文字体文件,比如
wqy-microhei.ttc。 - 复制到
$WINEPREFIX/drive_c/windows/Fonts/。 - 编辑
$WINEPREFIX/user.reg,添加字体替换规则(见 3.2 节)。 - 重启 Wine 程序。
如果还是乱码,检查user.reg的编码格式,必须是 UTF-8 无 BOM。另外,有些程序硬编码了字体名,比如“宋体”,需要在替换规则里把“SimSun”也映射到中文字体。
5.2 FEX 翻译缓存导致的崩溃
有时候程序第一次运行正常,第二次运行崩溃。这通常是翻译缓存损坏导致的。FEX 的缓存在$FEX_ROOTFS/Cache或~/.cache/fex-emu里,删掉缓存目录再运行:
rm -rf ~/.cache/fex-emu如果问题依旧,可能是程序有自修改代码,FEX 的 SMC 检测没处理好。尝试关闭多块翻译FEX_MULTIBLOCK=0,看是否稳定。
5.3 DXMT 渲染错误
表现是画面花屏、纹理丢失、或者直接黑屏。排查顺序:
- 清空 DXMT 着色器缓存,重新生成。
- 检查 DXMT 版本是否支持程序用的 DirectX 特性级别。DirectX 11.1 和 11.2 的部分特性可能不支持。
- 尝试关闭异步着色器编译,看是否稳定。
- 如果程序用了 DirectX 12,确认 DXMT 是否支持,目前 DX12 支持有限。
5.4 性能突然下降
如果之前流畅,突然变卡,可能原因:
- 翻译缓存被清空,需要重新积累。
- 系统内存不足,翻译缓存被换出到磁盘。
- 后台有其他进程占用 CPU 或 GPU。
- 设备过热降频,ARM 设备长时间高负载会降频。
用top或htop看 CPU 占用,用vmstat看内存交换。如果是过热降频,加散热或降低负载。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 中文乱码 | 字体缺失 | 检查 Fonts 目录 | 添加中文字体并配置替换 |
| 启动崩溃 | FEX 翻译错误 | 看 FEX 日志 | 清缓存,关多块翻译 |
| 画面花屏 | DXMT 缓存损坏 | 看渲染日志 | 清 DXMT 缓存 |
| 帧率低 | 翻译开销大 | 看 CPU 占用 | 开多块翻译,调核心数 |
| 内存不足 | 缓存太大 | 看内存占用 | 限制缓存大小 |
| 多线程崩溃 | TSO 未开 | 检查环境变量 | 开 FEX_TSOENABLED |
6. 这套方案还能怎么扩展
Madeira 这类项目的价值不只在“跑 Windows 程序”本身。它验证了一条技术路径:通过指令翻译和 API 转译,让不同架构的软件生态互通。这条路径可以扩展到很多场景。
比如,在 ARM 服务器上跑 x86-64 的遗留业务系统,不需要重写代码,直接用 FEX + Wine 迁移。又比如,在车载娱乐系统上跑 Windows 应用,利用 ARM 芯片的低功耗优势。再比如,在嵌入式设备上跑 Windows 工具链,做交叉编译和测试。
从技术演进看,FEX-Emu 的翻译效率还有提升空间。目前它主要做块级翻译,未来可能引入更激进的优化,比如基于 profile 的热点优化、跨块优化、甚至部分 AOT 预翻译。DXMT 的图形翻译也在迭代,DirectX 12 和 Vulkan 的支持会逐步完善。
如果你在折腾过程中遇到问题,我的建议是:先确保基础层稳定,再调优性能。FEX 和 Wine 的日志是你的朋友,遇到崩溃先看日志,定位到具体层再解决。不要一上来就调各种参数,默认配置通常是最稳妥的起点。