wine 如何运行exe 应用
知道了wine 能够运行Windows 应用以后,一直觉得很奇怪,它是如何调起exe 应用的。下文将详细解释。
在此之前,需要了解下Windows/Linux 分别是如何运行应用程序的。
Windows下可执行文件格式为 PE(Portable Executable),后缀一般为.exe;
Linux下可执行文件格式为 ELF(Executable Linkable Format),无后缀。
下面两幅图分别简要描述了两种格式的区别,如果需要进一步了解两种格式具体的内容,可以查阅官方文档。
Windows 下运行应用过程
当我们在双击exe 文件时,Windows 系统会执行操作如下:
- 创建进程,主线程;
- 系统程序检查.exe 文件头;
- 连接器嵌入exe 文件头信息;
- 导入所需要的dll;
- 初始化运行库;
- 执行代码中的main 类似入口函数;
Linux 下运行应用程序
当我们在执行Linux 应用时,Linux系统会执行如下操作:
- 内核加载映像(≈二进制文件)并看到它是一个动态可执行文件
- 内核加载动态加载器(ld.so)并给它控制权
- 动态加载器解决依赖关系并加载它们
- 动态加载器将控制权交还给原始二进制文件
- 原始二进制文件在_start()中开始执行,最终进入main()。
很显然,如果直接执行exe 文件,Linux 系统是不认可的,因为它不是linux 动态可执行文件。每个可执行文件都有定义的一些头文件,里面包含依赖的动态链接库以及运行地址等信息。以一个简单的helloworld 程序为例,编译成Window 版本和Linux 版本以后。均可以通过工具查看到其依赖:
那么wine 是如何实现在linux 环境中运行exe 的呢。基于Windows 和Linux 不同的运行流程,wine将Windows可执行文件加载到内存中,解析它,找出依赖关系,找出可执行代码的位置(即.text部分),然后最终跳转到该代码。也即实现了 ld-linux-x86-64.so.2 类似的功能。
理论很简单,实现上确实有较大的难度。难度主要集中在以下几个方面:
1、.text 代码段如何能够在linux 正常运行;
编译生成的机器代码,只要CPU架构是一样的,则意味着其指令集也是一样的,按照逻辑来讲,只要CPU 架构是一致的,是可以正常运行。
2、代码段中涉及系统调用的函数,需要由内核去完成,这时候,应用程序代码需要一种方法来 "中断 "自己,将控制权交给内核(这种操作通常称为上下文切换)。在每个操作系统上,操作系统所暴露的函数集和调用这些函数的方式都是不同的。
Linux上:read, write, open, brk, getpid
Windows上:NtReadFile, NtCreateProcess, NtCreateMutant 。
针对Windows ,可以看如下这幅图,当然,在linux 下同样有用户模式和内核模式的跳转。
在wine 实现中,在用户层和内核层的接口处,提供ntdll 的自定义实现。在Wine的最新版本中,它由两部分组成:ntdll.dll(PE库)和ntdll.so(ELF库)。第一个部分是一个薄层,只是将调用重定向到ELF对应部分。ELF对应库包含一个名为__wine_syscall_dispatcher的特殊函数,它执行了一个将当前堆栈从Windows转换到Linux并返回的魔术。因此用户调用创建文件 时,就会有如下的调用过程:
CreateFile(kernal) -> NtCreateFile(ntdll.dll) ->NtCreateFile(ntdll.so)
其中特殊分发函数__wine_syscall_dispatcher 和 __wine_syscall_dispatcher 函数,其实际上是一段针对不同CPU 优化的汇编代码,实现都在针对不同架构处理器的文件中,如:signal_arm.c /signal_arm64.c / signal_x86_64.c /signal_i386.c;
3、Windows 有着众多的dll 和系统API ,并且这些实现是不开源的,可以获取的文档资料也有限制,无法完全去实现部分API;
4、如果有应用(尤其是游戏应用),绕过ntdll,直接调用syscall,那么wine 将无法做到适配。