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 而不是 Box64ARM 上跑 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翻译成openReadFile翻译成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.GBKWine 界面全是乱码改成 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 调用翻译成 Metal4.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 比 DXVKMoltenVK 的帧率高 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 位 DLLsyswow64放 32 位 DLL。放错了会导致程序启动时报找不到入口点或者直接崩溃。另外 DXMT 需要设置WINEDLLOVERRIDES环境变量来确保 Wine 加载的是 DXMT 的 DLL 而不是自带的export WINEDLLOVERRIDESd3d11,d3d10core,dxgin,bn,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-addSYS_PTRACE因为 FEX-Emu 的 JIT 需要执行内存映射操作权限不够会直接失败。5.2 FEX-Emu 的编译与 rootfs 制作FEX-Emu 的编译流程比较标准但 rootfs 制作是容易被忽略的一步。rootfs 可以用 debootstrap 从 Debian 或者 Ubuntu 的 x86-64 仓库拉取sudo debootstrap --archamd64 --variantminbase 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_TYPERelease -DENABLE_LTOON 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-archsi386,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那会输出海量信息把关键错误淹没。常用的组合是WINEDEBUGseh,tid,loaddll分别对应异常、线程 ID、DLL 加载这三个是定位崩溃最有效的。# 推荐的排查命令组合 FEX_LOG_LEVELinfo WINEDEBUGseh,loaddll wine program.exe 21 | 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 或者 11DXMT 的翻译效率也够用。真正吃性能的 3A 大作即使能跑起来帧率也很难让人满意这时候还是得考虑原生 ARM 版本或者云游戏方案。最后分享一个小技巧如果你只是偶尔跑一两个 Windows 程序没必要把 Madeira 整套环境都编译一遍。可以先从发行版仓库里装现成的 FEX-Emu 和 Wine 包只把 DXMT 单独编译替换进去。这样能省掉大量编译时间而且发行版的包通常已经做好了 binfmt 注册和基础配置开箱即用的程度更高。等确认这套方案能满足需求了再考虑从源码编译做深度调优。