Wine 开发系列之 —— 入门

说起 Wine,稍微资深一点的 Linux 用户应该都听过,但是真要说起 Wine 到底是怎么回事,可能大多数人不见得说得清。这篇文章会简单的介绍 Wine 的工作原理,以及如何开始 Wine 的开发。所以如果您属于以下三类读者之一:
* 想参与 Wine 开发,但是不知如何开始的。
* 不认同 Wine 技术,仅仅想大致了解 Wine 是如何工作的。
* 只是想能够愉快的用上最新版本 Wine 的。

希望在看完本文后,能够有一些收获。

Wine 是什么

Wine 是 “Wine Is Not an Emulator”的递归缩写,如同 “GNU”一样(GNU’s Not Unix),字面意思就是 Wine 不是一个模拟器。这里的模拟器主要是指 Wine 并不是一个虚拟机,而是一个 Windows API 实现兼容层。这么说可能不太好理解,大家可以把 Windows 应用程序类比成 Android 应用程序,而 Wine 的角色就和 Android 很像了,将操作系统提供的各种功能封装成 API,并让应用程序在隔离的环境内运行。至于 API 长成啥样,隔离的力度如何,这些都是实现相关的,和本质并不冲突。另一方面,Wine 其实又是一个模拟器,不过模拟的对象不是硬件 CPU,而是 Windows 的行为。

Wine 原理介绍

本节内容较为枯燥,需要对操作系统有一定的了解。如果只是想编译、运行,对原理不敢兴趣的同学,可以跳过,不影响后面的阅读。

Wine 的目的是运行 Windows 上的可执行程序(PE,portable executable)。我们知道,可执行程序的本质其实就是按照某一规则排列的机器码,而机器码是指令集相关的。得益于常见的 PC 机一般是 x86/x64 的,因此 Windows 应用程序从指令集的角度看,是完全可以在 x86/x64 的 Linux 机器上直接运行,而不需要硬件层模拟的。

但是为了能够直接加载运行 PE 文件,需要满足一些 ABI 兼容。最基本的,Windows PE 程序,会假定自己被加载到地址 0x400000 处,因此 Wine 实现自己的 loader 时,需要保证将 PE 镜像加载到同样的位置。对于静态链接的程序,需要做的事情可能不是太多,但是对于动态链接的程序,Wine 需要模仿 Windows loader 的行为,加载依赖的库,并进行相应的重定位工作。

为了最大程序上减少对二进制层面的依赖,Wine 决定实现至少GDI32KERNEL32USER32三个动态库,因为其他库都是建立在这三个库的基础之上的。所以理论上来说,除此之外的其他动态库是可以直接使用 Windows 上面现有的库的,但由于各种原因,Wine 还是倾向于尽量实现所有的 API。我们把 Wine 自己实现的 API 库称作 builtin,把 Windows 上现成的库称作 native。当 Wine 在加载 builtin 动态库的同时,还会在内存中建立 PE header,用来模仿 Windows 上的内存布局。更加详细的实现,有机会可以写一篇 Wine loader 相关的文章来介绍 Wine 本身、PE 程序和动态库是如何被加载的。

除开 loader 的功能外,Wine 还需要解决进程间通信(IPC)的问题。Wine 的实现方式是将所有跨进程的对象和机制,比如 GDI 对象,比如信号量,全部实现在 Wine server 中。同时 Wine 允许系统运行多个 Wine server 的实例。这样存在于同一个 Wine server 中的对象自然是可以相互通信,好像在同一个空间内;而不同 Wine server 下的对象,是相互隔离的,这种架构使得不同容器之间的程序相互没有影响。Wine server 的具体实现是通过 unix socket,实现了一套 LPC 机制,完成和 API 层的交互。

有了以上这些基础,Wine 实现起各种功能就可以按部就班了,只需理解 Windows 下 API 的行为和含义,然后再重新实现一遍就行了。听起来虽然简单,实际难度不小,特别是一些未公开的行为,必须对整体有相当的了解后才能下手。甚至一些差异,比如 UI 相关的内容,由于 Windows 窗口系统和 X 在设计哲学上的不同,实现上需要有所舍取。目前 Wine 支持的平台不仅包括 Linux,还包括 BSD、Mac OS X 和 Android。

环境

下面的开发环境都以 Deepin 为例进行说明。

首先获取代码。Wine 官方代码仓库地址为 git://source.winehq.org/git/wine.git。如果你想方便打包给别人使用,又不太想折腾打包的一些细节,可以用各个发现版自己维护的 Wine。比如 Debian 维护的 Wine 仓库地址为 https://salsa.debian.org/wine-team/wine.git

这里以官方的 Wine 为例,git clone git://source.winehq.org/git/wine.git

然后安装开发的依赖。为了简单起见,我们只编译 32 位的 Wine,因为 64 位的 Wine 只支持 64 位的 PE 程序,而目前 Windows 上仍有大量的程序只提供了 32 位的版本。

sudo apt install\
    gcc-multilib\
    flex\
    bison\
    libx11-dev:i386\
    libfreetype6-dev:i386\
    libxcursor-dev:i386\
    libxi-dev:i386\
    libxshmfence-dev:i386\
    libxxf86vm-dev:i386\
    libxrandr-dev:i386\
    libxfixes-dev:i386\
    libxinerama-dev:i386\
    libxcomposite-dev:i386\
    libglu1-mesa-dev:i386\
    libosmesa6-dev:i386\
    ocl-icd-opencl-dev:i386\
    libpcap-dev:i386\
    libdbus-1-dev:i386\
    libgnutls28-dev:i386\
    libncurses-dev:i386\
    libsane-dev:i386\
    libv4l-dev:i386\
    libgphoto2-dev:i386\
    liblcms2-dev:i386\
    libpulse-dev:i386\
    libgstreamer-plugins-base1.0-dev:i386\
    libudev-dev:i386\
    libcapi20-dev:i386\
    libcups2-dev:i386\
    libfontconfig1-dev:i386\
    libgsm1-dev:i386\
    libkrb5-dev:i386\
    libtiff-dev:i386\
    libmpg123-dev:i386\
    libopenal-dev:i386\
    libldap2-dev:i386\
    libxrandr-dev:i386\
    libxml2-dev:i386\
    libxslt1-dev:i386\
    libjpeg62-turbo-dev:i386\
    libusb-1.0-0-dev:i386\
    gettext\
    libsdl2-dev:i386\
    libvulkan-dev:i386

接着运行脚本,./configure --with-gnutls --without-hal --without-oss,根据不同的 Wine 版本,此时可能会提示不同的 feature 支持情况。我们可以根据需求,对上面的依赖库和传入的参数进行调整,具体可以查看 configure.ac 的内容。

Wine 的源码比较大,编译有些耗时,可以根据 CPU 情况增加并行参数,比如 make -j8,进行编译。

编译完成后,运行 ./wine --version可以查看版本号。如果想安装到系统,可以运行 sudo make install,但是注意,安装后可能会修改一些文件的默认打开方式。

使用

运行 ./wine winecfg 可以对默认容器进行设置,默认的容器位于 home 目录下的 .wine,环境变量 WINEPREFIX 用来修改当前的容器路径。比如有一个叫 demo.exe 的可执行文件,我们想测试能否正常运行,可以运行 WINEPREFIX=~/.demo_exe ./wine demo.exe,home 目录下的 demo_exe 就会作为其容器目录。

开发

编译过后的 Wine 源码目录结构如下:

├── aclocal.m4
├── ANNOUNCE
├── AUTHORS
├── config.log
├── config.status
├── configure
├── configure.ac
├── COPYING.LIB
├── dlls
├── documentation
├── fonts
├── include
├── libs
├── LICENSE
├── LICENSE.OLD
├── loader
├── MAINTAINERS
├── Makefile
├── Makefile.in
├── po
├── programs
├── README
├── server
├── tools
├── VERSION
└── wine -> tools/winewrapper
  • 目录 dlls 按照模块存放了所有 API 的实现。
  • 目录 loader 是和 Wine 启动、加载相关的代码。
  • 目录 programs 存放了外部程序的代码,比如注册表管理工具 regedit
  • 目录 server 顾名思义,是 Wine server 的实现。

接下来需要做的就和普通开发没什么两样了。比如说我们发现某个应用存在字体相关的 BUG,可以首先根据经验判断在 Windows 上,该程序是如何实现的,然后查看对应的实现。例如 GDI 相关的字体实现,位于 dlls/gdi32/font.cdlls/gdi32/freetype.c。修改完代码后,在所在模块的目录,比如上例就是 dlls/gdi32 下重新 make 就可以快速验证了。

对于复杂的问题,不太好直接定位的,可以通过输出日志的方式来调试,环境变量 WINEDEBUG 指定了需要输出的日志。更加详细的说明可以查看这篇文章

有时我们可能需要把复杂的情况简单化,这时候难免会写一些小的 demo 程序来重现问题。如果不想到 Windows 上面编译,可以使用 mingw 直接在 Deepin 下编译出 exe 文件。方法很简单,首先安装 mingw,sudo apt install mingw-w64,接着正常利用 Windows API 实现程序,最后利用 mingw 编译工具链生成文件即可,Makefile 示例:

hello.exe: hello.c
    i686-w64-mingw32-g++ -o hello.exe hello.c -DUNICODE -D_UNICODE -municode -lgdi32

上面的例子,定义了 UNICODE,所以使用的 UNICODE 版本的 API,入口函数为 wmain,-lgdi32 表示需要链接库 gdi32。生成出来的 hello.exe,可以同时在 Windows 和 Deepin 下运行。

其他

本文介绍的内容只涉及到 Wine 开发的基础,Wine 本身还有很多东西值得去探索。比如 Wine 是如何使用 driver 机制让接口和实现分离的,再比如 Wine 是如何使用纯 C 实现 COM 机制的。虽然 Wine 的出现已经有一些年头了,但是目前的开发仍然比较活跃,感兴趣的同学可以加入进来,为 Linux 生态添砖加瓦,让大家能用到更多的优质应用,也算是曲线救国了:)

发表评论

电子邮件地址不会被公开。 必填项已用*标注