Wine 开发者指导/架构概览

Wine 开发者指导/架构概览

中文翻译 https://wiki.winehq.org/Wine_Developer's_Guide/Architecture_Overview#Memory_management

文章目录

  • Wine 开发者指导/架构概览
    • 1 Wine的概览
      • 1.1 前言
      • 1.2 可执行文件
    • 2 标准Windows架构
      • 2.1 Windows 9x 架构
      • 2.2 Windows NT 架构
    • 3 Wine 架构
      • 3.1 全局概览
      • 3.2 Wine服务器
      • 3.3 Wine内置DLL:关于中继,Thunks,和DLL描述符
      • 3.4 Wine/Windows DLLs
        • 3.4.1 原生DLL的优点
        • 3.4.2 原生DLL的缺点
        • 3.4.3 原生DLL和内置DLL之间的取舍
        • 3.4.4 DLL的加载顺序
      • 3.5 内存管理
        • 3.5.1 初始布局(Windows中)
        • 3.5.2 对比
      • 3.6 Wine驱动程序

1 Wine的概览

1.1 前言

Wine常常被看做一个缩写,代表“Wine Is Not an Emulator”。有时它也被称为“Windows模拟器”。从某种意义上说,这两种意义都是正确的,只是从不同的角度来看。第一个含义是,Wine不是虚拟机,它不模拟CPU,也不应该在W天上安装Windows或任何Windows设备驱动程序;相反,Wine是Windows API的一个实现,可以用作将Windows应用程序移植到Unix上的库。显然,第二个含义是,对于Windows二进制文件(.exe文件),Wine确实看起来像Windows,并且相当接近地模拟了它的行为和怪癖。

注意:“模拟器”的观点,就好像Wine是一个典型的低效模拟层,是不应该被认可的。Wine不会很慢 - 对设计不当的Windows API的忠诚可能会在某些情况下产生较小的开销,但是这可以通过运行Wine的Unix平台的更高效率来平衡,其他可能的抽象库(如Motif,GTK+,CORBA等)的运行时间开销通常与Wine’s相当。

1.2 可执行文件

Wine的主要任务是在非Windows操作系统下运行Windows可执行文件。它支持不同类型的可执行文件:

  • DOS可执行文件。那些更老的程序,使用DOS格式(.com或.exe(后者也称为MZ))。
  • Windows NE可执行文件,也称为16位。它们是由Windows 2.x和3.x运行的原生进程。 NE代表New Executable
  • Windows PE可执行文件。这些程序是在Windows 95中引入的(并成为所有后续更高版本Windows的原生格式),即使仍支持16位应用程序。 PE(Portable Executable)在某种意义上表示可执行文件的格式(作为文件)独立于CPU(即使文件的内容,也即代码,与CPU相关)。
  • Winelib可执行文件。这些是使用Windows API编写的应用程序,但编译为Unix可执行文件。 Wine提供了创建这种可执行文件的工具。

让我们快速查看受支持的可执行文件的主要区别:

DOS (.COM or .EXE) Win16 (NE) Win32 (PE) Win64 (PE) Winelib
多任务处理 同时只使用一个应用程序(除了TSR) 合作的 先占式 先占式 先占式
地址空间 1MB内存,每个应用程序都在内存中加载和卸载。 所有16位应用程序共享一个地址空间,保护模式。 每个应用程序都有自己的地址空间。需要获得CPU的MMU支持。 每个应用程序都有自己的地址空间。需要获得CPU的MMU支持。 每个应用程序都有自己的地址空间。需要获得CPU的MMU支持。
Windows API 没有Windows API,而是DOS API(比如Int 21h traps)。 会调用16位Windows API。 会调用32位的Windows API。 会调用32位的Windows API。 会调用32/64位的Windows API,可能也会调用Unix API。
代码(CPU级) 只在x86上以实模式可用。代码和数据是分段的,有16位偏移量。处理器处于实模式。 只在IA-32架构上可用,代码和数据都是分段式的,有16位偏移量(因此得名)。处理器处于保护模式。 在数个(带有NT的)cpu上可用,包括IA-32。在这个CPU上,使用一个带有32位偏移量的平面内存模型(因此得名) 只能在AMD64和AArch64架构上使用。 带有32/64位地址的平板模式(Flat model)
多线程 不可用 不可用 可用 可用 可用,但对于线程和同步,必须使用Win32/64 API,不能使用Unix API

Wine通过为每个Win32进程启动一个单独的Wine进程(实际上是一个Unix进程)来处理这个问题,但不适用于Win16任务。 Win16任务在同一个专用Wine进程中以不同于相互同步的Unix线程的方式运行;这个Wine进程通常被称为WOW进程(Windows上的Windows),指的是Windows NT使用的类似机制。

在WOW进程中运行的Win16任务之间的同步通常是通过Win16互斥体完成的——每当它们中的一个正在运行时,它就拥有Win16互斥体,从而阻止其他任务运行。当任务希望让其他任务运行时,线程将释放Win16互斥体,然后其中一个等待线程将获取它并让其任务运行。

winevdm 是致力于运行Win16进程的Wine进程。请注意,可能存在此进程的多个实例,因为Windows支持不同的VDM(虚拟Dos机器)以使Win16进程在不同的地址空间中运行。 Wine也使用相同的架构来运行DOS程序(在这种情况下,DOS模拟由 KRNL386.EXE 提供)。

2 标准Windows架构

2.1 Windows 9x 架构

Windows体系结构(Win 9x方式)如图:

+---------------------+        \
|     Windows EXE     |         } application
+---------------------+        /

+---------+ +---------+        \
| Windows | | Windows |         \ application & system DLLs
|   DLL   | |   DLL   |         /
+---------+ +---------+        /

+---------+ +---------+      \
|  GDI32  | | USER32  |       \
|   DLL   | |   DLL   |        \
+---------+ +---------+         } core system DLLs
+---------------------+        /
|     Kernel32 DLL    |       /
+---------------------+      / 

+---------------------+        \
|     Win9x kernel    |         } kernel space
+---------------------+        /

+---------------------+       \
|  Windows low-level  |        \ drivers (kernel space)
|       drivers       |        /
+---------------------+       /

2.2 Windows NT 架构

Windows体系结构(Windows NT方式)如下图所示。 请注意,新的DLL(NTDLL)允许实现不同子系统(如win32);NT架构中的 kernel32 就是实现在 NTDLL 之上的Win32子系统。

+---------------------+                      \
|     Windows EXE     |                       } application
+---------------------+                      /

+---------+ +---------+                      \
| Windows | | Windows |                       \ application & system DLLs
|   DLL   | |   DLL   |                       /
+---------+ +---------+                      /

+---------+ +---------+     +-----------+   \
|  GDI32  | |  USER32 |     |           |    \
|   DLL   | |   DLL   |     |           |     \
+---------+ +---------+     |           |      \ core system DLLs
+---------------------+     |           |      / (on the left side)
|    Kernel32 DLL     |     | Subsystem |     /
|  (Win32 subsystem)  |     |Posix, OS/2|    /
+---------------------+     +-----------+   / 

+---------------------------------------+
|               NTDLL.DLL               |
+---------------------------------------+

+---------------------------------------+     \
|               NT kernel               |      } NT kernel (kernel space)
+---------------------------------------+     /
+---------------------------------------+     \
|       Windows low-level drivers       |      } drivers (kernel space)
+---------------------------------------+     /

还要注意(上面的架构中没有描述)16位应用程序被一个特定子系统所支持。Win9x和NT架构之间的一些基本区别包括:

  • 几个子系统(Win32,Posix …)可以在NT上运行,而不能在Win 9x上运行。
  • Win 9x将其架构植根于16位系统,而NT则是真正的32位系统。
  • Win 9x和NT中的驱动程序模型和接口是不同的(即使微软试图通过Win 98及以上版本的WDM驱动程序的一些支持弥合差距)。

3 Wine 架构

3.1 全局概览

即使数个子系统尚未实现,Wine的实现仍更接近于Windows NT体系结构(同时提醒16位支持是在32位Windows EXE中实现的,而不是作为子系统实现的)。 总体情况如下:

+---------------------+                                  \
|     Windows EXE     |                                   } application
+---------------------+                                  /

+---------+ +---------+                                  \
| Windows | | Windows |                                   \ application & system DLLs
|   DLL   | |   DLL   |                                   /
+---------+ +---------+                                  /

+---------+ +---------+     +-----------+  +--------+  \
|  GDI32  | |  USER32 |     |           |  |        |   \
|   DLL   | |   DLL   |     |           |  |  Wine  |    \
+---------+ +---------+     |           |  | Server |     \ core system DLLs
+---------------------+     |           |  |        |     / (on the left side)
|    Kernel32 DLL     |     | Subsystem |  | NT-like|    /
|  (Win32 subsystem)  |     |Posix, OS/2|  | Kernel |   /
+---------------------+     +-----------+  |        |  / 
                                           |        |
+---------------------------------------+  |        |
|                 NTDLL                 |  |        |
+---------------------------------------+  +--------+

+---------------------------------------+               \
|            Wine executable            |                } unix executable
+---------------------------------------+               /
+---------------------------------------------------+   \
|                   Wine drivers                    |    } Wine specific DLLs
+---------------------------------------------------+   /

+------------+    +------------+     +--------------+   \
|    libc    |    |   libX11   |     |  other libs  |    } unix shared libraries
+------------+    +------------+     +--------------+   /  (user space)

+---------------------------------------------------+   \
|         Unix kernel (Linux,*BSD,Solaris,OS/X)     |    } (Unix) kernel space
+---------------------------------------------------+   /
+---------------------------------------------------+   \
|                 Unix device drivers               |    } Unix drivers (kernel space)
+---------------------------------------------------+   /

Wine必须至少完全替代“三大”DLL(KERNEL / KERNEL32GDI / GDI32USER / USER32),其他所有DLL都被分层堆砌。但是,由于Wine(出于各种原因)倾向于NT的实现方式,NTDLL是另一个在Wine中实现的核心DLL,许多 KERNEL32ADVAPI32 特性将通过NTDLL实现。

截至今日,在Wine中没有实现真正的子系统(除Win32之外)。

Wine服务器为实现核心DLL提供了主干。主要实现进程间同步和对象共享。从功能的角度来看,它可以看作是一个NT内核(即使Wine DLL和Wine服务器之间使用的API和协议是特定于Wine的)。

Wine使用Unix驱动来访问盒子上的各种硬件。但是,在某些情况下,Wine会提供一个驱动程序(在Windows的意义上)用于物理硬件设备。该驱动程序将成为Unix驱动程序的代理(例如,对于带有X11或Mac驱动程序的图形部件,带有OSS或ALSA驱动程序的音频…)。

Wine提供的所有DLL都尽可能地将其从Windows平台上的导出的API中复制出来。在极少数情况下,情况并非如此,并且都被正确记录下来(Wine DLL会导出一些Wine特定的API)。通常,这些API以 __wine 为前缀。

现在让我们更详细地回顾所有这些组件。

3.2 Wine服务器

Wine服务器是Wine中最令人困惑的概念之一。它在Wine中的功能是什么?简而言之,它提供了进程间通信(IPC),同步和进程/线程管理。Wine服务器启动时,它会根据(如下所示)主目录下的.wine子目录(或WINEPREFIX 环境变量所指向的任何位置)为当前主机创建一个Unix套接字——稍后启动的所有Wine进程都将使用此套接字连接到Wine服务器。如果Wine服务器尚未运行,则第一个Wine进程将以自动终止模式(即Wine服务器将在最后一个Wine进程终止后自行终止)启动Wine服务器。

上面提到的主套接字是在/tmp目录中创建的,其名称反映了配置目录。这意味着实际上可以有几个分离的Wine服务器副本运行;每个用户和配置目录的组合对应一个Wine服务器副本(翻译待定)。请注意,您不应该有多个用户同时使用相同的配置目录;他们将有不同的Wine服务器副本运行,这可能会导致他们共享的注册表信息出现问题。

每个Wine进程中的每个线程都有自己的请求缓冲区,它与Wine服务器共享。当一个线程需要与任何其他线程或进程同步或通信时,它将填充其请求缓冲区,然后通过套接字写入一个命令代码。 Wine服务器根据需要处理命令,而客户端线程等待回复。在某些情况下,会有如同各种 WaitFor ??? 同步原语,服务器通过将客户端线程标记为等待并在等待条件满足之前不发送回复来处理它。

Wine服务器本身是一个独立的分离的Unix进程,没有自己的线程——相反,它是建立在一个大的 poll() 循环之上的,当任何事情发生时都会向Wine服务器发出警报,例如:发送命令的客户端或者等待条件已经满足。因此,Wine服务器本身没有内部竞争条件的危险——它经常被要求进行对它的客户端来说完全原子化的操作。

由于Wine服务器需要管理进程,线程,共享句柄,同步以及任何相关问题,因此所有客户端的Win32对象也由Wine服务器管理,并且客户端必须在任何需要知道任何Win32对象句柄的关联Unix文件描述符时向Wine服务器发送请求(在这种情况下,Wine服务器复制文件描述符,将其传回客户端,并在客户端完成时将其留给客户端以关闭副本)。

3.3 Wine内置DLL:关于中继,Thunks,和DLL描述符

本节主要适用于内置DLL(由Wine提供的DLL)。有关本机与内置DLL处理的详细信息,请参阅3.4节。

将Windows二进制文件加载到内存本身并不难,难的部分就是它导入的所有那些各种DLL和入口点,并且期望它们在那里并按预期发挥功能;显然,整个Wine实现的全部内容都是关于它的。Wine包含一系列DLL实现。您可以在 dlls/ 目录中找到DLL的实现。

每个DLL(至少是32位版本,见下文)都是以Unix共享库的形式实现的。共享库的文件名是带有 .dll.so 后缀(或 .drv.so 或任何其他相关扩展,具体取决于DLL类型)的DLL的模块名称。该共享库包含DLL的代码本身,以及一些更多信息,如DLL资源和Wine特定的DLL描述符。

当DLL被实例化时,DLL描述符被用于创建内存中的PE头,该头将提供对关于DLL的各种信息的访问,包括但不限于其入口点,其资源,其段,其调试信息…

DLL描述符和入口点表由 winebuild 工具生成,以DLL规范文件的扩展 .spec 作为输入。将资源(由 wrc 编译之后)或消息表(由 wmc 编译之后)通过 winebuild 添加到描述符。

当一个应用程序模块想要导入一个DLL时,Wine会看到:

  • 通过其已注册的DLL列表(实际上,已加载的DLL以及已经加载的已注册DLL描述符的共享库)。因为在加载共享库时,DLL描述符会自动注册——请记住,注册调用函数放在共享库构造函数中。
  • 如果它没有注册,Wine会在磁盘上查找它,从DLL模块名称构建共享库名称。搜索的目录由 WINEDLLPATH 环境变量指定。
  • 否则,它会寻找一个真正的Windows .DLL 文件来使用,并查看其导入等,并使用内置DLL的加载。

在识别出DLL之后(假设它仍然是一个内置DLL), dlopen() 调用函数将其映射到内存中。请注意,Wine不使用共享库机制来解析 和/或 导入两个共享库之间的函数(对于两个DLL)。共享库仅用于提供按需加载代码的方式。由于DLL描述符,这段代码将提供原生DLL所能提供的相同类型的信息。然后Wine可以使用相同的代码来处理导入/导出操作,这些代码用于原生和内置DLL。

Wine还依赖于Unix共享库的动态加载特性来根据需要重新定位DLL(同一个DLL可以在两个不同进程中的不同地址加载;如果加载DLL的顺序不同,则可以在同一个可执行文件的两个连续运行中的不同地址加载相同的DLL)。

DLL描述符使用一些策略在Wine的领域注册。Winebuild 工具在创建DLL描述符的代码时也会创建一个构造函数,当共享库被加载到内存中时将会调用该构造函数。这个构造函数实际上将描述符注册到Wine的DLL加载器。因此,在 dlopen 调用返回之前,将会了解并注册DLL描述符。这也有助于处理在不同共享库之间仍存在的依赖关系(在ELF共享库级,而不是在嵌入式DLL级)的情况:嵌入式DLL将被正确注册,甚至加载(从Windows的角度)。

由于Wine本身就是32位代码,并且如果编译器支持Windows调用约定 stdcall(gcc执行),Wine可以通过直接替换Wine处理程序的地址来将导入文件解析为Win32代码,而不需要之间的任何thunking层。这消除了大多数与“仿真”相关联的开销,并且是应用程序期望的。

但是,如果用户指定了 WINEDEBUG=+relay,则会在应用程序的导入文件和Wine处理程序之间插入一个thunk层(实际上它修改了DLL的导出表,并在表中插入了一个thunk);这个层被称为“中继”,因为它所做的只是打印参数值或返回值(通过使用DLL描述符的入口点表中的参数列表),然后传递该调用,但这对调试错误的Wine代码调用非常有用。 Windows DLL之间也存在类似的机制——通过使用WINEDEBUG=+snoop,Wine可以选择性地在它们之间插入thunk层,但由于没有针对 non-Wine DLL存在的DLL描述符信息,因此它不太可靠并且可能导致崩溃。

对于Win16代码,thunking是无法绕过的——Wine需要在16位和32位代码之间进行中继。 这些thunk在应用程序的16位堆栈和Wine的32位堆栈之间切换,根据需要复制和转换参数(整数大小不同,指针在16位中是分段的但是在32位中是32位线性值),并处理 Win16互斥体。一些更好的控制可以在转换中获得,参见 winebuild 参考手册了解详情。 尽管如此,这种错综复杂的内容杂乱无章的结果,并不适合初学者。

3.4 Wine/Windows DLLs

本文档主要介绍Wine对当前DLL支持的状态。winecfg 当前支持更改DLL加载顺序的设置。加载顺序取决于几个问题,这会导致对不同DLL的不同设置。

3.4.1 原生DLL的优点

原生DLL当然可以保证它们实现的例程具有100%的兼容性。例如,使用原生 USER DLL可以保持窗口边框,对话框控件等显示几乎完美并且类似于Windows 95的外观。 另一方面,使用该库的内置Wine版本会产生一个不精确模仿的Windows 95的显示。当内置的Wine DLL在加载顺序上超过其他类型时,这种细微的差异可能会在其他重要的DLL中产生,例如公共控件库 COMMCTRL 或通用对话框库 COMMDLG

更重要的是,如果内置Wine版本的 SHELL DLL在原生版本的 SHELL 之前加载,由于原生版本包含了很多安装程序使用程序用于创建桌面快捷方式之类的例程,则可能会导致美学导向的问题大多无法被解决。使用Wine内置的 SHELL 时,某些安装程序可能会失败。

3.4.2 原生DLL的缺点

并非每个应用程序在原生DLL下的性能都更好。如果一个库试图访问在Wine中未完全实现的系统其余部分的功能,则原生DLL可能会比相应的内置DLL功能更糟糕(如果有的话)。例如,原生Windows GDI 库必须与Windows显示驱动程序配对,这在Unix和Wine下显然不存在。

最后,偶尔内置的Wine DLL会实现比相应的原生Windows DLL更多的功能。 这种行为最重要的例子可能是Wine内置的 USER DLL提供的Wine与X的集成。如果原生Windows USER 库具有加载顺序优先级,那么在Wine窗口和X窗口之间使用剪贴板或拖放的功能将会丢失。

3.4.3 原生DLL和内置DLL之间的取舍

显然,没有一个关于使用哪个加载顺序的经验法则。 因此,您必须熟悉具体的DLL所做的工作以及给定库与哪些其他DLL或功能进行交互,并使用这些信息进行个案决策。

3.4.4 DLL的加载顺序

默认加载顺序遵循以下算法:对于具有完全功能的Wine实现的所有DLL,或者已知原生DLL不工作的情况下,将首先加载内置DLL。在其他所有情况下,原生DLL都会采用加载顺序优先顺序。

有关如何更改设置的信息,请参阅 Wine用户指南。

3.5 内存管理

Wine中的每个Win32进程在主机系统上都有自己的专用本机进程,因此也有自己的地址空间。 本节探讨Windows地址空间的布局以及它如何被仿真。

首先,快速回顾虚拟内存的工作原理。 RAM片中的物理内存被分成几个帧,每个进程看到的内存都被分成多个页面。每个进程都有其自己的4GB的地址空间(4GB是可用32位指针寻址的最大空间)。页面可以是已被映射或未映射的:尝试访问未映射的页面会导致识别代码为0xC0000005的 EXCEPTION_ACCESS_VIOLATION。 任何页面都可以映射到任何帧,因此您可以拥有多个实际“包含”相同内存的地址。 页面也可以映射到诸如文件或交换空间之类的东西,在这种情况下,访问该页面将导致磁盘访问将内容读取到空闲帧中。

3.5.1 初始布局(Windows中)

当Win32进程启动时,它没有明确的地址空间可供使用。操作系统已经映射了许多页面。特别是,EXE文件本身以及它需要的任何DLL都被映射到内存中,并且为栈和一些堆保留了空间(用于为应用程序分配内存的区域)。这些东西中的一些需要位于固定地址,其他的则可以放置在任何地方。

EXE文件本身通常映射在地址 0x400000 或更高的位置上:实际上,大多数EXE都会将其重定位记录删除,这意味着它们必须加载到其基址,并且不能在任何其他地址加载。

DLL在内部与EXE文件非常相似,但它们具有重定位记录,这意味着它们可以映射到地址空间中的任何地址。请记住,我们不是在处理物理内存,而是在每个进程中都有不同的虚拟内存。因此,OLEAUT32.DLL 可以在一个进程中的一个地址中加载,而在另一个进程中在完全不同的地址上加载。确保所有加载到内存中的函数都可以找到对方,这是Windows动态链接器的工作,它是 NTDLL 的一部分。

所以,我们将EXE及其DLL映射到内存中。另外还有两个非常重要的区域:栈和进程堆。进程堆简单地等同于UNIX上的libc malloc:它是由操作系统管理的内存区域,通过malloc / HeapAlloc 分区并分发给应用程序。 Windows应用程序可以创建多个堆,但进程堆始终存在。

Windows 9x还实现了另一种堆:共享堆。共享堆不同寻常,因为从它分配的任何东西都可以在其他每个进程中看到。

3.5.2 对比

到目前为止,我们已经假设整个4G地址空间可用于应用程序。实际情况并非如此:只有较低的2G可用,较高的2G在操作系统使用的Windows NT上是内核区域(从 0x80000000 开始)。为什么内核映射到每个地址空间?主要是为了提高性能:虽然也有可能给内核自己的地址空间——这是Ingo Molnar的4G/4G VM分割补丁为Linux所做的事情——它要求每个进入内核的系统调用都交换地址空间。由于这是一个相当昂贵的操作(需要刷新翻译后备缓冲区等),并且系统调用是经常进行的,所以最好避免将内核映射到每个进程地址空间中的一个固定位置。

基本上,内存映射的比较如下所示:

Address Windows 9x Windows NT Linux
00000000-7fffffff User User User
80000000-bfffffff Shared User User
c0000000-ffffffff Kernel Kernel Kernel

在Windows 9x上,实际上内核只使用最高的1GB(0xC0000000和以上),2到3G的区域是用于加载系统DLL和文件映射的共享区域。 NT和9x的低端2G可用于程序内存分配和堆栈。

3.6 Wine驱动程序

Wine不允许在Unix下运行原生Windows驱动程序。这主要是因为(查看通用体系结构模式)Wine没有实现Windows的内核功能(这里的内核是指实际上的内核,而不是 KERNEL32 DLL ),而是在Unix内核之上建立一个代理层,提供 NTDLLKERNEL32 功能。这意味着Wine不提供内部基础架构来运行原生驱动程序,无论是从Win9x系列还是从NT系列。

换句话说,当且仅当:1)在Unix中支持此设备时(有Unix驱动程序与其交互),2)Wine已经实现在Windows驱动程序API和Unix驱动程序的Unix接口之间建立连接的代理代码,Wine才能够提供对特定设备的访问。

但是,Wine试图在需要访问设备的不同DLL中,通过用户空间中的设备驱动程序的标准Windows API来实现对特定设备的访问。例如,多媒体驱动程序就是这种情况,Wine会将Wine内置DLL加载到OSS接口或ALSA接口。这些DLL就像Windows中的任何用户空间音频驱动程序一样实现了相同的接口。

你可能感兴趣的:(Wine 开发者指导/架构概览)