0引言
不同操作系统有着各自的可执行文件格式和系统调用,各类处理器指令集也互不兼容,这使得针对某一平台所构建的应用程序通常无法直接在另一平台上运行。为解决这一问题,常见的方案可分为指令模拟器、虚拟机监视器(Virtual Machine Monitor, VMM和应用程序接口(Application Programming Interface, API兼容层等几类。Android作为一个近年来发展起来的十分重要的操作系统,目前有关在其中部署虚拟化或兼容层的研究仍不成熟。本文所提出的框架实现了在Android系统中对Win32环境的高效虚拟,使Windows应用程序不经任何修改便可在Android系统中运行。特别地,这里的Windows程序指使用x86指令集的PE(Portable Executable格式Win32可执行程序,而Android系统基于高级精简指令集机器(Advanced Reduced Instruction Set Computing Machine, ARM架构处理器平台。
在相关研究中,Hentschel等研究了在Android系统中通过KVM(Kernelbased Virtual Machine运行Windows 8系统,其缺点在于需虚拟化整个操作系统及相关硬件,资源消耗较大。
与上述研究相比,本文所提出的兼容层方案初步实现了在ARM平台Android系统中运行x86指令集Win32可执行程序,并仅对程序本身及若干必要的守护进程进行动态二进制翻译(Dynamic Binary Translation和提供API模拟,具有速度快、资源消耗低的特点。该技术的主要应用前景包括:1在平板电脑等移动终端中运行某些专业领域的软件;2解决某些私有函数库未提供Android版实现的问题;3某些涉及隐私、安全等重要信息的Android应用缺乏可信任的开源实现。
PE格式x86可执行程序及连同程序发布的动态库文件依赖于Windows系统所提供的动态库。这里,我们通过兼容层Wine[4]使库的依赖关系得以满足。兼容层所提供的各ELF(Executable and Linkable Format格式x86共享库为上层应用程序提供了所有必需的Win32 API函数接口,而这些函数的实现则依赖于Linux系统中常见的共享库,如C标准库libc.so、X窗口系统客户端接口libX11.so等。由于PE文件一般无法直接被Linux内核识别加载,因此还需引入一个特殊的动态连接器(Dynamic Linker来完成对PE和ELF文件的加载、重定位和动态连接等操作。
加载完成后,内存中的代码段此时存放的仍为x86指令,无法在ARM处理器中正确执行。利用QEMU[5]所提供的用户模式(User Mode,可以高效地将代码段中的x86指令转换为ARM指令。指令的翻译是动态进行的,即仅当x86指令片段将要被执行时,该指令片段才会被翻译为ARM指令。已完成翻译的指令片段将被缓存在内存中供下一次直接调用。
此外,兼容层提供了若干守护进程,用于模拟Windows系统内核的部分功能,如进程管理等。守护进程与应用程序通过socket套接字和管道通信,并采用主从式架构,即这些守护进程可同时为多个独立的应用程序服务。
作为Win32 API中重要的一族函数,图形设备接口(Graphics Device Interface, GDI相关的绘图函数在兼容层中以X窗口系统服务进程作为一个特殊的X窗口系统客户端,通过X11协议及协议扩展与X显示服务通信,将虚拟Framebuffer中的图像通过RFB(Remote Frame Buffer协议发送至VNC客户端,或将来自客户端的输入发送至X显示服务。
2基于指令翻译的Win32兼容层
2.1基于QEMU的动态二进制翻译
x86程序的执行采用动态二进制翻译的方式实现,其核心流程如图2所示。
图片
图2动态二进制翻译核心算法
x86程序被视为由许多翻译块(Translation Block, TB组成,翻译块的划分以跳转等分支指令作为标志,但其大小也受限于指令条数、分页长度等。当程序将执行由内存中虚拟的程序计数器所指向的某一x86指令翻译块时,首先将该翻译块内的x86指令解析为中间表示再进而翻译为等价的ARM指令并执行,当该段动态生成的ARM程序执行结束后,根据新的虚拟程序计数器值确定下一x86指令翻译块起始地址并重复以上步骤。翻译生成的ARM指令翻译块将被保存在缓存中,并以x86指令翻译块起始地址的哈希值作为索引。这样,当程序再次跳转至同一x86指令翻译块时,可直接调用之前已生成的ARM程序而避免再次触发指令翻译。ARM指令翻译块在执行完毕时将返回最后的跳转指令地址及所取的分支等信息,这些信息被用于与下一ARM指令翻译块串接,使下次前一翻译块执行完毕后可直接跳转至后一翻译块而不必再返回主循环。 []
为处理某些特殊指令及中断和异常,内存中包含一虚拟的x86处理器,且生成的ARM指令翻译块被插入了对虚拟处理器中寄存器和标志位的更新操作。特别地,在处理Linux系统调用时,生成的指令会将系统调用号及参数保存于虚拟寄存器中,并设置虚拟处理器的中断标志,随后结束当前ARM指令翻译块的执行,并在主循环中根据检测到的中断标志,对所保存的参数作适当预处理后由Android系统的Linux内核完成相应的系统调用。在多数情况下,这些虚拟的寄存器和标志位并不随着指令的执行而更新,即每一条x86指令的执行并不是通过该虚拟处理器完成的。
2.2基于Wine的Win32兼容层
Win32兼容层主要包括动态连接器、服务程序和API兼容库等组件。这些组件在用户空间实现了Linux内核所不具备的功能,包括PE格式可执行文件的加载、Windows NT内核的对象和任务管理以及系统调用等。
PE文件需由一个特殊的ELF格式动态连接器(或称加载器完成加载。PE文件加载的主要步骤如下:
1保留进程中低于2GB的虚拟地址空间,以保持与Windows NT内核一致的用户空间分配;否则PE文件可能因文件头中所指定的内存
空间已被其他操作占用而被映射至另一地址,而此时仅当该PE文件的机器码为地址无关代码(PositionIndependent Code, PIC时程序方可正确执行。
2解析可执行文件的PE文件头,若加载器搜索路径中存在与模块名相应的PE格式动态连接库,则直接加载映射该动态连接库;否则加载器将模块名映射为兼容层中相应的ELF格式共享库,并调用Linux系统原生的动态连接器加载该API兼容库。
3完成模块的重定位和函数的动态连接等操作。
4与兼容层守护进程建立连接,通知守护进程初始化进程环境块(Process Environment Block, PEB、线程环境块(Thread Environment Block, TEB等Windows NT内核中重要的数据结构。
5初始化Win32进程堆栈,并跳转至PE文件头中所指定的入口点,将控制权交给所加载的可执行程序。
服务程序是在用户空间中对Windows NT内核部分功能的模拟,维护着作为客户端的各Win32进程的相关数据结构,以实现Win32环境中的句柄管理、消息分派和进程间通信等机制。客户端对服务端的远程过程调用(Remote Procedure Call, RPC是通过UNIX域socket和管道完成的。当客户端需要调用Windows内核函数时,服务端将收到来自客户端的调用请求,并在执行相应的处理后将结果返回客户端。以文件读写为例,其过程如图3所示。
图片
图3文件读写流程
由于Linux内核没有提供某些Windows系统调用的等价实现,因此该文件必须由服务端打开,以保证服务端能拥有足够的信息进行某些涉及进程的处理。为使客户端能直接读写文件而避免大量数据的进程间拷贝,服务端的文件描述符会通过UNIX域socket应答的控制信息发送至客户端,使客户端拥有对文件的访问权。
与Windows系统所提供的动态连接库相对应,兼容层通过一系列ELF格式共享库在Linux系统中为上层应用程序提供了与Win32 API一致的函数接口。以GDI绘图为例,由于加载器完成了应用程序与兼容层所提供的函数库的动态连接,故图形绘制将通过兼容层转化为对X窗口系统客户端接口的调用完成。兼容层还实现了对文件系统、内存管理和进程间通信等其他各类接口的封装,此处不再赘述。
2.3基于Xvfb的图形渲染
如2.2节中所指出的,窗口和各控件所涉及的GDI调用是借助X窗口系统客户端接口实现的,然而Android系统本身并不提供X显示服务进程。为防止与系统原有图形栈相冲突,并尽可能避免引入权限和硬件相关方面的问题,此处我们选择了基于虚拟Framebuffer的显示服务,即Xvfb。Xvfb仍通过X协议与各客户端通信,但并不访问物理屏幕,而是在进程中分配一块连续的内存空间用于渲染各窗口的图像位图。
虚拟Framebuffer的显示是通过VNC服务完成的。在X显示服务看来,VNC服务进程作为一个普通的X客户端,不断地通过X协议的MITSHM扩展接口请求虚拟Framebuffer中的位图。VNC服务进程在比较前后两帧位图的差异后,将有变化的矩形区域的坐标、大小和位图数据通过RFB协议发送给VNC客户端。由于本系统中VNC客户端位于本地,故矩形位图数据在传输时没有经过压缩和解压,这既降低了处理器负载,也避免了图像质量的损失。最后,VNC客户端根据端口收到的数据刷新上一帧图像,并调用Android系统提供的标准接口将图像显示到屏幕上。
在处理用户输入时,VNC客户端将键盘和鼠标事件通过RFB协议发送至服务端,服务端进一步通过X协议的XTEST扩展接口将这些事件发送至X显示服务,并交由相应的X客户程序处理。
2.4系统的进一步优化
动态二进制翻译与API兼容层保证了系统整体的高效性,但基于QEMU的动态二进制翻译算法决定了API兼容库以及这些兼容库所依赖的其他共享库必须同所加载的可执行文件一样使用x86指令集,程序在执行时这些库中的x86代码段也需经过动态翻译。若参考动态二进制翻译对系统调用的处理方式,则可实现对经过特殊构造的ARM指令集兼容库的直接调用。该方案的基本思想为:
1在兼容库各函数入口处引入无效指令,使程序一旦调用这些函数即触发异常,进入动态二进制翻译主循环中的异常处理函数;
2异常处理函数根据无效指令的地址反查出相应兼容库函数真正的ARM指令入口地址;
3对虚拟寄存器和栈中所保存的参数作适当预处理,随后跳转至该入口地址。
该方案可复用Android系统中的C标准库等基础框架,有利于减少兼容层所占用的存储空间,增强系统安全性。 []
在图形方面,显示服务进程可与VNC服务进程合并,并进一步可与VNC客户端整合,即完全由Java和JNI调用实现Android框架下的X显示服务,避免进程间通信所产生的负载。基于X协议良好的可拓展性,还可按图4的架构进一步完善系统功能。在处理3D渲染方面,Win32环境中的Direct3D接口是通过兼容库转化为OpenGL调用实现的,然而在Android系统中无法利用直接渲染架构(Direct Rendering Infrastructure, DRI[8]实现高效的客户端OpenGL接口调用。Mesa项目[9]能够为X客户端提供OpenGL标准的一个纯软件实现,但该方案仅能满足一些简单场景的实时渲染。另一种可能的解决方案是将X客户端的OpenGL请求通过X协议的GLX扩展发送至X显示服务进程,由显示服务进程调用OpenGL硬件加速接口完成场景渲染,即非直接渲染(Indirect Rendering。此外,Android系统所提供的OpenGL ES接口仅可视为OpenGL标准的一个子集, 而glshim项目[10]提供了一个基于OpenGL ES且满足OpenGL 1.x标准的兼容库,是我们的重要参考。在输入法方面,输入法服务进程可通过XIM(X Input Method框架接收来自X显示服务的输入或向客户端提交文本。可以无需提供X窗口系统下独立的输入法服务,而选用Android系统中已安装的输入法。为此,运行于系统前端的程序需监听来自X显示服务进程的输入法请求,触发系统输入法,获取用户输入,并将所输入的文本通过XIM协议发送至X客户端。最后,在窗口管理方面,可以通过移植Linux桌面系统中已有的窗口管理器实现窗口边框的绘制和处理窗口拖动、缩放等事件。
图片
3性能测试
以下主要从启动时间、内存占用和应用程序性能等方面考查系统表现。测试平台使用Android 4.4.3系统,其主要硬件参数如表1所示。
表格(有
表名
表1测试平台主要硬件参数
硬件参数描述
CPU1.5 GHz四核Krait 300
GPU400 MHz四核Adreno 320
内存2 GB DDR3L
作为对比,方案1使用Bochs虚拟机[11]进行完整的系统级虚拟,方案2为本文所提出的基于动态二进制翻译的兼容层。
在启动时间方面,方案1与方案2均可分为环境的初始化和应用程序本身的加载连接两个阶段。其中:方案1的环境初始化主要包括启动引导程序、初始化Windows内核和启动系统服务进程等过程,耗时可达10~20min;而方案2的初始化主要为兼容层服务进程的启动和显示服务进程的启动,该过程可在30s内完成。由于应用程序的加载时间取决于可执行文件的大小和I/O速率,且启动多个应用程序时仅需初始化环境一次,故该阶段耗时此处不予比较。
类似地,在内存占用方面,仅考查不启动任何应用程序时系统基本环境的内存占用情况。测试表明,系统启动完毕进入稳定状态后,方案1典型的内存占用为200±20MB,而方案2所消耗的内存空间为140±10MB。
在应用程序性能方面,选取了qtperf[12]、SQLIO[13]和LINPACK[14]这3个有代表性的基准测试程序。其中:qtperf是一个基于Qt库的自动化测试工具,用于衡量各常用控件和GDI绘图的性能;SQLIO用于测试文件读写性能,实验中所使用的测试文件大小为1GB,测试持续时间为60s,每次I/O请求的数据量为2kB,I/O线程数为1;LINPACK给出了求解不同规模n元一次方程组时的浮点运算性能,作为参考本文也给出了ARM原生程序的相关数据。由表2、表3和表4可以看出,本文所提出的兼容层框架在图形界面渲染、文件读写和浮点运算等重要性能指标中的表现均大幅优于基于系统级虚拟的方案1。
表格(有表名
表2图形性能测试结果
测试项目重复次数
耗时/s
方案1方案2
更新文本框文本10025.35.4
展开下拉菜单100442.933.0
更新进度条进度10019.14.9
点击按钮1009.82.3
勾选复选框选项1006.62.0
移动文本框滚动条位置10067.18.8
绘制随机直线10000015737.61792.6
绘制随机实心椭圆10000015570.11761.7
绘制位置随机的彩色文本100001650.3185.9
绘制位置随机的彩色位图1000165.718.1
表格(有表名
表3文件读写性能测试结果
测试项目
速率/(MB·s-1
方案1方案2
连续读取0.836.20
连续写入0.834.96
随机读取0.856.20
随机写入0.832.63
表格(有表名
表4浮点运算性能测试结果
方程组规模n
方案1
耗时/s速率/KFLOPS
方案2
耗时/s速率/KFLOPS
ARM原生程序
耗时/s速率/KFLOPS
50030.9712.55.63843.10.8824515.5
1000230.5744.344.93779.37.3223222.5
此外,方案1的实现原理导致了一些其他难以解决的问题。例如,虚拟机需精确维护虚拟处理器的运行状态;虚拟机中的应用程序无法直接访问宿主机文件系统;虚拟机中客户系统的I/O缓存占用虚拟机进程空间,故该方案在实际情况下的内存占用将随着程序的运行而增大。而方案2则可利用宿主系统硬件而非软件模拟的方式实现分页等内存管理,且应用程序所产生的I/O缓存由Android系统的Linux内核管理,在必要时可释放作为空闲内存。
但是,目前的实现仍是实验性的,因而在运行一些较为复杂的应用程序时,其兼容性尚与系统级虚拟存在差距。 []
4结语
为使普通Android设备能够运行Win32应用程序,本文提出了以一套兼容层框架来提供系统环境的轻量级虚拟。在框架设计中,考虑到多数设备在性能方面的局限性,选择了将QEMU所提供的动态二进制翻译与Wine兼容层相结合这一最佳方案,以避免对整个操作系统的虚拟。其中,动态二进制翻译解决了指令集不兼容的问题,而API的模拟则通过对Linux系统常用共享库函数的封装和若干服务进程完成。实现中不涉及对原Android系统的修改,且尽可能避免了对硬件设备的直接访问,因而具有高度的可移植性。测试结果表明,兼容层框架在图形界面渲染、文件读写和浮点运算等方面的速度可数倍于基于虚拟机的解决方案,是一种Win32环境的高性能虚拟。