1. 从"Madeira"这个名字说起:一个跨平台兼容层的真实需求
第一次看到"Madeira"这个项目名,很多人会联想到那个葡萄牙的岛屿或者一种葡萄酒。但在跨平台兼容这个圈子里,这个名字背后代表的是一类非常实际的需求:让原本为 Windows 编译的 x86-64 应用程序,能够在非 Windows 环境里跑起来。关键词里出现的 Wine、FEX-Emu、DXMT、x86-64 这几个词,基本就把这个项目的技术底座勾勒清楚了。
我做跨平台兼容相关工作有些年头了,从最早的纯 Wine 方案,到后来配合 DXVK、VKD3D,再到近两年开始接触 FEX-Emu 这类 x86-64 到 ARM64 的指令翻译层,踩过的坑可以说能写一本书。Madeira 这个项目吸引我的地方在于,它不是单纯地套一层 Wine 就完事,而是把指令翻译、图形 API 转换、系统调用桥接这几层做了整合。换句话说,它想解决的是"一个 Windows 程序在异构平台上从能启动到能用"的完整链路问题。
这篇文章适合几类人看:一是正在做 Linux 或 ARM 平台 Windows 应用兼容的开发者;二是对 Wine 生态感兴趣、想搞清楚 FEX-Emu 和 DXMT 各自扮演什么角色的技术爱好者;三是遇到了 Wine 乱码、组件下载失败、图形渲染异常这些具体问题,想找到排查思路的运维或测试人员。我会尽量把原理讲透,同时给出可以直接上手操作的步骤和参数。
需要提前说明的是,Madeira 目前并不是一个像 Wine 那样有十几年沉淀的成熟项目,它更像是一个整合方案或者实验性项目。所以下面的内容里,有一部分是基于我在类似项目中的实践经验做的合理推演,我会明确标注哪些是实测结论、哪些是基于常见实践的补充。
2. Madeira 的技术栈拆解:Wine、FEX-Emu、DXMT 各自管什么
2.1 Wine 负责的是 API 翻译,不是指令翻译
很多人对 Wine 有个误解,以为它能把 Windows 程序"翻译"成 Linux 程序。实际上 Wine 做的是 API 层面的兼容:它实现了 Windows 的 PE 加载器、NT 内核接口、Win32 API、注册表、COM 组件等一整套运行时环境。当一个 Windows 程序调用CreateFileW的时候,Wine 把这个调用映射到 Linux 的open系统调用上。
但这里有个关键前提:Wine 本身不负责 CPU 指令集的转换。如果你的程序是 x86-64 的二进制,而你的运行平台也是 x86-64,那 Wine 直接加载执行就行。可如果你的平台是 ARM64,比如苹果 M 系列芯片或者高通的 ARM 笔记本,那 x86-64 的指令就没法直接执行了。这时候就需要 FEX-Emu 出场。
2.2 FEX-Emu 做的是 x86-64 到 ARM64 的指令翻译
FEX-Emu 是一个用户态的 x86-64 模拟器,它的工作方式是把 x86-64 的机器码动态翻译成 ARM64 的机器码。和 QEMU 那种全系统模拟不同,FEX-Emu 是用户态的,它只翻译应用程序本身的指令,系统调用还是走宿主系统的。
这里有个性能上的关键点:FEX-Emu 采用了 JIT 编译加缓存的方式。第一次执行某段代码的时候,它把 x86-64 指令翻译成 ARM64 指令并缓存起来,后续再执行同一段代码就直接用缓存。所以冷启动会慢一些,但热路径的性能可以做到原生性能的 60% 到 80%,具体取决于程序的指令特征。
在 Madeira 这个组合里,FEX-Emu 和 Wine 是配合工作的。Wine 的 x86-64 版本本身也是 x86-64 二进制,所以它也需要被 FEX-Emu 翻译。这就形成了一个链路:FEX-Emu 翻译 Wine 的代码,Wine 再去翻译 Windows 程序的 API 调用。
2.3 DXMT 解决的是 Direct3D 到 Metal 的转换
DXMT 这个名字可能有些人不太熟,但如果说 DXVK 和 D3DMetal,了解的人就多了。DXVK 是把 D3D9/10/11 转换成 Vulkan,D3DMetal 是苹果官方方案把 D3D 转成 Metal。DXMT 走的是类似 D3DMetal 的路线,把 Direct3D 的调用转换成 Metal 的调用。
为什么在 Madeira 的场景里需要 DXMT 而不是 DXVK?因为在 ARM 平台的 macOS 上,Vulkan 的支持并不原生,需要通过 MoltenVK 再转一层到 Metal,链路太长、开销太大。DXMT 直接做 D3D 到 Metal 的转换,少了一层中间环节,对于图形密集型的 Windows 程序来说,帧率和延迟表现会更好。
| 组件 | 职责 | 输入 | 输出 |
|---|---|---|---|
| Wine | Windows API 翻译 | Windows PE 二进制 | 宿主系统调用 |
| FEX-Emu | 指令集翻译 | x86-64 机器码 | ARM64 机器码 |
| DXMT | 图形 API 转换 | Direct3D 调用 | Metal 调用 |
这三个组件叠在一起,才构成了 Madeira 让 Windows 程序在 ARM 平台运行的基础。理解了这个分层,后面遇到问题的时候就能快速定位是哪一层出了状况。
3. 环境搭建:从零把 Madeira 跑起来的关键步骤
3.1 确认你的平台和依赖版本
Madeira 目前主要面向 ARM64 的 Linux 和 macOS 环境。在开始之前,你需要确认几件事:
- CPU 架构是 ARM64(aarch64)。可以用
uname -m确认,输出应该是aarch64或arm64。 - 内核版本建议 5.15 以上,因为 FEX-Emu 依赖一些较新的系统调用特性。
- 如果是 Linux,需要确认
binfmt_misc已经挂载,这是让系统能识别 x86-64 二进制并自动交给 FEX-Emu 处理的关键机制。
# 确认架构 uname -m # 确认 binfmt_misc 是否挂载 mount | grep binfmt_misc # 如果没有挂载,手动挂载 sudo mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc这里有个容易忽略的点:很多发行版默认没有启用 binfmt_misc 的自动注册。你需要把 FEX-Emu 的 binfmt 配置写进去,否则系统看到 x86-64 二进制会直接报"无法执行二进制文件"。
3.2 安装 FEX-Emu 并注册 binfmt
FEX-Emu 的安装方式取决于你的发行版。在 Ubuntu/Debian 系上,可以通过 PPA 安装;在 Arch 上可以用 AUR。安装完成后,最关键的一步是注册 binfmt。
# 以 FEX-Emu 为例,注册 x86-64 二进制的处理程序 sudo mkdir -p /usr/lib/binfmt.d echo ':FEX-x86_64:M::\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x3e\x00:\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff:/usr/bin/FEXInterpreter:PF' | sudo tee /usr/lib/binfmt.d/FEX-x86_64.conf # 重新加载 binfmt 配置 sudo systemctl restart systemd-binfmt注册完成后,你可以随便找一个 x86-64 的二进制测试一下,比如下载一个静态编译的 busybox x86-64 版本,直接执行看是否能跑起来。如果能跑,说明 FEX-Emu 的指令翻译层已经工作了。
注意:binfmt 的魔数(magic number)配置必须和实际的 ELF 头匹配,写错一个字节就会导致注册失败但没有任何报错。建议用
xxd看一下目标二进制的头部,确认魔数是否正确。
3.3 Wine 的安装与 WoW64 模式选择
在 ARM64 平台上装 Wine,有两种模式:一种是纯 64 位 Wine,另一种是 WoW64 模式(支持 32 位程序)。如果你要跑的程序里有 32 位的,就必须用 WoW64 模式。
Wine 的 WoW64 模式在 9.0 之后有了新实现,叫"new WoW64",它不需要 32 位的宿主库支持,而是通过 64 位的 Wine 来承载 32 位程序。这在 ARM64 平台上特别有用,因为很多 ARM64 发行版根本不提供 32 位的库。
# 检查 Wine 版本和 WoW64 支持 wine --version wine --check-libs # 初始化 Wine prefix(建议指定为 64 位) WINEARCH=win64 WINEPREFIX=~/.wine-madeira wineboot --init初始化过程中,Wine 会提示你安装 Wine Gecko 和 Wine Mono。这两个组件分别用于支持 HTML 渲染和 .NET 程序。如果你在离线环境或者网络受限的环境里,这一步会卡住。解决办法是提前下载好对应的 .msi 包,放到 Wine 的缓存目录里。
3.4 DXMT 的部署位置和加载顺序
DXMT 本质上是一组 DLL,需要放到 Wine prefix 的system32和syswow64目录里,并且要在 Wine 的 DLL 加载顺序里覆盖掉原生的 d3d11.dll、d3d10core.dll、dxgi.dll 等。
# 假设 DXMT 的构建产物在 /opt/dxmt/ # 拷贝到 Wine prefix cp /opt/dxmt/x64/*.dll ~/.wine-madeira/drive_c/windows/system32/ cp /opt/dxmt/x32/*.dll ~/.wine-madeira/drive_c/windows/syswow64/ # 设置 DLL 覆盖 WINEPREFIX=~/.wine-madeira winecfg # 在 Libraries 标签页里,把 d3d11、dxgi、d3d10core 设置为 Native这里有个顺序问题:DXMT 依赖 Metal,而 Metal 在 Linux 上是不存在的。所以 DXMT 实际上主要用于 macOS 环境。如果你在 Linux ARM64 上,图形转换应该用 DXVK 配合 MoltenVK 或者直接用 VKD3D。这一点在部署前必须搞清楚,否则你会花大量时间调试一个根本跑不起来的组合。
4. 那些让人抓狂的乱码和组件问题:逐个击破
4.1 Wine 乱码的三种典型场景
Wine 乱码是我被问得最多的问题,没有之一。但"乱码"这个词太笼统了,实际上它至少分三种情况,每种的原因和解决办法完全不同。
第一种是菜单栏和对话框里的文字变成方块或者问号。这通常是字体缺失导致的。Wine 默认会去找系统字体,但如果宿主系统没有安装中文字体,或者 Wine 的字体替换表没配好,就会显示成方块。解决办法是安装中文字体,并且在 Wine 的注册表里配置字体替换。
# 安装常用中文字体 sudo apt install fonts-wqy-microhei fonts-wqy-zenhei # 在 Wine 里注册字体替换 WINEPREFIX=~/.wine-madeira wine reg add "HKCU\Software\Wine\Fonts\Replacements" /v "SimSun" /t REG_SZ /d "WenQuanYi Micro Hei" /f第二种是终端输出里的中文变成乱码。这是编码问题。Wine 的控制台默认用 CP936(GBK)编码,而 Linux 终端通常是 UTF-8。你需要在 Wine 的注册表里把代码页改成 UTF-8,或者用chcp 65001在程序内部切换。
第三种是某些程序界面里的文字显示为"口口口"或者奇怪的符号。这往往是程序自己带了字体,但字体渲染出了问题。可以尝试在winecfg的 Graphics 标签页里调整 DPI 设置,有时候 DPI 不对会导致字体渲染异常。
| 乱码表现 | 根本原因 | 解决方向 |
|---|---|---|
| 方块、问号 | 字体缺失 | 安装字体 + 注册表替换 |
| 终端乱码 | 编码不匹配 | 改代码页为 UTF-8 |
| 口口口 | 字体渲染异常 | 调整 DPI 和渲染设置 |
| 菜单栏乱码 | 字体链接错误 | 检查 FontLink 注册表项 |
4.2 Wine Gecko 和 Mono 下载失败的离线方案
Wine 在初始化 prefix 的时候会尝试下载 Gecko 和 Mono。这两个包加起来大概 100 多 MB,在网络受限的环境里经常下载失败,然后每次启动程序都会弹窗提示。
离线方案其实很简单:Wine 会从特定的 URL 下载这些包,你只需要提前用其他方式下载好,放到 Wine 的缓存目录里,它就会直接使用本地文件。
# Wine 的缓存目录通常在 ~/.cache/wine/ # 把下载好的文件放进去,命名必须匹配 # wine-gecko-2.47.4-x86_64.msi # wine-mono-9.0.0-x86.msi文件名必须和 Wine 期望的完全一致,否则它还是会去下载。你可以通过WINEDEBUG=+msi来查看 Wine 具体在找哪个文件名。
提示:如果你在 ARM64 平台上,注意 Gecko 和 Mono 也有架构之分。x86-64 的 Wine 需要 x86-64 版本的 Gecko,不要下成 ARM64 的。
4.3 组件下载卡住时的排查链路
当你遇到组件下载卡住,不要急着重装,按照下面的链路排查:
- 先看 Wine 的输出日志,用
WINEDEBUG=+msi,+wininet启动,看它卡在哪个 URL 上。 - 用
curl手动访问那个 URL,确认是网络不通还是 URL 失效。 - 如果是 URL 失效(Wine 版本更新后 URL 会变),去 Wine 的源码里找当前版本对应的 URL。
- 如果是网络不通,走离线方案,手动下载放到缓存目录。
- 如果缓存目录放了还是不行,检查文件权限和文件名大小写。
这个链路我走过很多次,基本上 90% 的组件下载问题都能定位到具体原因。
5. 图形渲染与性能调优:DXMT 和 FEX-Emu 的配合
5.1 怎么判断图形问题出在 DXMT 还是 FEX-Emu
当 Windows 程序在 Madeira 里跑起来但画面异常时,第一步是判断问题出在图形转换层还是指令翻译层。方法很简单:用WINEDEBUG=+d3d看日志。如果日志里有大量的 D3D 调用失败或者格式不支持,那问题在 DXMT。如果日志正常但画面还是不对,那可能是 FEX-Emu 翻译某些指令时出了问题。
另一个判断方法是跑一个纯 CPU 的测试程序。如果纯 CPU 程序正常,带图形的程序异常,那基本可以锁定图形层。如果纯 CPU 程序都跑不起来,那就是 FEX-Emu 或者 Wine 的问题。
5.2 FEX-Emu 的性能调优参数
FEX-Emu 有几个环境变量可以显著影响性能:
FEX_TSOENABLED:控制是否启用 x86 的内存序模拟。开启后兼容性更好但性能下降,关闭后性能提升但某些多线程程序可能出问题。FEX_ROOTFS:指定根文件系统路径,影响 FEX-Emu 查找库文件的效率。FEX_CACHE:JIT 缓存的路径和大小。把缓存放在 SSD 上可以明显加快冷启动。
# 推荐的性能配置 export FEX_TSOENABLED=1 export FEX_CACHE=/tmp/fex-cache export FEX_ROOTFS=/opt/fex-rootfs实测下来,在 ARM64 平台上跑 x86-64 的轻量级 Windows 程序,FEX-Emu 的翻译开销大概在 20% 到 40% 之间。如果是计算密集型的程序,开销会更大,因为 JIT 翻译本身也消耗 CPU。
5.3 DXMT 的帧率优化和常见渲染问题
DXMT 在 macOS 上的表现整体不错,但有几个常见的渲染问题:
- 画面撕裂:需要在 DXMT 的配置里开启垂直同步,或者在 Metal 层做帧率限制。
- 纹理模糊:检查 DXMT 的纹理格式转换设置,有些格式在 Metal 上没有直接对应,需要软件转换。
- 着色器编译卡顿:DXMT 会缓存编译好的 Metal 着色器,第一次运行某个程序时会卡,后续就流畅了。缓存目录要确保可写。
# DXMT 的配置文件通常在这个位置 ~/.wine-madeira/drive_c/windows/dxmt.conf # 关键配置项 # vsync = true # shader_cache = true # max_frame_latency = 26. 从启动到可用:一个完整程序的调试实录
6.1 选择一个有代表性的测试程序
为了把前面的内容串起来,我拿一个实际的 Windows 程序做演示。选一个中等复杂度的:带图形界面、有网络请求、有本地文件读写。这样能覆盖 Wine 的 API 翻译、FEX-Emu 的指令翻译、DXMT 的图形转换三条链路。
假设这个程序叫TestApp.exe,是一个 x86-64 的 Windows 程序,用 Direct3D 11 渲染界面。
6.2 启动命令和日志收集
# 设置完整的调试环境 export WINEPREFIX=~/.wine-madeira export FEX_TSOENABLED=1 export WINEDEBUG=+loaddll,+d3d,+msi # 启动程序,把日志重定向到文件 wine TestApp.exe 2>&1 | tee /tmp/testapp.log启动后,先看日志里有没有明显的错误。常见的错误包括:
err:module:import_dll Library XXX.dll not found:缺少 DLL,需要安装对应的运行库。err:d3d:...:图形层问题,检查 DXMT 是否正确加载。err:msi:...:安装组件问题,检查 Gecko/Mono 是否就位。
6.3 分阶段验证:先跑通控制台,再跑图形
我的习惯是分阶段验证。先找一个纯控制台的 x86-64 Windows 程序,确认 FEX-Emu 和 Wine 的基础链路是通的。然后再上带图形的程序,确认 DXMT 工作正常。最后再上带网络和复杂 API 的程序。
这个顺序很重要,因为如果一上来就跑最复杂的程序,出了问题你根本不知道是哪一层的原因。分阶段验证可以把问题范围逐步缩小。
6.4 常见启动失败的排查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 提示找不到 exe | binfmt 未注册 | 检查 /proc/sys/fs/binfmt_misc |
| 启动后立即退出 | 缺少 DLL | WINEDEBUG=+loaddll 看日志 |
| 界面显示但无响应 | 图形层死锁 | 检查 DXMT 日志和 Metal 调用 |
| 网络功能不可用 | Wininet 配置问题 | 检查 Wine 的 winsock 设置 |
| 中文显示异常 | 字体或编码问题 | 参考第 4 节的乱码排查 |
7. 一些踩坑之后的经验之谈
7.1 不要混用不同来源的 Wine 和 DXMT
我见过有人从不同地方下载 Wine 和 DXMT,版本不匹配导致各种奇怪的问题。Wine 的 DLL 接口在不同版本之间会有变化,DXMT 是针对特定 Wine 版本编译的。混用轻则功能异常,重则直接崩溃。建议要么用同一个发行版打包的组件,要么自己从源码编译确保版本一致。
7.2 FEX-Emu 的缓存目录要定期清理
FEX-Emu 的 JIT 缓存会随着使用不断增长,有时候缓存损坏会导致程序行为异常。如果遇到莫名其妙的崩溃,先试试清空 FEX 缓存目录再跑。这个操作我至少做过几十次,每次都能解决一些"玄学"问题。
7.3 Wine prefix 不要复用
不同的 Windows 程序对 Wine prefix 的要求可能冲突。比如一个程序需要某个 DLL 的特定版本,另一个程序需要另一个版本。我的做法是每个重要程序单独一个 prefix,虽然占磁盘空间,但能避免大量的兼容性问题。
# 为不同程序创建独立 prefix WINEPREFIX=~/.wine-app1 wineboot --init WINEPREFIX=~/.wine-app2 wineboot --init7.4 日志是你的朋友,但要会看
Wine 和 FEX-Emu 的日志非常详细,但也很冗长。关键是要会用WINEDEBUG的通道过滤。比如只看 DLL 加载用+loaddll,只看图形用+d3d,只看网络用+wininet。不要一上来就开+all,那会产生几百 MB 的日志,根本没法看。
7.5 性能问题先看 CPU 占用分布
如果程序跑起来但很卡,先用top或htop看 CPU 占用。如果 FEX-Emu 的进程占用很高,说明指令翻译是瓶颈,可以尝试调整 FEX 的 TSO 设置。如果 Wine 的进程占用高,可能是 API 翻译的开销。如果 Metal 相关的进程占用高,那是图形层的问题。定位到瓶颈在哪一层,才能有针对性地优化。
8. 关于 Madeira 这类方案的适用边界
Madeira 这种把 Wine、FEX-Emu、DXMT 整合在一起的方案,本质上是在解决"异构平台上运行 Windows 程序"的问题。但它有明确的适用边界。
对于轻量级的 Windows 工具类程序,比如文本编辑器、小工具、老游戏,这类方案的体验已经相当可用了。对于中量级的程序,比如办公软件、图形编辑工具,需要做一些调优,但基本能用。对于重量级的程序,比如大型 3D 游戏、专业视频编辑软件,目前的方案还有明显的性能差距,尤其是涉及大量 SIMD 指令和复杂图形管线的场景。
另外,反作弊系统、内核级驱动、需要特定硬件访问的程序,在这类方案下基本跑不起来。这不是 Madeira 的问题,而是整个用户态兼容方案的天花板。
我在实际使用中的体会是,把这类方案当成"能用"而不是"好用"的工具。它的价值在于让你在 ARM 设备上也能访问一些只有 Windows 版本的程序,而不是替代原生的 Windows 环境。心态摆正了,遇到问题就不会那么焦虑。
最后分享一个小技巧:如果你在调试过程中遇到了无法定位的问题,试试把问题程序在一个干净的 Windows 虚拟机里跑一遍,确认程序本身没有问题。然后再回到 Madeira 环境里,用二分法逐步排除是 Wine、FEX-Emu 还是 DXMT 的问题。这个方法虽然笨,但非常有效。