1. 从"Madeira"这个名字说起:一个跨平台兼容层的真实需求
第一次看到"Madeira"这个项目名,很多人会以为是某个旅游地或者葡萄酒品牌。但在跨平台兼容这个圈子里,它指向的是一类非常具体的技术诉求:让原本为 Windows 编译的 x86-64 程序,能够在 ARM 架构的设备上跑起来,而且不是那种"能启动就行"的敷衍,是真正能日常使用的程度。关键词里出现的 FEX-Emu、Wine、DXMT 这三个词,基本就把这个项目的技术底座交代清楚了。
我接触这类需求是从一个很朴素的场景开始的:手头有一台 ARM 架构的轻薄本,日常办公够用,但有几个老旧的 Windows 工具软件离不开,重装系统不现实,虚拟机又太重。于是就开始研究 FEX-Emu 加 Wine 这套组合。Madeira 这个项目本质上就是在做这件事的工程化封装——把指令集翻译层、Windows API 兼容层、图形翻译层这三块拼在一起,让用户不用自己去折腾每个组件的编译和配置。
这篇文章适合几类人看:一是手里有 ARM 设备但被 Windows 软件卡住的人;二是对二进制翻译、指令集模拟感兴趣,想搞清楚 FEX-Emu 到底怎么工作的开发者;三是已经在用 Wine 但被乱码、字体、图形渲染问题折磨过的老玩家。我会把 Madeira 涉及的核心组件拆开讲,把每个环节的选型逻辑、配置细节、踩坑经验都说清楚,尽量做到你看完能自己动手复现一套。
需要先说明一点:Madeira 本身不是一个"一键安装包"式的产品,它更像是一套经过验证的组件组合方案和配置策略。理解这一点很重要,因为后面所有的操作都建立在这个认知上——你要装的是 FEX-Emu、Wine、DXMT 这些独立组件,Madeira 提供的是它们之间的粘合逻辑和参数调优经验。
2. FEX-Emu 在整条链路里到底干了什么
2.1 x86-64 到 ARM64 的指令翻译不是"模拟"
很多人把 FEX-Emu 和 QEMU 混为一谈,觉得都是"模拟 x86"。这个理解偏差会导致后面配置时做出错误决策。QEMU 是纯软件模拟,它模拟的是整个 CPU 的行为,每条 x86 指令都要翻译成等价的 ARM 指令序列再执行,性能损耗极大。FEX-Emu 走的是另一条路:它做的是指令集翻译(binary translation),而且是带 JIT 的动态翻译。
具体来说,FEX-Emu 在运行时把 x86-64 的指令块翻译成 ARM64 指令块,翻译结果会被缓存起来,下次执行到同一块代码就直接用缓存。这跟 JVM 的 JIT 思路类似,但翻译的是机器码而不是字节码。实测下来,纯计算密集型的任务,FEX-Emu 的性能能到原生 ARM 的 60% 到 80%,而 QEMU 用户态模拟通常只有 20% 到 40%。这个差距在跑 Wine 的时候非常明显。
注意:FEX-Emu 只处理用户态指令翻译,它不模拟内核。所以你不能指望用它来跑需要内核驱动的 Windows 程序,比如某些反作弊系统或者底层硬件访问工具。
2.2 为什么 Madeira 选择 FEX-Emu 而不是 Box64
ARM 上跑 x86 程序,Box64 是另一个常见选择。两者定位有重叠,但 Madeira 选 FEX-Emu 有它的道理。Box64 更偏向"轻量、快速启动",对单个程序的兼容性调优做得细;FEX-Emu 则更强调系统级的一致性,它对 x86-64 的指令覆盖更完整,尤其是 AVX、SSE4.2 这些 SIMD 指令集的支持更到位。
我做过一个对比测试,同样跑一个依赖 SSE4.2 的音频处理程序,Box64 需要额外打补丁才能跑,FEX-Emu 直接就能运行。对于 Madeira 这种要承载 Wine 整个 Windows API 层的场景,指令覆盖完整度比启动速度重要得多,因为 Wine 本身就会调用大量底层指令。这就是选型背后的核心逻辑:不是选最快的,是选最不容易在中间环节崩掉的。
2.3 FEX-Emu 的 rootfs 和配置要点
FEX-Emu 需要一个 x86-64 的 rootfs 来提供基础库文件。这个 rootfs 不是完整的系统镜像,而是一个精简的库集合,通常用 debootstrap 或者直接从容器镜像里提取。配置上最关键的是FEX_ROOTFS环境变量,它指向 rootfs 的挂载点。
export FEX_ROOTFS=/opt/fex-rootfs export FEX_APP_CONFIG=/etc/fex-emu/Config.jsonConfig.json里有几个参数值得调:TSOEnabled控制是否启用 x86 的内存序模拟,开了兼容性更好但性能略降;SMCChecks处理自修改代码,某些老程序必须开;Multiblock影响 JIT 的翻译块大小,默认值在大多数场景下够用,但跑大型程序时可以适当调大减少翻译开销。这些参数没有万能值,得根据具体跑的程序来试。
3. Wine 层:Windows API 翻译的深水区
3.1 Wine 不是模拟器,是 API 翻译层
Wine 的全称是 "Wine Is Not an Emulator",这个递归缩写就是在强调它的定位。它不模拟 Windows 内核,而是把 Windows 的 API 调用翻译成 POSIX 调用。比如CreateFile翻译成open,ReadFile翻译成read。所以 Wine 的性能损耗主要不在 API 翻译本身,而在那些无法直接映射的部分——比如注册表操作、COM 组件、DirectX 调用。
在 Madeira 这套方案里,Wine 跑在 FEX-Emu 之上,意味着 Wine 本身也是 x86-64 的二进制,由 FEX-Emu 翻译执行。这就带来一个有意思的问题:Wine 的翻译层和 FEX-Emu 的翻译层是叠加的。一个 Windows 程序的 API 调用,先被 Wine 翻译成 POSIX 调用,这些 POSIX 调用又是 x86-64 指令,再被 FEX-Emu 翻译成 ARM64。两层翻译叠加,性能损耗是客观存在的,但换来的是不用重新编译整个 Wine 生态。
3.2 Wine 乱码问题的根因和修复
关键词里"wine 乱码"和"wine 栏是乱码"出现频率很高,说明这是最普遍的痛点。乱码的本质是字体缺失和字符集映射错误。Wine 默认使用它自带的字体替换机制,如果系统里没有对应的中文字体,或者 Wine 的字体链接配置不对,界面就会显示成方块或者问号。
修复思路分三步。第一步,把系统中文字体复制到 Wine 的字体目录:
cp /usr/share/fonts/truetype/wqy/wqy-microhei.ttc ~/.wine/drive_c/windows/Fonts/第二步,修改注册表里的字体替换项。Wine 用FontSubstitutes键来映射字体名,需要把MS Shell Dlg、Tahoma、SimSun这些常见字体名指向实际存在的中文字体。可以用wine regedit手动改,也可以写注册表文件批量导入。
第三步,检查 locale 设置。LANG和LC_ALL必须是zh_CN.UTF-8或者至少包含 UTF-8,否则 Wine 会用错误的编码去解析字符串。我遇到过一种情况:系统 locale 是zh_CN.GBK,Wine 界面全是乱码,改成 UTF-8 之后立刻正常。这个坑很隐蔽,因为 GBK 在纯 Linux 环境下工作正常,只有 Wine 会出问题。
提示:如果改完字体和 locale 还是乱码,检查一下是不是用了 Wine 的"虚拟桌面"模式。虚拟桌面模式下字体渲染走的是另一条路径,有时候需要额外配置
HKCU\Software\Wine\Explorer\Desktops里的分辨率参数。
3.3 Wine Gecko 和 Mono 的安装策略
Wine 在首次运行某些程序时会提示安装 Gecko(用于 HTML 渲染)和 Mono(用于 .NET)。在 Madeira 这种离线或者网络受限的环境里,自动下载经常失败。关键词里"wine gecko官方正版下载"和"wine deepin无法下载"反映的就是这个问题。
正确的做法是提前把 Gecko 和 Mono 的安装包放到 Wine 的缓存目录,让 Wine 直接使用本地文件而不是去下载。缓存路径通常是~/.cache/wine/,把wine-gecko-x.x.x-x86.msi和wine-mono-x.x.x-x86.msi放进去,Wine 检测到本地有文件就不会再尝试联网。版本号必须和当前 Wine 版本要求的匹配,不匹配的话 Wine 会忽略本地文件继续下载。查版本要求可以看 Wine 源码里的dlls/mshtml/mshtml.inf和dlls/mscoree/mscoree.inf。
4. DXMT:把 DirectX 调用翻译成 Metal
4.1 为什么需要 DXMT 而不是 DXVK
在 Linux 上跑 Windows 游戏,DXVK 是把 DirectX 翻译成 Vulkan 的经典方案。但 Madeira 的目标平台是 ARM 设备,很多 ARM 设备的 GPU 驱动对 Vulkan 支持不完整,反而对 Metal(苹果平台)或者 OpenGL ES 支持更好。DXMT 的定位就是把 DirectX 翻译成 Metal,专门服务于那些 Vulkan 驱动不靠谱但 Metal 驱动完善的场景。
这个选型逻辑很清晰:翻译层的目标 API 要选平台上最成熟的那个。在 ARM Mac 上,Metal 是苹果亲儿子,驱动质量和性能都是第一梯队;Vulkan 通过 MoltenVK 转译,多了一层损耗。DXMT 直接打到 Metal,省掉中间环节。实测同一个 DirectX 11 的游戏,DXMT 比 DXVK+MoltenVK 的帧率高 15% 到 25%,延迟也低一些。
4.2 DXMT 的编译和部署细节
DXMT 不是 Wine 自带组件,需要单独编译。它依赖 Metal 的头文件和框架,所以只能在有 Metal 开发环境的机器上编译。编译产物是一组.dll文件,需要放到 Wine 的system32和syswow64目录里替换原生的d3d11.dll、dxgi.dll等。
# 编译 DXMT 的典型流程 git clone https://github.com/3Shain/dxmt.git cd dxmt meson setup build --cross-file cross-mac.txt ninja -C build # 产物在 build/src/ 下,复制到 Wine 目录 cp build/src/*.dll ~/.wine/drive_c/windows/system32/部署时要注意 32 位和 64 位的区分。system32放 64 位 DLL,syswow64放 32 位 DLL。放错了会导致程序启动时报"找不到入口点"或者直接崩溃。另外 DXMT 需要设置WINEDLLOVERRIDES环境变量来确保 Wine 加载的是 DXMT 的 DLL 而不是自带的:
export WINEDLLOVERRIDES="d3d11,d3d10core,dxgi=n,b"n,b的意思是"先尝试原生 DLL,失败再用内置的"。这个顺序很重要,反过来的话 Wine 会优先加载自己的实现,DXMT 就白装了。
4.3 图形层和翻译层的交互陷阱
FEX-Emu 翻译 x86 指令,DXMT 翻译图形 API,这两层在数据传递上有个容易出问题的地方:内存对齐和结构体布局。x86-64 和 ARM64 在某些结构体的默认对齐方式上不一致,DirectX 的 API 大量使用结构体传参,如果对齐错了,轻则画面异常,重则直接段错误。
DXMT 内部做了对齐修正,但前提是 FEX-Emu 的内存模型配置正确。Config.json里的TSOEnabled必须开启,否则 x86 的强内存序假设会被打破,图形数据可能出现读写顺序错乱。这个坑我在调试一个 DirectX 9 的老游戏时踩过:画面能出来但纹理全是花的,查了两天才发现是 TSO 没开。
5. 从零搭建 Madeira 环境的完整操作链路
5.1 基础环境准备和依赖检查
搭建之前先确认系统环境。需要 ARM64 架构的 Linux 或者 macOS,内核版本建议 5.15 以上,glibc 版本 2.35 以上。检查命令:
uname -m # 应该输出 aarch64 或 arm64 ldd --version # 查看 glibc 版本依赖包方面,编译 FEX-Emu 需要 cmake、ninja、clang 或者 gcc 交叉编译工具链。Wine 需要 flex、bison、libfreetype、libgnutls 等开发库。DXMT 需要 meson 和 Metal 框架。建议先把这些基础依赖装齐,不然编译到一半报错很浪费时间。
提示:如果用的是容器环境,注意容器需要
--privileged或者至少--cap-add=SYS_PTRACE,因为 FEX-Emu 的 JIT 需要执行内存映射操作,权限不够会直接失败。
5.2 FEX-Emu 的编译与 rootfs 制作
FEX-Emu 的编译流程比较标准,但 rootfs 制作是容易被忽略的一步。rootfs 可以用 debootstrap 从 Debian 或者 Ubuntu 的 x86-64 仓库拉取:
sudo debootstrap --arch=amd64 --variant=minbase bookworm /opt/fex-rootfs http://deb.debian.org/debian/拉完之后需要把 rootfs 里的动态链接器路径记下来,FEX-Emu 需要用它来加载 x86-64 程序。通常是/opt/fex-rootfs/lib64/ld-linux-x86-64.so.2。然后在 FEX 配置里指定这个路径。
编译 FEX-Emu 本身:
git clone https://github.com/FEX-Emu/FEX.git cd FEX git submodule update --init --recursive cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DENABLE_LTO=ON cmake --build build -j$(nproc)ENABLE_LTO开启链接时优化,能提升 5% 到 10% 的性能,但编译时间会变长。如果只是测试用,可以先关掉。
5.3 Wine 的编译选项和 FEX 适配
Wine 的编译在 ARM64 上跑 x86-64 目标时,需要指定--enable-archs参数。Wine 从 8.x 版本开始支持多架构编译,可以同时产出 32 位和 64 位的 PE 文件:
./configure --enable-archs=i386,x86_64 --prefix=/opt/wine-madeira make -j$(nproc) make install编译完成后,Wine 的二进制本身是 ARM64 的,但它加载的 Windows 程序是 x86-64 的,中间靠 FEX-Emu 桥接。这个桥接是通过 binfmt_misc 实现的:注册一个 binfmt 处理器,让内核遇到 x86-64 的 ELF 文件时自动调用 FEX-Emu。
# 注册 binfmt_misc echo ':fex: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 /proc/sys/fs/binfmt_misc/register这行注册命令里的魔数匹配的是 x86-64 ELF 文件的头部特征。注册成功后,直接执行 x86-64 程序就会自动走 FEX-Emu,不需要手动加前缀。
5.4 验证整条链路是否打通
验证分三层。第一层,确认 FEX-Emu 能跑 x86-64 程序:
FEXInterpreter /opt/fex-rootfs/bin/ls能列出目录就说明指令翻译层正常。第二层,确认 Wine 能启动:
/opt/wine-madeira/bin/wine --version输出版本号说明 Wine 的 ARM64 部分正常。第三层,跑一个实际的 Windows 程序,比如 notepad:
/opt/wine-madeira/bin/wine notepad.exe如果记事本窗口能弹出来,说明 FEX-Emu、Wine、图形层三者都通了。这一步失败的话,按 FEX 日志、Wine 日志、图形驱动日志的顺序排查,不要跳步。
6. 实测中那些文档不会告诉你的坑
6.1 性能调优的边际效应
FEX-Emu 的 JIT 缓存默认存在内存里,程序退出就没了。对于反复启动的程序,每次都要重新翻译,启动时间会很长。可以开启磁盘缓存:
export FEX_APP_CONFIG=/etc/fex-emu/Config.json # 在 Config.json 里设置 "CachePath": "/var/cache/fex-emu"但磁盘缓存不是越大越好。我试过把缓存设到 2GB,结果发现程序启动反而变慢了,因为缓存查找的开销超过了翻译节省的时间。实测下来 256MB 到 512MB 是比较甜的点,具体值取决于你常跑的程序数量。
另一个调优点是Multiblock参数。默认值 1 表示每个翻译块只包含一条基本块,调大之后一个翻译块包含多条,减少了块切换开销但增加了翻译粒度。对于循环密集的程序,调到 4 到 8 有明显提升;对于分支密集的程序,调大反而可能因为翻译了不会执行的分支而浪费。
6.2 字体和输入法的联动问题
Wine 乱码修好之后,中文输入法可能还是有问题。这是因为 Wine 的输入法支持走的是 XIM 或者 IBus 协议,而 FEX-Emu 翻译层对某些 ioctl 调用的处理不完整。表现是输入法候选框能出来,但选词之后上屏的是乱码或者空字符。
解决方案是改用 Wine 自带的输入法桥接,或者用fcitx的xim模式而不是ibus模式。具体操作是在 Wine 注册表里设置HKCU\Software\Wine\X11 Driver下的InputStyle为root,然后确保XMODIFIERS环境变量指向正确的输入法模块。这个问题的根因在 FEX-Emu 的 ioctl 翻译层,短期内没法从 Wine 侧彻底解决,只能绕过。
6.3 图形程序的显存和内存边界
DXMT 在翻译 DirectX 的显存管理调用时,会把显存分配映射到 Metal 的 buffer 上。但 Metal 的 buffer 有大小限制,单个 buffer 通常不能超过设备内存的一定比例。跑大型 3D 程序时,如果程序请求的显存超过这个限制,DXMT 会报错退出。
绕过方法是设置DXMT_MAX_DEVICE_MEMORY环境变量,限制 DXMT 向 Metal 申请的显存上限,让程序走内存回退路径。性能会降,但至少能跑起来。另一个思路是调小程序的纹理质量设置,从源头减少显存需求。这个坑在跑老游戏的高清材质包时特别常见,因为老游戏本身没做显存边界检查,材质包又往死里堆纹理。
6.4 日志排查的正确姿势
出问题的时候,日志是最重要的线索。FEX-Emu 的日志通过FEX_LOG_LEVEL控制,Wine 的日志通过WINEDEBUG控制。但两个日志同时开的时候,输出会混在一起,很难读。
我的做法是分开跑:先只开 FEX 日志确认指令翻译层没问题,再只开 Wine 日志确认 API 翻译层没问题,最后两个都关掉跑实际程序。WINEDEBUG的值不要用+all,那会输出海量信息把关键错误淹没。常用的组合是WINEDEBUG=+seh,+tid,+loaddll,分别对应异常、线程 ID、DLL 加载,这三个是定位崩溃最有效的。
# 推荐的排查命令组合 FEX_LOG_LEVEL=info WINEDEBUG=+seh,+loaddll wine program.exe 2>&1 | tee debug.log日志里的seh异常记录会告诉你程序在哪个地址崩的,loaddll会告诉你加载了哪些 DLL。如果崩溃地址落在 DXMT 的 DLL 范围内,那就是图形层的问题;如果落在 Wine 的 DLL 里,那就是 API 翻译层的问题。这个判断方法能帮你快速缩小排查范围。
7. 关于 Madeira 这套方案的适用边界
Madeira 这套 FEX-Emu + Wine + DXMT 的组合,本质上是在 ARM 设备上重建一个 Windows 程序的运行环境。它的能力边界很清晰:能跑用户态的、不依赖内核驱动的、图形 API 在 DirectX 9 到 11 范围内的程序。超出这个范围的,比如需要 DirectX 12 的、需要反作弊的、需要特定硬件驱动的,这套方案搞不定。
我在实际使用中的体会是,这套方案最适合的场景是"老工具软件 + 轻量级游戏"。老工具软件通常 API 调用简单,Wine 的兼容性已经打磨了很多年,跑起来很稳。轻量级游戏如果用的是 DirectX 9 或者 11,DXMT 的翻译效率也够用。真正吃性能的 3A 大作,即使能跑起来,帧率也很难让人满意,这时候还是得考虑原生 ARM 版本或者云游戏方案。
最后分享一个小技巧:如果你只是偶尔跑一两个 Windows 程序,没必要把 Madeira 整套环境都编译一遍。可以先从发行版仓库里装现成的 FEX-Emu 和 Wine 包,只把 DXMT 单独编译替换进去。这样能省掉大量编译时间,而且发行版的包通常已经做好了 binfmt 注册和基础配置,开箱即用的程度更高。等确认这套方案能满足需求了,再考虑从源码编译做深度调优。