
1. 项目缘起为什么要在 Linux 上折腾 Windows 应用兼容层第一次看到 Madeira 这个项目名很多人会以为是某个旅游地或者饮料品牌。但在我们这群长期混迹于 Linux 桌面环境、又不得不偶尔跑几个 Windows 专属工具的人眼里Madeira 代表的是一个非常具体的方向在非 Windows 平台上用一套相对轻量的方案把 x86-64 的 Windows 应用跑起来。它和 FEX-Emu、Wine、DXMT 这几个关键词绑在一起本质上是在解决同一个老问题——我手头只有一台跑着 Linux 的机器但某个刚需软件只有 Windows 版怎么办。这个需求听起来小众实际上非常普遍。做嵌入式的要用某款只出 Windows 版的烧录工具做财务的要用某个老旧的报税客户端做设计的可能离不开某个特定版本的看图软件。过去大家的解法无非是双系统、虚拟机、或者干脆再买一台 Windows 机器。但这三种方案都有明显的代价双系统切换麻烦虚拟机吃资源且图形性能差再买一台机器则是纯粹的成本浪费。Madeira 这类项目想做的就是把这三种方案的痛点都绕开让 Windows 应用像本地应用一样直接跑在当前的桌面环境里。需要先说明一点Madeira 并不是一个孤立的新发明它更像是把几条成熟技术路线重新组合、重新打包的一次工程实践。理解它之前得先理解它脚下的三块基石Wine 负责把 Windows 的系统调用翻译成 POSIX 调用FEX-Emu 负责处理指令集层面的差异DXMT 负责把 Direct3D 的图形调用转到 Metal 或 Vulkan 上。这三者各管一段合起来才能让一个 Windows 程序从能启动走到能用。适合读这篇内容的人大概分三类。第一类是 Linux 桌面用户手里有一两个离不开的 Windows 软件想找个比虚拟机更轻的替代方案。第二类是对兼容层技术本身感兴趣的开发者想搞清楚 Wine 这类项目到底是怎么把两套完全不同的系统 API 对上号的。第三类是负责企业内部桌面环境的技术人员需要评估在统信、麒麟这类国产系统上批量部署 Windows 兼容组件的可行性。不管你是哪一类接下来的内容都会从原理讲到实操尽量把每个环节的为什么说清楚。2. 核心组件拆解Wine、FEX-Emu、DXMT 各自在干什么2.1 Wine 不是模拟器它是翻译层很多人第一次接触 Wine 时会误以为它是个模拟器这个误解会导致后面一系列判断失误。Wine 的全称是 Wine Is Not an Emulator它做的事情是在运行时把 Windows 的 API 调用动态翻译成当前系统能理解的调用。举个生活化的例子一个只会说方言的人要和只会说普通话的人交流模拟器的做法是找个配音演员把方言整段录下来再放普通话版本而 Wine 的做法是找个同声传译你说一句我翻一句。这个区别带来的直接后果是Wine 不需要模拟 CPU 指令所以性能损耗主要来自 API 翻译的开销而不是指令执行的模拟。对于原生 x86-64 的 Windows 程序在 x86-64 的 Linux 上跑Wine 的性能可以做到接近原生。但如果目标平台是 ARM 架构比如某些国产 ARM 芯片的笔记本那就需要额外的指令翻译层这时候 FEX-Emu 就登场了。Wine 的另一个关键点是它的实现方式。它提供了一套名为 Winelib 的开发库允许开发者把 Windows 源码直接编译成 Linux 原生程序。同时它维护了一个庞大的 DLL 实现集合每个 Windows 系统 DLL 在 Wine 里都有一个对应的开源实现。当你运行一个 exe 时Wine 会加载这些替代 DLL拦截程序对系统功能的调用。这就是为什么有些程序在 Wine 下跑得完美有些却直接崩溃——取决于它用到的那些 API 有没有被完整实现。2.2 FEX-Emu 解决的是指令集鸿沟FEX-Emu 的定位非常明确在 ARM64 设备上运行 x86-64 的 Linux 程序。注意这里说的是 Linux 程序不是 Windows 程序。它和 Wine 是上下游关系——FEX-Emu 先把 x86-64 指令翻译成 ARM64 指令让程序能在 ARM 上跑起来然后 Wine 再把 Windows API 调用翻译成 Linux 调用。两层翻译叠加才能让一个 Windows 的 x86-64 程序在 ARM 的 Linux 上运行。FEX-Emu 的技术路线和传统的全量模拟器不同。它采用的是动态二进制翻译加缓存的策略程序第一次执行某段 x86 指令时FEX 把它翻译成对应的 ARM64 指令并缓存起来下次再执行到同一段就直接用缓存。这个思路和 JIT 编译器很像好处是热代码的执行效率会随着运行时间提升。实测下来对于计算密集型的程序FEX-Emu 的翻译开销可以控制在可接受范围内但对于频繁调用系统调用的程序开销会明显一些。这里有个容易踩的坑FEX-Emu 对 x86-64 指令集的支持是分等级的。基础的整数运算、浮点运算支持得很好但一些较新的扩展指令集比如 AVX-512支持就不那么完整。如果你的目标程序编译时开启了这些扩展指令运行时可能会遇到非法指令错误。解决办法通常是在编译时降级指令集或者找不带这些扩展的版本。2.3 DXMT 把 Direct3D 调用转到 Metal 上图形是 Windows 应用兼容里最难啃的骨头之一。Windows 上的游戏和图形软件大量使用 Direct3D而 Linux 和 macOS 上根本没有 Direct3D 的原生实现。DXMT 做的事情就是把 Direct3D 的调用翻译成 Metal 的调用主要面向 Apple Silicon 平台。为什么是 Metal 而不是 Vulkan因为在 Apple 的生态里Metal 是官方主推的图形 API驱动层面的优化最到位。Vulkan 在 macOS 上是通过 MoltenVK 这层转译实现的多一层转译就多一层开销。DXMT 直接对接 Metal省掉了中间环节理论上效率更高。当然代价是它只能在 Apple 平台上用Linux 平台上的 Direct3D 转译通常走 DXVK 到 Vulkan 这条路。DXMT 目前主要覆盖 Direct3D 11 和部分 Direct3D 12 的功能。对于老一点的 Direct3D 9 程序通常还是走 Wine 自带的 D3D 实现或者 DXVK。这里的选择逻辑是先看程序用哪个版本的 D3D再决定用哪套转译方案。用错了方案轻则画面异常重则直接闪退。3. 从零搭建一套可复现的兼容环境配置流程3.1 环境准备与依赖梳理在动手之前先把基础环境理清楚。假设你用的是一台 ARM64 的 Linux 设备目标是跑一个 x86-64 的 Windows 程序。整个链路是Windows 程序 → WineAPI 翻译→ FEX-Emu指令翻译→ Linux 内核 → ARM64 硬件。第一步是确认系统架构和内核版本。用uname -m看架构用uname -r看内核版本。FEX-Emu 对内核版本有要求太老的版本可能缺少必要的特性支持。一般来说5.15 以上的内核比较稳妥。第二步是安装 FEX-Emu。不同发行版的包名不一样Debian 系通常叫fex-emuArch 系在 AUR 里。如果发行版仓库里没有就得从源码编译。编译 FEX-Emu 需要 CMake、Clang 或者 GCC 的新版本以及一些架构相关的头文件。编译过程比较吃时间建议在性能好一点的机器上做。第三步是配置 Wine。这里有个关键选择用系统仓库里的 Wine还是用第三方打包的版本。系统仓库的版本稳定但可能偏旧第三方版本新但可能有兼容性问题。我的建议是先用系统仓库的版本跑一遍确认基础功能正常再考虑换版本。第四步是处理 FEX-Emu 和 Wine 的配合。FEX-Emu 提供了一个名为FEXBash的环境进去之后所有命令都会自动经过 x86-64 到 ARM64 的翻译。Wine 需要在这个环境里运行才能正确处理 x86-64 的 Windows 程序。如果直接在普通 shell 里跑 Wine它会尝试用 ARM64 的方式加载 x86-64 的 exe结果必然是失败。3.2 Wine 前缀的创建与调优Wine 的前缀prefix是它管理程序运行环境的核心概念。你可以把它理解成一个虚拟的 C 盘里面包含了注册表、系统目录、以及每个程序独立的配置。默认情况下Wine 会在~/.wine下创建一个前缀但更推荐的做法是为每个程序或每类程序创建独立的前缀。为什么因为不同程序对 Windows 版本、DLL 覆盖、字体配置的要求可能完全不同。一个程序需要模拟 Windows 7另一个需要 Windows 10放在同一个前缀里就会打架。独立前缀的好处是互不干扰坏处是每个前缀都要占一份磁盘空间而且初次创建时要初始化一遍。创建前缀的命令是WINEPREFIX/path/to/prefix wineboot。这里的wineboot会初始化前缀生成注册表和目录结构。初始化完成后可以用winecfg打开配置界面调整 Windows 版本、显示设置、DLL 覆盖等。在 ARM64 环境下这些命令都要在 FEXBash 里执行。DLL 覆盖是调优的重点。Wine 对某些系统 DLL 的实现可能不如原生 Windows 完整这时候可以用winetricks安装原生的 DLL 替换。常见的需要替换的包括msvcrt、vcrun系列、dotnet系列。但要注意替换 DLL 不是越多越好有些 DLL 替换后反而会引入新的问题。原则是遇到具体问题时再针对性替换不要一上来就装一大堆。3.3 图形栈的配置与选择图形配置取决于你的目标程序用什么图形 API。如果是 Direct3D 11 或 12 的程序在 Apple 平台上优先考虑 DXMT在 Linux 平台上优先考虑 DXVK。如果是 OpenGL 程序Wine 自带的实现通常够用但可能需要调整一些注册表项。DXVK 的安装方式是把它的 DLL 文件复制到前缀的system32目录下然后在winecfg的库设置里把对应的 DLL 设为原生。DXVK 需要 Vulkan 驱动支持所以要先确认系统装了正确的 Vulkan 驱动。用vulkaninfo可以查看 Vulkan 的支持情况。DXMT 的安装类似但它是针对 Metal 的所以只在 macOS 上有意义。安装后同样需要在winecfg里配置 DLL 覆盖。DXMT 的配置项比 DXVK 多一些因为它要处理 Metal 的一些特性比如着色器编译、内存管理等。这些配置项通常在 DXMT 的文档里有说明按需调整即可。这里有个实测经验图形栈的配置对性能影响很大但不要盲目追求最新版本。有时候新版本引入了新特性但也可能引入新的 bug。如果当前版本能稳定运行你的目标程序就没必要频繁升级。我自己的做法是一旦某个组合跑通了就把版本号记下来后续除非遇到必须升级的理由否则不动。4. 常见故障与排查从乱码到闪退的实战记录4.1 Wine 乱码问题的根因与解法Wine 乱码是搜索热词里出现频率很高的问题具体表现是程序界面上的中文显示成方块或者问号。这个问题的根因通常有两个字体缺失和编码不匹配。字体缺失比较好理解Wine 的前缀里默认只有很少的字体中文字体需要额外安装。解决办法是把系统的中文字体复制到前缀的drive_c/windows/Fonts目录下或者用winetricks安装corefonts和cjkfonts。复制字体后可能还需要在注册表里配置字体替换让 Wine 知道用哪个字体来显示中文。编码不匹配则更隐蔽一些。有些程序在读取配置文件或者资源文件时假设系统使用某个特定的代码页而 Wine 的默认代码页可能和它期望的不一致。这时候需要在winecfg里调整区域设置或者在启动程序时设置LANG环境变量。实测下来把LANG设为zh_CN.UTF-8能解决大部分编码相关的问题。还有一个容易被忽略的点某些程序的乱码是因为它自带的字体文件没有被正确加载。这种情况下可以尝试把程序目录下的字体文件复制到前缀的字体目录或者在注册表里手动注册这些字体。4.2 程序启动失败的分层排查法程序启动失败的原因可能分布在链路的任何一层所以排查时要从底层往上逐层确认。我的习惯是分四层来看第一层是硬件和内核层。确认 CPU 架构、内核版本、必要的内核模块是否加载。这一层的问题通常表现为 FEX-Emu 本身无法启动或者启动后立即崩溃。第二层是指令翻译层。确认 FEX-Emu 能否正确翻译目标程序的指令。这一层的问题表现为程序在启动初期就报非法指令错误或者翻译后的代码行为异常。排查方法是看 FEX-Emu 的日志它会记录翻译过程中遇到的指令和可能的错误。第三层是 API 翻译层。确认 Wine 能否正确加载程序依赖的 DLL以及这些 DLL 的调用是否被正确翻译。这一层的问题表现为程序启动后报缺少某个 DLL或者调用某个 API 时出错。排查方法是看 Wine 的调试输出用WINEDEBUGloaddll可以看到 DLL 的加载过程。第四层是程序自身层。确认程序本身的配置、依赖、运行环境是否满足。这一层的问题表现为程序能启动但功能异常或者在某些特定操作时崩溃。排查方法是看程序自己的日志以及用调试器附加到进程上看崩溃点。这个分层排查法的好处是每一层的问题都有明确的特征和对应的排查工具不会眉毛胡子一把抓。实际用下来大部分问题都能在前三层定位到。4.3 性能调优的几个关键参数兼容层的性能调优空间比原生环境小但也不是完全没有。几个比较有效的调优点是FEX-Emu 的翻译缓存大小。默认的缓存可能不够大导致热代码被反复翻译。可以在配置里调大缓存减少重复翻译的开销。具体调多大取决于程序的工作集大小一般调到几百 MB 比较稳妥。Wine 的图形同步设置。Wine 默认的图形同步策略可能不是最优的可以在注册表里调整CSMT相关的项。开启 CSMT 后图形命令会在单独的线程里处理能提升一些帧率但也可能引入输入延迟。这个需要根据具体程序来权衡。CPU 亲和性设置。在 ARM64 设备上大小核的调度策略会影响翻译层的性能。把 Wine 和 FEX-Emu 的进程绑定到大核上能减少翻译延迟。用taskset可以设置亲和性。内存分配策略。FEX-Emu 和 Wine 都会申请不少内存如果系统的内存压力大翻译缓存的命中率会下降。适当调整vm.swappiness和vm.overcommit_memory能改善这种情况。5. 生态现状与选型建议不同场景下怎么选方案5.1 国产系统上的兼容组件选择在统信 UOS、麒麟这类国产系统上Windows 兼容组件的选择相对有限。这些系统通常自带一个基于 Wine 的兼容层但版本和配置可能比较保守。如果自带的组件能满足需求优先用它因为它的集成度最好出问题也容易找到支持。如果自带组件不够用可以考虑手动安装新版本的 Wine。但要注意国产系统上的依赖库版本可能和主流发行版有差异直接装官方包可能会遇到依赖冲突。比较稳妥的做法是从源码编译或者找针对该系统的第三方打包。麒麟 wine 助手这类工具的出现说明这个需求是真实存在的。这类工具通常把 Wine 的安装、配置、常用运行库的部署打包成一键操作降低了使用门槛。但一键工具的问题是出问题时排查起来更麻烦因为你不清楚它到底做了什么。我的建议是先用一键工具跑通再逐步了解它背后的配置这样既省事又不失掌控。5.2 不同硬件平台的方案差异x86-64 的 Linux 上方案最简单直接装 Wine不需要 FEX-Emu。图形方面DXVK 是首选兼容性和性能都比较成熟。ARM64 的 Linux 上需要 FEX-Emu 加 Wine 的组合。这个组合的成熟度比纯 x86-64 方案低一些遇到问题的概率更高。图形方面如果设备有 Vulkan 支持DXVK 仍然可用如果没有可能要用 Wine 自带的 D3D 实现性能会差一些。Apple Silicon 的 macOS 上方案是 FEX-Emu 加 Wine 加 DXMT。这个组合的图形性能在三者中可能是最好的因为 Metal 的驱动优化到位。但 macOS 的系统限制比较多比如对内核扩展的限制可能会影响某些底层功能的实现。5.3 什么情况下不值得折腾兼容层不是万能的有些情况下折腾的成本远高于收益。比如程序依赖内核级驱动。这类程序在 Wine 下基本跑不起来因为 Wine 不提供内核态的模拟。典型的例子是杀毒软件、虚拟化软件、某些硬件驱动工具。程序用了反作弊或强完整性校验。这类程序会检测运行环境发现不是原生 Windows 就拒绝运行。游戏里的反作弊系统是重灾区。程序对性能要求极高。兼容层的开销虽然不大但对于帧率敏感的游戏或者低延迟的音频处理这点开销可能就是不可接受的。程序有成熟的 Linux 原生替代。这种情况下直接用原生替代是更明智的选择没必要为了兼容而兼容。判断标准很简单先花半小时用 Wine 试跑一下如果能启动且基本功能正常就值得继续折腾如果连启动都做不到或者启动后核心功能不可用那就果断放弃换别的方案。6. 实操心得那些文档里不会写的经验折腾兼容层这些年踩过的坑比走过的路还多。有几个经验是文档里不会写但实际用起来很关键的。第一个是版本锁定。Wine、FEX-Emu、DXVK 这些组件的版本组合很敏感某个组合能跑通的程序换个版本可能就崩了。所以一旦找到能用的组合就把版本号记下来最好把安装包也备份一份。系统更新时要注意别让包管理器把这些组件顺手升级了。第二个是日志先行。遇到问题时第一反应不应该是改配置而是看日志。Wine 的WINEDEBUG环境变量能打开详细的调试输出FEX-Emu 也有自己的日志选项。日志里通常有明确的错误信息比盲目试错高效得多。我习惯把日志重定向到文件然后用 grep 搜关键词这样比在终端里翻屏快。第三个是隔离环境。不同的程序用不同的前缀不同的前缀用不同的配置。这样即使某个程序把前缀搞坏了也不会影响其他程序。前缀的备份也简单直接打包目录就行。恢复时解包回去一切照旧。第四个是社区资源。Wine 的 AppDB 是个宝库里面记录了大量的程序兼容性报告和配置方法。遇到问题时先去 AppDB 搜一下程序名大概率能找到别人踩过的坑和解决方案。FEX-Emu 和 DXMT 的 GitHub issue 里也有不少有价值的信息。第五个是别追求完美。兼容层的目标是能用不是和原生一模一样。有些小问题比如界面上的轻微错位、偶尔的卡顿如果能接受就别花时间去修。把精力放在核心功能上性价比更高。最后分享一个具体的小技巧如果程序在 Wine 下启动很慢可以试试用wine start /unix的方式来启动而不是直接wine program.exe。前者会让 Wine 用更接近 Windows 的方式处理启动流程有时候能绕过一些初始化阶段的问题。这个技巧在跑一些老程序时特别管用我试过好几个原本启动就卡死的程序换成这种方式后都能正常起来了。