分享

在ARM上实现的一种Java虚拟机

 guitarhua 2013-12-17

在ARM上实现的一种Java虚拟机

A Java Virtual Machine Implemented On Arm

 

摘要

本文叙述了Java虚拟机(JVM)的概念,ARM体系结构的介绍及如何根据现有代码修改和移植实现一个ARM上的Java虚拟机—ArmJVM。着重介绍了虚拟机的体系结构,实验室已有基于WIN32平台的代码从Windows到Linux再到Arm Linux的修改、移植、优化过程以及对移植后的虚拟机的改进工作。其中详细介绍了ARM Linux、Linux与Windows程序设计的异同、ArmJVM虚拟机具体实现中遇到的难点和解决、gcc的内嵌汇编以及arm汇编简要介绍。最后通过测试ArmJVM来验证其正确性和运行效率。

关键词

ARM,Linux,Java虚拟机(JVM),本地方法,动态链接,gcc内嵌汇编

 

 

Abstract

This paper describes the conception of Java Virtual Machine (JVM), the ARM architecture and how to implement the JVM on ARM which modified and ported from the exist codes in WIN32 of my lab. It emphasizes the architecture of JVM and how to port the existed codes from Win32 platform to Linux and then to Arm Linux. It describes the details about the JVM porting, including the differences between Windows, Linux and Arm Linux, the problem with implementing the ArmJVM on Arm machine, the gcc inline asm language and the arm asm language. At last, the correctness and efficiency of the implementation is validated by testing ArmJVM

Keywords

Arm, Linux, Java Virtual Machine (JVM), Native Method, Dynamic Link, Gcc Inline Asm


目录

 

 

 

 

在ARM上实现的一种Java虚拟机... 1

A Java Virtual Machine Implemented On Arm.. 1

第一章       绪论... 4

第二章       开发平台及运行环境... 10

1.    硬件平台... 10

2.    软件平台... 13

3.    运行环境... 14

第三章       实现方案... 14

1.    原有代码MiniJavaVM概述... 14

2.    代码修改重组... 16

3.    移植到X86 PC的Linux平台上... 17

4.    从X86 PC的Linux移植到ARM Linux. 17

5.    代码的优化... 17

6.    移除对STL的使用... 17

7.    API的剪裁... 17

第四章       移植... 18

1.    从Windows到Linux(X86下)... 18

Makefile的编写... 18

动态链接库的使用... 19

编码问题... 25

Visual Studio C++与GCC对于c++的支持不同... 27

GCC行内汇编... 29

2.    从X86 Linux到ARM Linux. 31

ARM指令集及汇编代码的重写... 31

动态链接库定位问题... 36

所需库的编译... 37

第五章       改进与优化... 37

1.    解压代码效率分析及重写... 38

2.    API的裁剪... 40

第六章       验证ArmJVM的正确性... 41

1.    ArmJVM的使用方法... 41

2.    测试操作码实现的正确性... 44

3.    其他方面验证... 46

第七章       不足与后续工作... 46

1.    本地方法... 46

2.    I/O操作... 46

3.    多线程... 46

4.    效率... 47

5.    后续工作:... 47

致谢... 47

参考文献... 47

 


 

第一章          绪论

1.      嵌入式系统

1.1嵌入式系统概述

何谓嵌入式系统?根据英国电机工程师协会的定义所作的翻译,“嵌入式系统为控制、监视或辅助设备、机器或甚至工厂操作的装置”。它具备了下列四项的特性:

1)通常执行特定功能

2)一位电脑与外围构成核心

3)严格的时序与稳定性要求

4)全自动操作循环

嵌入式系统是电脑软件与硬件的综合体,亦可涵盖机械或其他的附属装置。整个综合体设计的目的在于满足某种特殊功能。嵌入式系统的架构可分为五部分:处理器、内存、输入与输出、操作系统与应用软件。他们常见于各种实验仪器、办公设备、交通运输设备、电信设备、制造设备、建筑设备、医疗设备及个人电脑等。

嵌入式系统另外可以分为硬件及软件两部分,其中硬件的设计包括单片机控制电路的设计、网络功能设计、无线通信设计及使用接口等等,嵌入式软件为信息、通信网络或消费性电子等产品系统中的必备软件,专司硬件产品的驱动、控制处理或基本接口功能,以提升硬件产品的价值,为该硬件产品不可或缺的重要部分,它常以韧件形式,如控制器或驱动程序等方式呈现。现今嵌入式系统大多数的产品仍然以低级的8位处理器配合少量的内存与电路来作控制,不过高级的嵌入式系统产品也逐渐增加。我们的这个ArmJVM即是运行于高级的嵌入式产品ARM7上。

1.2嵌入式系统的可移植性

嵌入式系统产品总是针对某个特定领域的应用而开发的,因此嵌入式系统不存在通常意义上的可移植性。这里我们所说的嵌入式系统的可移植性实际上指的是嵌入式开发平台缩提供的自动移植功能。

为了方便快捷的适应不同的目标系统,一个移植性良好的嵌入式开发平台必须具备两个特性:剪裁性和开放性。

剪裁性是指开发平台能提供多种可选的功能,应用开发者可以根据性能、功耗、体积等特征参数选用一些功能,舍弃一些功能,开发出规模合适的应用产品。从某种角度来说,剪裁性实际上是一种针对不同应用的移植性。

开放性提供了管理和维护平台的基本途径。开发平台是应用产品系统开发关键部分,它不仅是一种设计方法,也是一种技术管理的方法。研发部门希望不断积累的经验能以可见的知识保存起来,开发经验不会随着技术人员的流失而流失,这就要求开发平台在不断加强的同时符合一定的规范,从而定制出满足特定要求的嵌入式系统开发平台。

通常在底层的硬件驱动程序上,效率和可移植性是相互矛盾的,必须找到一个折衷。

2.      Java虚拟机概述

说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成: Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)。它们的关系如下图所示:[1]

      运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件)。最后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。从上图也可以看出Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序可以运行在这个平台上。这个平台的结构如下图所示:[1]

 

在Java平台的结构中, 可以看出,Java虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统, 其中依赖于平台的部分称为适配器;JVM通过移植接口在具体的平台和操作系统上实现;在JVM的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 可以在任何Java平台上运行而无需考虑底层平台, 就是因为有Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java的平台无关性。[1]

什么是Java虚拟机?Java虚拟机是运行所有Java程序的抽象计算机,它仅仅是由一个规范来定义的抽象的计算机。当提及“Java虚拟机”时,可能指的是如下三种不同的东西:

抽象规范

一个具体的实现

一个运行中的虚拟机实例[2]

Java虚拟机负责Java程序设计语言的内存安全、平台无关和安全特性。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机(JVM)在多个平台上实现统一语言。Java之所以得以大行其道,除了它是一门面向对象、构造精美的语言之外,更重要的原因在于:它摆脱了具体机器的束缚,使跨越不同平台编写程序成为可能。

3.      项目背景

越是高级的语言,其编译和运行的系统开销就越大,应用程序也越大,运行越慢。因此一般来说,编程人员都会首选汇编语言和C语言,然后才会考虑C++语言或Java语言。

但是随着不断增长的市场需求,很多嵌入式设备必须适应网上交流的需要,为了迎合此要求,考虑到开发Internet应用程序的便利,众多开发者都发现使用Java语言是有意义的。而随着内存及32位处理器价格的下降,最初在嵌入系统使用Java太昂贵的问题不再有了,使用Java的成本开始减少。于是,Java在嵌入式领域迎来了新的机会。在嵌入式系统的应用开发中,研发人员需要面对许多新的设备与新的技术,同时也遇到种种限制,例如运行速度、内存配置、外形尺寸以及与时间相关的技术问题。Java具有良好的跨平台性、面向对象的特性、内在的Internet集成,因此获得了大批拥有雄厚技术实力的开发商,如Sun、MicroSoft等,可以帮助人们顺利地完成嵌入式系统的开发,而且它的优秀的面向对象特性、平台无关性和丰富的类库将大大方便开发人员的开发和调试,提高工作效率。可以相信,Java在嵌入式环境中的应用将越来越多,我们有必要研究Java技术核心—虚拟机,包括其结构、运行机制及虚拟机移植到不同的嵌入式系统平台的关键技术。

Java虚拟机(JVM)在多个平台上实现统一语言。Java之所以得以大行其道,除了它是一门面向对象、构造精美的语言之外,更重要的原因在于:它摆脱了具体机器的束缚,使跨越不同平台编写程序成为可能。

Java是当今最为有生命力的一种语言。由于它本来就是为嵌入式系统的设计所开发的,所以,同嵌入式Linux的结合也就顺利成章了。Java程序语言在其产生之初,就是为机顶盒设备设计的。后来,由于它在互联网上的出色表现,使它赢得了巨大的声誉和财富。现在它又回到自己原来的领地—嵌入式系统。

不过,现在用Java来开发嵌入式系统,产生了许多复杂的新问题:由于新设备、新技术的出现,使其在速度、内存、大小、和时间定义等方面面临着一些以前从没有遇到过的问题。

我们将在一个嵌入式设备—ARM上实现一个简单的Java虚拟机,这个Java虚拟机能在嵌入式设备有限资源的情况下执行Java程序。

当前,国内外关于嵌入式JVM的研究,一般是对其进行部分的优化,比如对垃圾回收机制进行改进,而且一般是对开源项目Kaffe的改进。并且都没有实际的可用产品,而嵌入式设备的内存和存储器容量都是有限的,因此对JVM程序本身的大小也有限制。因此,我们的项目将根据目标机器的实际需要进行开发,并根据平台本身做相应优化,根据需要对Java API进行剪裁和修改,以达到基于本平台的最大运行效率,得到我们自己需要的Java虚拟机程序。

4.      ArmJVM的功能

2       能够装载并解析java class文件

对于已经编译好的java class文件,能够读取该class文件的内容,装载该类,并保存在程序内部的数据结构中。当在程序运行的过程中需要解析该类时,进行解析,并替换符号引用为直接引用

2       在完成虚拟机的初始化后,能够找到main函数并执行程序错误!未找到引用源。]

对于指定的入口类,在虚拟机完成了初始化后,寻找该类的main()方法,如果找到,则执行该方法,否则抛出异常,虚拟机运行中止。

2       支持Java虚拟机规范中规定的200多个操作码的功能

实现了Java虚拟机的200多个操作码的功能,由此使MiniJavaVM这个虚拟机模拟Java虚拟机的功能成为可能,这200多个操作码包括:

         栈和局部变量操作指令

ü         将常量池入指令

ü         从栈中的局部变量中装载值指令

ü         将栈中的值存入局部变量指令

ü         通用栈操作指令

         类型转换指令

         整数运算指令

         逻辑运算指令

ü         移位操作指令

ü         按位布尔运算指令

         浮点运算指令

         对象和数组指令

ü         对象操作指令

ü         数组操作指令

         控制流指令

ü         条件分支指令

ü         比较指令

ü         无条件转移指令

ü         表跳转指令

         异常指令

         finally子句指令

         方法调用与返回指令

ü         方法调用指令

ü         方法返回指令

         线程同步指令

2       具有内存管理和垃圾收集机制

Java虚拟机对内存的管理使得java程序具有很高的安全性,程序员不用担心内存访问越界问题,也不用为在合适的时候释放分配的空间而费心。垃圾收集机制的存在解决何时回收不用的内存和如何回收内存的问题。

2       支持非本地方法调用

按照Java虚拟机规范中的要求来设置非本地方法的调用情况,包括参数压栈,分配局部变量空间,压入方法调用的栈桢等。

2       支持本地方法调用

Java虚拟机中所有与本地方法相关的部分都重新写过,以动态链接库的形式为MiniJavaVM工程提供支持。MiniJavaVM的本地方法只实现最基本的功能,不再负责虚拟机的安全机制。

2       支持异常处理

有了异常处理,就能够在程序运行时平稳处理意外情况。根据Java class文件中的异常表,MiniJavaVM程序支持所有的异常处理,并在不能解决异常时输出异常信息,虚拟机停止运行。

2       能够运行与I/O无关的完整Java程序,并提供参数供查看运行效果提供了-version,-showversion,–help,-?,–verbose命令。

l       version命令显示MiniJavaVM的版本信息,然后退出

l       showversion命令显示MiniJavaVM的版本信息,然后继续运行Java程序

l       help,-?命令显示帮助信息

l       verbose命令输出详细数据显示运行过程

第二章          开发平台及运行环境

1.      硬件平台

本项目采用的开发板为导师翁恺老师设计的,采用的CPU为EP7312,EP7312是为超低功率应用产品而设计的,例如要求数字音频解压的手持设备、互联网产品和小功率工业用控制器。该芯片的核心逻辑功能建立在一个ARM720T处理器上,其运行时钟速率达74和90兆赫,并带有8KB 4路联合并行处理统一高速缓存和写缓冲器。增强的存储器管理部件与ARM720T相结合,可支持成熟的操作系统,例如Microsoft Windows CE和Linux.

EP7312可进行超低功率运行。它的核心运行功率仅为2.5伏,它的输入输出运行范围在2.5至3.3伏之间。该芯片有三种基本电源状态: 运行、空转和待机。

EP7312集成了一个可与许多低成本、高质量的Cirrus Logic音频转换器直接连接的接口。

通过在高度集成的EP7312上简单地增加所需内存和外设,可完成一个小功率系统的解决方案。所有必需的逻辑接口都已集成在片上。

EP7312具有如下特性:

·   ARM7TDMI处理器和MMU支持Thumb模式

o      8KB 4路联合并行处理高速缓存

·   支持MP3、WMA、AAC、ADPCM、Audible等

·   48 KB 片上SRAM

·   用于数字版权管理或IP安全设计的32位唯一MaverickKey ID

·   74兆赫和90兆赫动态时钟速率

·   LCD控制器、中断控制器和启动ROM

·   IrDA、PWM (2) 和 16550 UART (2) 接口

·   实时时钟和两个通用16位计时器

·   集成外设接口

o      可与两个外部组合相连接的32位SDRAM接口

o      8/32/16位SRAM/Flash/ROM

o      非胶合数字音频加CODEC端口

o      两个同步串行接口(SSI1,SSI2)

o      8x8键盘扫描仪和专用LED断续开关(通过实时时钟控制)

o      27个通用输入/输出引脚

·   超低功耗

o      74 兆赫下90毫瓦

o      90 兆赫下108微瓦(典型值)

o      待机状态下小于0.03毫瓦

·   封装:208引脚 LQFP,256球 PBGA,204球 TFBGA

o      可适用于消费和工业温度条件

另外还在板上加入了CS8900 NIC IC的网卡。

EP7312的内核为ARM7TDMI,在ARM Linux下显示的情况如下:

Processor       : ARM ARM720T rev 2 (v4l)

BogoMIPS      : 65.33

Hardware       : ARM-Prospector720T

Revision        : 0000

Serial          : 0000000000000000

ARM是Advanced RISC Machine的缩写。

ARM芯片具有RISC体系的一般特点,如:具有大量的寄存器,绝大多数操作都在寄存器中进行,寻址方式简单,指令格式固定等等。此外,ARM还采用了一些特别的技术,以保证高性能的同时减小芯片的体积、降低芯片的功耗。

ARM7系列处理器是低功耗的32位RISC处理器。它主要用于对功耗和成本要求比较苛刻的消费类电子产品。ArmBoy就是在ARM7TDMI系列处理器上的一个可移植的BIOS和C语言标准函数库的实现。

结合了Thumb指令集的ARM7TDMI支持0.25、0.18、0.13微米的CMOS制造工艺,内核仅占1mm2不到。Thumb较好的处理了RISC处理器常见的代码大小问题。系统设计者可以充分利用32位RISC内核提供的高性能和大寻址范围。这使得应用开发可以提高功能和性能而保持有竞争性的系统开销和功耗。ARM7TDMI还集成了ARM的EmbeddedICE JTAG软件调试逻辑,使用ARM的软件开发工具包和Multi-ICE接口。EmbeddedICE逻辑允许源代码级调试、代码下载和数据断点,使得编程和调试过程非常的简便快捷。

ARM7TDMI处理器核的主要性能特点如下:

内核:32位的RISC体系结构;3级流水线(见下图);32位ALU,并带有高性能硬件乘法器。

存储系统:冯·诺依曼体系结构,指令与数据共用一个存储区,不区分指令和数据总线;32位同一指令/数据总线,简化了SoC集成工艺。

指令系统:采用ARMv4T版本的指令集体系结构,支持32位ARM指令集和16位Thumb指令集;ARM和Thumb之间可以无缝切换,不会增加额外的开销;支持协处理器指令和扩展接口。

片上和在线调试:支持扩展调试功能,包括EmbeddedICE、JTEG、ETM。

2.      软件平台

由于我们的最终软件要运行于ARM Linux,因此我认为使用Linux作为我们的开发平台是最佳选择,这样我们可以先实现运行于X86 PC Linux的可执行程序,然后再通过交叉编译环境得到运行于目标ARM Linux平台的可执行代码。最终我们所采用的软件开发平台为Linux和GCC工具链,GCC的ARM交叉编译器工具链版本为2.95.2,基于x86 PC的编译器使用了GCC 3.4和GCC 3.2.2 20030222 (Red Hat Linux 3.2.2-5)两个版本编译运行通过。

针对嵌入式系统来说,其编译器数不胜数。其中,gcc和汇编器as是非常优秀的变异工具,而且免费。当作为交叉变异工具使用时,gcc支持很多种的平台和宿主机-目标及的组合,当然也包括所使用的X86PC+Red Hat Linux——arm7+Arm Linux的组合形式。GCC作为GNU工作集的一个组成部分,与源码级调试器gdb仪器工作,提供了在开发一个嵌入式Linux系统中要用到的所有软件工具。

对于使用交叉编译器的基本工作过程如下图:

3.      运行环境

我们的代码将最终运行于ARM Linux中,在我们的项目中,测试平台为:

ARMLinux

Linux 2.4.3-rmk1-wk1 on EP7312

其中CPU为EP7312,内核为ARM720T,使用16M的RAM和4M的Flash ROM,由于Flash的比较小,因此还使用了nfs,一些程序和动态链接库存在了nfs上,只有重要的库文件烧到了Flash上。

第三章          实现方案

1.      原有代码MiniJavaVM概述

本项目使用了部分我们实验室上一届师兄所写的一个Java虚拟机——MiniJavaVM代码,原有代码的运行环境如下:

开发平台:Windows XP/2003

开发语言:ANSI C/C++

开发工具:Visual Studio C++/ Visual Studio.net

运行平台:Windows XP/2000/2003

MiniJavaVM的框架合理地组织了虚拟机运行时所需的各模块,将各模块的输入与输出有效地结合在一起,使这些模块组合在一起完成了Java虚拟机的功能。这些模块包括:命令参数解析模块、类的装载和解析模块、内存管理模块、执行引擎模块、方法调用模块、异常处理模块、多线程处理模块(未完成)。这个MiniJavaVM总的组织方式如图2.2.1所示。

其中除命令参数解析模块外,其他模块一起构成了完整的MiniJavaVM虚拟机,这些模块之间协同合作,完成了虚拟机的功能。其中命令参数解析模块负责解析命令,根据MiniJavaVM后的参数来设定虚拟机的运行模式及输出信息;类的装载和解析模块能从class文件或是rt.jar文件中装载指定名称的Java类,并采用迟解析的方式在需要时解析该类,类的信息维护在虚拟机的一个数据结构中;内存管理模块负责为类的实例及静态字段分配空间,并在虚拟机内维护类的实例和静态字段,当虚拟机空间不足时会启动垃圾回收机制来回收内存;执行引擎模块负责解释执行200多个操作码,解释的过程包括对栈桢、栈、PC、局部变量区的修改;多线程处理模块负责维护虚拟机内的表示线程的数据结构,在语言级提供多线程支持;方法调用模块负责处理方法调用过程,对于非本地方法,包括找到调用方法的指针,新建栈桢、将方法参数设置在新栈桢的局部变量区,调用方法并将返回值压栈的过程,对于本地方法,包括找到调用方法的指针,将方法参数用汇编的方式压栈,调用本地方法并将返回值压栈的过程;异常处理模块负责处理虚拟机抛出的异常,记录异常产生处的异常信息,并试图通过查找当前方法的异常表来处理异常信息,如果能够通过异常表找到处理异常的代码,则修改PC的值使虚拟机处理当前异常,否则,当虚拟机不能处理该异常时,输出异常信息,然后终止虚拟机的运行。通过这几个模块的协同合作, MiniJavaVM虚拟机能够很好地模拟Java虚拟机的功能。

其中,分别有三个工程:JavaNativeCall,JavaVM,jvm,JavaNativeCall这个工程为JNI部分,即本地代码接口部分,被编译成dll动态链接库文件供JavaVM工程显式调用。而JavaVM也被编译为dll动态链接文件,供jvm工程隐式调用,即静态调用,JavaVM是虚拟机的主体部分,提供了虚拟机的所有功能的实际实现。Jvm工程供编译为最终可执行文件用的,实现了用户界面接口,并根据要求调用JavaVM来实现用户需求。

这个基于X86 WIN32平台的项目实现了本项目需要功能的大部分,因此我们使用了大量其中的代码,但是,毕竟X86 WIN32平台和ARM Linux平台有很大的区别,另外一些模块的效率较低,不能适应于目标机器的小内存,低频率以及使用NFS(网络文件系统)的特点。因此我们做了大量的修改和移植工作,其中包括:Makefile的编写,WINAPI函数的替换,动态链接库代码的修改和编译,ARM汇编的使用,编码转换模块的修改,JAR解压的重写,本地代码JNI的修改及添加等。

 

2.      代码修改重组

由于有一个现成的WIN32平台代码供使用,我们的系统框架基本依照MiniJavaVM的结构进行设计,并使用了其中大量平台无关的c++代码,对于其中平台相关的代码,首先进行分析,如能修改对其进行修改,不能的话重写。

3.      移植到X86 PC的Linux平台上

由于直接移植到ARM Linux平台的难度较大,因此我们首先将代码移植到了普通PC的Linux上,即作为宿主机器的Red Hat Linux。在此期间,会遇到大部分的平台相关问题。

4.      从X86 PC的Linux移植到ARM Linux

这个期间,主要涉及的问题有:交叉编译器的使用,汇编代码部分的重写。由于arm-linux-gcc的版本较老,还会有一些与Red Hat上的gcc不兼容的情况,另外还缺少一些动态库,需要自己交叉编译这些库到ARM Linux上。

5.      代码的优化

这个期间,首先测试代码的运行瓶颈,找到原因,然后改进效率低下的代码。

6.      移除对STL的使用

由于存储空间有限,为了减少代码对空间的占用,需要将代码中用到的STL库的部分自己重写,其中有vector,stack、map和list,由于其中使用的函数不多,因此只需要实现这些类里需要的功能。然后在编译中即可不将STL编译进来,可减少很大一部分的代码空间占用。

7.      API的剪裁

由于嵌入式系统的内存较小,而且使用了网络文件系统,若是API仍然用标准的,将会占用大量空间,占用大量的传输时间,而其中大量功能是嵌入式应用根本不需要的。因此要对API进行剪裁,并对一些常用类和方法进行重写,结合JNI的编写使其运行更快。这个阶段的前期和第3、4阶段同时进行,首先进行需求分析,选出需要的API包,并整理出需要的类和公用方法,保证在其他地方编译出来的只使用了这些API包的代码在本虚拟机上仍然能正常运行。

第四章          移植

1.      从Windows到Linux(X86下)

此过程主要涉及到两种不同的操作系统,以及不同的编译器,硬件都为X86平台,因此没有涉及到指令集问题。遇到的主要问题及其解决如下:

Makefile的编写

代码要从Windows的visual studio转到Linux使用gcc编译器,首先要做的当然是写好Makefile,毕竟我们不能每次都手工输入所有命令,在编译文件数量很多的情况,这几乎是不可能的。为此,我找了本关于怎么写Makefile的书,首先写好了本项目的makefile,这样,使得以后代码的修改测试更为方便了。

源文件代码模块的内部关系决定了源程序的编译和链接顺序。通过建立makefile可以描述模块间的相互依赖关系。Make命令从中读取这些信息,然后根据信息对程序进行管理和维护。在makefile里主要提供的是有关目的文件,即“target”与依靠文件,即“dependencies”之间的关系,还指明了用什么命令生成和更新目标文件。有了这些信息,make会检查磁盘上的文件,如果目的文件的时间标志(该文件生成或被改动时的时间)比它的任意一个依赖文件旧,make就执行相应的命令,以便更新目的文件。

因此,要写好makefile,首先要搞清楚程序内部和要生成的目标文件之间的依赖关系。延续之前windows里代码的结构,我们的重组后的项目总共有也是三个工程:JavaNativeCall,JavaVM和jvm,其中JavaNativeCall和JavaVM是编译成动态链接库文件的,通过jvm编译成最终可执行代码。通过分析,大致的依赖关系如下:JavaNativeCall工程作为jni(本地代码)部分相对独立,但是要依赖于JavaVM的一些.h文件,JavaVM也相对独立,但是jvm要依赖由JavaVM所生成的动态链接文件,所以最终的编译顺序为:JavaNativeCall,JavaVM,jvm,于是对每个工程分别写了个makefile,再在他们的上层目录写了一个总的makefile,对于每个文件的依赖关系,使用GCC的-M选项可以得到每个cpp文件所依赖的文件。

CC = g++

soureces := $(wildcard *.cpp)

dd=$(soureces:%.cpp=%.d)

do=$(soureces:%.cpp=%.o)

MiniJava.so: $(do)

        $(CC) -shared -Wl,-export-dynamic -o $@ $(do); cp MiniJava.so /usr/lib/;

gen: $(dd)

%.d: %.cpp

        @set -e; rm -f $@; \

$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \

sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \

echo "  "$(CC) -c $*.cpp >> $@; \

rm -f $@.$$$$

clean:

        rm *.d*;rm *.o;rm test.so

动态链接库的使用

动态库(Dynamic Link Library,DLL)技术是程序设计中经常采用的技术。其目的减少程序的大小,节省空间,提高效率,具有很高的灵活性。采用动态库技术对于升级软件版本更加容易。与静态库(Static Link Library)不同,动态库里面的函数不是执行程序本身的一部分,而是根据执行需要按需载入,其执行代码可以同时在多个程序中共享。

在Windows和Linux操作系统中,都可采用这种方式进行软件设计,但他们的调用方式以及程序编制方式不尽相同。

Windows动态库技术

动态链接库是实现Windows应用程序共享资源、节省内存空间、提高使用效率的一个重要技术手段。常见的动态库包含外部函数和资源,也有一些动态库只包含资源,如Windows字体资源文件,称之为资源动态链接库。通常动态库以.dll,.drv、.fon等作为后缀。相应的windows静态库通常以.lib结尾,Windows自己就将一些主要的系统功能以动态库模块的形式实现。

Windows动态库在运行时被系统加载到进程的虚拟空间中,使用从调用进程的虚拟地址空间分配的内存,成为调用进程的一部分。DLL也只能被该进程的线程所访问。DLL的句柄可以被调用进程使用;调用进程的句柄可以被DLL使用。DLL模块中包含各种导出函数,用于向外界提供服务。DLL可以有自己的数据段,但没有自己的堆栈,使用与调用它的应用程序相同的堆栈模式;一个DLL在内存中只有一个实例;DLL实现了代码封装性;DLL的编制与具体的编程语言及编译器无关,可以通过DLL来实现混合语言编程。DLL函数中的代码所创建的任何对象(包括变量)都归调用它的线程或进程所有。

根据调用方式的不同,对动态库的调用可分为静态调用方式和动态调用方式。

(1)静态调用,也称为隐式调用,由编译系统完成对DLL的加载和应用程序结束时DLL卸载的编码(Windows系统负责对DLL调用次数的计数),调用方式简单,能够满足通常的要求。通常采用的调用方式是把产生动态连接库时产生的.LIB文件加入到应用程序的工程中,想使用DLL中的函数时,只须在源文件中声明一下。 LIB文件包含了每一个DLL导出函数的符号名和可选择的标识号以及DLL文件名,不含有实际的代码。Lib文件包含的信息进入到生成的应用程序中,被调用的DLL文件会在应用程序加载时同时加载在到内存中。

(2)动态调用,即显式调用方式,是由编程者用API函数加载和卸载DLL来达到调用DLL的目的,比较复杂,但能更加有效地使用内存,是编制大型应用程序时的重要方式。

Linux共享对象技术

在Linux操作系统中,采用了很多共享对象技术(Shared Object),虽然它和Windows里的动态库相对应,但它并不称为动态库。相应的共享对象文件以.so作为后缀,为了方便,在本文中,对该概念不进行专门区分。Linux系统的/lib以及标准图形界面的/usr/X11R6/lib等目录里面,就有许多以so结尾的共享对象。同样,在Linux下,也有静态函数库这种调用方式,相应的后缀以.a结束。Linux采用该共享对象技术以方便程序间共享,节省程序占有空间,增加程序的可扩展性和灵活性。Linux还可以通过LD-PRELOAD变量让开发人员可以使用自己的程序库中的模块来替换系统模块。

同Windows系统一样,在Linux中创建和使用动态库是比较容易的事情,在编译函数库源程序时加上-shared选项即可,这样所生成的执行程序就是动态链接库。通常这样的程序以so为后缀,在Linux动态库程序设计过程中,通常流程是编写用户的接口文件,通常是.h文件,编写实际的函数文件,以.c或.cpp为后缀,再编写makefile文件。对于较小的动态库程序可以不用如此,但这样设计使程序更加合理。

编译生成动态连接库后,进而可以在程序中进行调用。在Linux中,可以采用多种调用方式,同Windows的系统目录(..\system32等)一样,可以将动态库文件拷贝到/lib目录或者在/lib目录里面建立符号连接,以便所有用户使用。下面介绍Linux调用动态库经常使用的函数,但在使用动态库时,源程序必须包含dlfcn.h头文件,该文件定义调用动态链接库的函数的原型。

(1)_打开动态链接库:dlopen,函数原型void *dlopen (const char *filename, int flag); dlopen用于打开指定名字(filename)的动态链接库,并返回操作句柄。

(2)取函数执行地址:dlsym,函数原型为: void *dlsym(void *handle, char *symbol); dlsym根据动态链接库操作句柄(handle)与符号(symbol),返回符号对应的函数的执行代码地址。

(3)关闭动态链接库:dlclose,函数原型为: int dlclose (void *handle);
dlclose用于关闭指定句柄的动态链接库,只有当此动态链接库的使用计数为0时,才会真正被系统卸载。

(4)动态库错误函数:dlerror,函数原型为: const char *dlerror(void); 当动态链接库操作函数执行失败时,dlerror可以返回出错信息,返回值为NULL时表示操作函数执行成功。

在取到函数执行地址后,就可以在动态库的使用程序里面根据动态库提供的函数接口声明调用动态库里面的函数。在编写调用动态库的程序的makefile文件时,需要加入编译选项-rdynamic和-ldl。

除了采用这种方式编写和调用动态库之外,Linux操作系统也提供了一种更为方便的动态库调用方式,也方便了其它程序调用,这种方式与Windows系统的隐式链接类似。其动态库命名方式为“lib*.so.*”。在这个命名方式中,第一个*表示动态链接库的库名,第二个*通常表示该动态库的版本号,也可以没有版本号。在这种调用方式中,需要维护动态链接库的配置文件/etc/ld.so.conf来让动态链接库为系统所使用,通常将动态链接库所在目录名追加到动态链接库配置文件中。如具有X window窗口系统发行版该文件中都具有/usr/X11R6/lib,它指向X window窗口系统的动态链接库所在目录。为了使动态链接库能为系统所共享,还需运行动态链接库的管理命令./sbin/ldconfig。在编译所引用的动态库时,可以在gcc采用 –l或-L选项或直接引用所需的动态链接库方式进行编译。在Linux里面,可以采用ldd命令来检查程序依赖共享库。

两种系统动态库比较分析

Windows和Linux采用动态链接库技术目的是基本一致的,但由于操作系统的不同,他们在许多方面还是不尽相同,下面从以下几个方面进行阐述。

(1)动态库程序编写,在Windows系统下的执行文件格式是PE格式,动态库需要一个DllMain函数作为初始化的人口,通常在导出函数的声明时需要有_declspec(dllexport)关键字。Linux下的gcc编译的执行文件默认是ELF格式,不需要初始化入口,亦不需要到函数做特别声明,编写比较方便。

(2)动态库编译,在windows系统下面,有方便的调试编译环境,通常不用自己去编写makefile文件,但在linux下面,需要自己动手去编写makefile文件,因此,必须掌握一定的makefile编写技巧,另外,通常Linux编译规则相对严格。

(3)动态库调用方面,Windows和Linux对其下编制的动态库都可以采用显式调用或隐式调用,但具体的调用方式也不尽相同。

(4)动态库输出函数查看,在Windows中,有许多工具和软件可以进行查看DLL中所输出的函数,例如命令行方式的dumpbin以及VC++工具中的DEPENDS程序。在Linux系统中通常采用nm来查看输出函数,也可以使用ldd查看程序隐式链接的共享对象文件。

(5)对操作系统的依赖,这两种动态库运行依赖于各自的操作系统,不能跨平台使用。因此,对于实现相同功能的动态库,必须为两种不同的操作系统提供不同的动态库版本。

最后,我们根据这些方面对动态链接库调用的代码进行了重写。

对于显式调用方式,需要改的地方主要是代码,然后编译时加上适当的参数:

void * NativeMethod_access::m_hDll=NULL;

 

NativeMethod_access::NativeMethod_access(void)

{

        if(!m_hDll){

                g_vout<<"Loading Library \""<<DLL_MODULE_NAME<<"\"...\n";

                m_hDll=dlopen(DLL_MODULE_NAME, RTLD_NOW);

                if(!m_hDll){

                        cout<<"Error~! Can not load library \""

<<DLL_MODULE_NAME<<"\"!\n";

                        WRONG_HANDLE(__FILE__,__LINE__);

                }

                else{

                        g_vout<<"Loading Library \""

<<DLL_MODULE_NAME<<"\" success!\n";

                }

        }

}

 

string NativeMethod_access::GetNativeMethodName(method_info* pMethod_info)

{

        string retStr="_MiniJava";

        int begin=0,end=0;

        string szClassName=pMethod_info->m_pClassFile->m_szThis_class;

        while((end=szClassName.find('/',begin))!=string::npos){

                retStr += "_"+szClassName.substr(begin,end-begin);

                begin=end+1;

        }

        retStr += "_"+szClassName.substr(begin,szClassName.length()-begin) + "_"+pMethod_info->m_szMethodName;

        return retStr;

}

然后,在NativeMethodExecute函数里,使用下面代码即可根据所需方法函数名获得它的函数地址,即一个void *指针,再根据jvm所知道的这个函数的类型强制转换成合适的函数指针进行使用:

 

JavaVM* pJVM=GetJVM();

        char * err;

        pJVM->PushMethodInfo(pMethod_info);

        string szNativeMethodName=GetNativeMethodName(pMethod_info);

        g_vout<<"Executing Native method:"<<szNativeMethodName<<":"<<pMethod_info->m_szDescriptor<<"...\n";

        if(!m_hDll){

                cout<<"Error~! Can not load library: "

<<DLL_MODULE_NAME<<"\n";

                WRONG_HANDLE(__FILE__,__LINE__);

                return false;

        }

        native_function nfp;

        native_value nv;

        cout<<szNativeMethodName.c_str()<<endl;

        dlerror(); /* clear error code */

        void * proc=dlsym(m_hDll,szNativeMethodName.c_str());

        if((err = dlerror()) != NULL){

                cout<<err<<endl;

                cout<<"Error~! Can not get proc address: "

<<szNativeMethodName.c_str()<<"\n";

                WRONG_HANDLE(__FILE__,__LINE__);

                return false;

        }

另外,由于本地方法代码是用c++写的,在编译后编译器在符号表里保存的并不是原来的函数名,而是含有其他类型信息之类,因此一开始我们在获得函数的时候总是失败,说是动态库中找不到这个函数。在知道原因后,将动态库的函数名申明加上extern “C”关键字就能够成功装载了。

对于隐式链接,代码不需要做很大的修改,只需要在编译动态库时加上参数-shared,编译成libJavaVM.so文件,然后在使用动态库的地方编译时指定so文件的位置并用-lJavaVM参数。在我们的项目中,把生成的动态库文件直接复制到了/usr/lib/目录下,因此在用的时候就不需要指定库文件路径了。上面关于makefile的章节中的makefile内容已经包括动态链接库的编译了,这里再看看使用动态链接库部分的makefile:

CC = g++

soureces := $(wildcard *.cpp)

dd=$(soureces:%.cpp=%.d)

do=$(soureces:%.cpp=%.o)

jvm: $(do) /usr/lib/libJavaVM.so

        $(CC) -rdynamic -o $@ $(do) -lJavaVM;cp jvm ../bin/;

gen: $(dd)

%.d: %.cpp

        @set -e; rm -f $@; \

$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \

sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \

echo "  "$(CC) -c $*.cpp >> $@; \

rm -f $@.$$$$

clean:

        rm *.d*;rm *.o;rm test.so

编码问题

对于系统移植,不可必免的要遇到编码的问题,特别是c/c++程序,由于没有标准的编码处理库,因此不同操作系统提供的API也不同,即使是对于unicode,Windows和Linux的内部表示都不同,Windows的wchar_t是2个字节表示的,而在Linux则是4字节表示。

由于java内部文件是使用utf8编码的,而java的char内部表示是16位的unicode,而我们要显示给用户的UI部分是本地系统所选的编码。因此我们要解决的问题有两个:

1.我们要把java内部文件的utf8编码在装载进来时转成16位的unicode表示,对于每个char应该都是一个16位的unicode表示。

2.把要显示给用户看的字符转成用户在本地所选择的编码,对于Linux用户来说,是通过locale来设定自己的编码的,比如一般中文用户会使用zh_CN.gbk或者zh_CN.utf8,因此,我们要通过系统提供的API将unicode表示的字符串转化成本低编码进行显示。

对于API,我们找到了和原项目里类似的几个函数:

wcstombs,mbstowcs,wcslen,wcscpy

但是,我们发现,由于java的char是16位unicode,而Linux的wchar_t(也是unicode)是32位的,而这几个函数的参数都要求是wchar_t的unicode,于是不能直接使用这几个API函数。

man了一下unicode,发现linux里的wchar_t说是UCS code,而查msdn发现windows也说它16位的WCHAR(也就是wchar_t)是unicode:

WCHAR 16-bit Unicode character. For more information, see Character Sets Used by Fonts. This type is declared in WinNT.h as follows:

typedef wchar_t WCHAR;

然后在MSDN找到的说法是:

To address the problem of multiple coding schemes, the Unicode standard for data representation was developed. A 16-bit character coding scheme, Unicode can represent 65,536 (2^16) characters, which is enough to include all languages in computer commerce today, as well as punctuation marks, mathematical symbols, and room for expansion. Unicode establishes a unique code for every character to ensure that character translation is always accurate.

不过对于我要转换的java中的utf8编码来说,在utf8中只是下面表的前三排,也就是说高的2个字节都是0了,而且看到有文章说老的16位unicode在新的32位里面也是高2个字节为0,那就可以把原先使用16位的东西前面补0当成wchar_t传给wcstombs这几个函数了。

U-00000000 - U-0000007F: 0xxxxxxx

U-00000080 - U-000007FF: 110xxxxx 10xxxxxx

U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx

U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

但是,如果这样做,就要先将java的char类型字符串通过高16位补0复制到另外一个32位的wchar_t数组中,这个复制的过程会使程序的效率降低。通过分析,发现:

1        原先代码使用Windows的WideCharToMultiByter部分,即16位unicode转为本地编码的过程并不需要整个字符串一起处理,因为这个地方本身是要将unicode字符串转化后放到另一个存放这个字符串本地编码的数组中,可以一个个字符处理,取一个unicode字符,扩充成32位,使用wctowb函数转成本地编码,放到目的地址中,而不需要另外一个临时字符串来保存,代码也就修改成了这样:

java_char* s=szJavaCharDst+nJavaCharLen;

nJavaCharLen++;

memcpy(s,szMultiByteDst+j-2,2);

wchar_t tmp = (wchar_t)((int)(*s));

wctomb(szMultiByteDst+j-2,tmp);

2        对于,wcslen和wcscpy如果要先将16位unicode的串复制成32位的串就显得太浪费了,因此,最后决定自己写一个wcslen和wcscpy,直接处理16unicode的java char数组:

size_t JavaCharString::wcslen(const java_pcstr jpcstr)

{

        size_t len = 0;

        while (jpcstr[len] != 0)

        {

                if (jpcstr[++len] == 0)

                        return len;

                ++len;

        }

        return len;

}

java_pstr JavaCharString::wcscpy(java_pstr dest, java_pstr src)

{

        java_pstr wcp = dest;

        java_char c;

        do

        {

                c = *src++;

                *wcp++ = c;

        }

        while (c != 0);

        return dest;

}

Visual Studio C++与GCC对于c++的支持不同

对于这个问题,遇到的主要是一些c++标准没规定从而编译器支持不同的东西或是有些东西用了VC或GCC不支持的标准。对于VC不支持的标准,原先的代码使用了申明一个函数会抛出某种异常来增强程序安全性,然而由于VC不支持而忽略了这部分代码,但是这个代码写错了也就没被发现,因此在移植时进行了修改。还有其他问题列举一些如下:

1 原代码使用了编译器间不兼容的#pragma编译选项,比如#pragma once,而没有用#ifndef来防止编译时重复引用.h文件,于是需要改成#ifndef的方式。

2 一些函数的参数描述不同,比较多的是const问题,GCC的库对这部分要求比较高。

3 标准的实现不一样,其中,遇到的最大问题就是在:stack有个成员变量c,在VC下是public的,而在GCC中是protected的,而在代码中很多地方都用了这个变量,我们最后采取的办法是:

将类似

for(int i=size-1;i>=0;i--){

   method_info* pMethod_info=m_stackInvokeMethod.c[i];

   g_vout<<"\t"<<pMethod_info->m_pClassFile->m_szThis_class<<":"

<<pMethod_info->m_szMethodName<<": "

<<pMethod_info->m_szDescriptor<<"\n";

}

的代码换成类似下面的代码:

        for(int i=size-1;i>=0;i--){

//              method_info* pMethod_info=m_stackInvokeMethod.c[i];

                method_info* pMethod_info=tmp_stackInvokeMethod.top();

                tmp_stackInvokeMethod.pop();

                g_vout<<"\t"<<pMethod_info->m_pClassFile->m_szThis_class<<": "<<pMethod_info->m_szMethodName<<": "<<pMethod_info->m_szDescriptor<<"\n";

        }

4 其他一些类似__int64换成long long,以及使用的变量与gcc内部库函数重名等问题。都比较好解决。

GCC行内汇编

由于需要在知道函数名和参数的时候通过动态链接库的显式调用直接调用本地方法,而函数的参数类型组合很多,我们不可能根据函数和参数动态地将获得的函数动态地强制转化为合适的函数形式,因此,我们使用的方法是直接将所有函数申明为无参数的,再使用行内汇编将参数压栈,从函数返回后再将栈平衡。

在GCC中,对于X86行内汇编的写法也与在VC下不同,这里使用的是AT&T汇编,另外GCC也提供了很多其他东西供行内汇编使用,简单介绍如下:

基本的内嵌汇编很简单,一般是按照下面的格式
   asm(statements);

  例如:asm(nop); asm(cli);

   asm 和 __asm__是完全一样的.

  如果有多行汇编,则每一行都要加上 nt例如:

   asm( pushl %eaxnt

   movl ,%eaxnt

   popl %eax);

   实际上gcc在处理汇编时,是要把asm(...)的内容打印到汇编文件中,所以格式控制字符是必要的.再例如:

   asm(movl %eax,%ebx);

   asm(xorl %ebx,%edx);

   asm(movl ,_booga);

   在上面的例子中,由于我们在行内汇编中改变了edx和ebx的值,但是由于gcc的特殊的处理方法,即先形成汇编文件,再交给gas去汇编,所以gas并不知道我们已经改变了edx和ebx的值,如果程序的上下文需要edx或ebx作暂存,这样就会引起严重的后果.对于变量_booga也存在一样的问题.为了解决这个问题,就要用到扩展的行内汇编语法:

扩展的行内汇编类似于watcom.

    基本的格式是:

    asm ( statements : output_regs : input_regs : clobbered_regs);

    clobbered_regs指的是被改变的寄存器.

最后,汇编部分的代码是这么写的:

//将调用函数时用到的参数入栈,必须写为宏,不能封装成函数

#define PUSH_METHOD_PARAM(pMethod_info,size,args) \

{ \

     int j=0;  \

     for(int i=0;i<pMethod_info->m_nMethodArgs;i++){ \

         switch(pMethod_info->m_typeArgs[i]){ \

              case Type_Java_Unknown: \

              WRONG_HANDLE(__FILE__,__LINE__);        /*出错了*/                                                                      \

              break; \

              case Type_Java_Long:  \

              {  \

                   size+=8; \

                   java_long l=0; \

                   word* w=(word*)&l; \

                   w[1]=args[j++]; \

                   w[0]=args[j++]; \

                   ASM_PUSH_JAVALONG(l) \

              } \

              break; \

              case Type_Java_Double: \

              { \

                   size+=8;\

                   java_double d=0;\

                   word* w=(word*)&d; \

                   w[1]=args[j++];\

                   w[0]=args[j++]; \

                   ASM_PUSH_JAVADOUBLE(d) \

              } \

              break; \

              default: \

              { \

                   word w=args[j++]; \

                   size+=4; \

                   ASM_PUSH_JAVAWORD(w)  \

              }  \

         }  \

     }\

}

#define ASM_PUSH_JAVALONG(var)                          \

     __asm__(                                                        \

     "leal   %0,%%eax\n"                     \

     "movl   4(%%eax),%%eax\n"       \

     "pushl  %%eax\n"                        \

     "movl   %0,%%eax\n"     \

     "pushl  %%eax\n"                        \

     :                                                       \

     :"m"(var)                                       \

     );

 

 

#define ASM_PUSH_JAVAWORD(var)                          \

     __asm__(                                                        \

     "movl   %0,%%eax\n"                     \

     "pushl  %%eax\n"                        \

     :                                                       \

     :"m"(var)                                       \

     );

#define ASM_PUSH_JAVADOUBLE(var)                        \

     __asm__(                                                        \

     "fldl   %0\n"                           \

     "subl   $8,%%esp\n"                     \

     "fstpl  (%%esp)\n"                      \

     :                                                       \

     :"m"(var)                                       \

     );

#define ASM_END_CALL(bsize)                             \

     __asm__(                                                        \

     "addl   $"bsize",%esp"                  \

     );

 

2.      从X86 Linux到ARM Linux

在这一过程中,涉及到交叉编译器的使用,硬件不同导致的指令集不同,嵌入式系统和PC系统的不同,资源的不同等,遇到的问题及解决如下:

ARM指令集及汇编代码的重写

由于这个阶段CPU不再是intel的X86体系,原来的汇编代码完全不能使用了。在arm-linux-gcc中,内嵌汇编的格式基本还是一样的,它的输入输出操作符如下表:

Constraint

Used for

Range

F

Floating point registers

 

I

Immediate operands

8 bits, possibly shifted.

J

Indexing constants

-4095 .. 4095

K

Negated value in rhs

-4095 .. 4095

L

Negative value in rhs

-4095 .. 4095

M

For shifts.

0..32 or power of 2

R

General registers

 

 

Modifier

Specifies

=

Write-only operand, usually used for all output operands.

+

Read-write operand (not supported by inline assembler)

&

Register should be used for output only

 

ARM微处理器的指令的分类与格式如下:

ARM微处理器的指令集是加载/存储型的,也即指令集仅能处理寄存器中的数据,而且处理结果都要放回寄存器中,而对系统存储器的访问则需要通过专门的加载/存储指令来完成。

ARM微处理器的指令集可以分为跳转指令、数据处理指令、程序状态寄存器(PSR)处理指令、加载/存储指令、协处理器指令和异常产生指令六大类,具体的指令及功能如下表所示(表中指令为基本ARM指令,不包括派生的ARM指令)。

ARM指令及功能描述

助记符

指令功能描述

ADC

带进位加法指令

ADD

加法指令

AND

逻辑与指令

B

跳转指令

BIC

位清零指令

BL

带返回的跳转指令

BLX

带返回和状态切换的跳转指令

BX

带状态切换的跳转指令

CDP

协处理器数据操作指令

CMN

比较反值指令

CMP

比较指令

EOR

异或指令

LDC

存储器到协处理器的数据传输指令

LDM

加载多个寄存器指令

LDR

存储器到寄存器的数据传输指令

MCR

从ARM寄存器到协处理器寄存器的数据传输指令

MLA

乘加运算指令

MOV

数据传送指令

MRC

从协处理器寄存器到ARM寄存器的数据传输指令

MRS

传送CPSR或SPSR的内容到通用寄存器指令

MSR

传送通用寄存器到CPSR或SPSR的指令

MUL

32位乘法指令

MLA

32位乘加指令

MVN

数据取反传送指令

ORR

逻辑或指令

RSB

逆向减法指令

RSC

带借位的逆向减法指令

SBC

带借位减法指令

STC

协处理器寄存器写入存储器指令

STM

批量内存字写入指令

STR

寄存器到存储器的数据传输指令

SUB

减法指令

SWI

软件中断指令

SWP

交换指令

TEQ

相等测试指令

TST

位测试指令

当处理器工作在ARM状态时,几乎所有的指令均根据CPSR中条件码的状态和指令的条件域有条件的执行。当指令的执行条件满足时,指令被执行,否则指令被忽略。

每一条ARM指令包含4位的条件码,位于指令的最高4位[31:28]。条件码共有16种,每种条件码可用两个字符表示,这两个字符可以添加在指令助记符的后面和指令同时使用。例如,跳转指令B可以加上后缀EQ变为BEQ表示“相等则跳转”,即当CPSR中的Z标志置位时发生跳转

它有如下寄存器可用:

Register

Alt. Name

Usage

r0

a1

First function argument
Integer function result
Scratch register

r1

a2

Second function argument
Scratch register

r2

a3

Third function argument
Scratch register

r3

a4

Fourth function argument
Scratch register

r4

v1

Register variable

r5

v2

Register variable

r6

v3

Register variable

r7

v4

Register variable

r8

v5

Register variable

r9

v6
rfp

Register variable
Real frame pointer

r10

sl

Stack limit

r11

fp

Argument pointer

r12

ip

Temporary workspace

r13

sp

Stack pointer

r14

lr

Link register
Workspace

r15

pc

Program counter

由于我们主要使用汇编代码来传参数,我们主要研究了ARM的函数调用传参方式:

在ARM中,一般使用b或bl或直接写pc寄存器进行函数调用。过程栈是典型的从高位到地位地址建立的。栈指针(sp)定义当前帧的底端,而帧指针(fp)定义最后一个帧的底端。只有在执行过程中,栈帧随着过程而增长时fp在技术上是必须的。过程可通过向对于sp的寻址引用帧中元素。当一个新的过程被调用时,改变sp和fp将另外一个帧压到栈中。

ARM过程调用标准(APCS)是一个典型的过程联接机制的很好例证。尽管栈帧位于主存中,但理解如何使用寄存器是理解这种机制的关键,现说明如下:

r0到r3被用于把参数传递给过程。r0也用于保存返回值。如果参数需要的字节数超过4个寄存器能容纳的字节数,它们将置于栈帧上。

r4到r7保存寄存器变量。

r11是帧指针,而r13是栈指针。

r10保存栈容地址上限,用于检查栈的溢出。

其他寄存器在该协议中有另外的用途。

在做这部分的过程中,我首先写了几个小程序使用arm-linux-gcc编译成汇编代码,观察gcc是怎么做函数调用及参数压栈及sp,fp指针处理的。然后写好传参数需要用的宏,并在这些小程序中充分测试,然后才放到了我们的armjvm代码中。

另外,我们重新分析了原代码,发现并不需要对每个参数的类型做判断再分别处理每个参数,由于传参数只是把前面的16个字节放到r0到r3种,其他的按顺序放到栈上,因此我们直接将参数的每个字节循环处理,代码量也大大缩小了,代码如下:

//将调用函数时用到的参数入栈,必须写为宏,不能封装成函数

#define PUSH_METHOD_PARAM(size,args,a,b)\

{\

     word temp1,temp2;\

     if (size > 0) temp1 = args[size-1];\

     if (size > 1) temp2 = args[size-2];\

     for(int i=0;i<size-2;i++)\

         {\

         __asm__(\

         "@------------PUSH METHOD PARAM------------------\n\t"\

         "sub sp, sp,#4\n\t"\

         "str %0,[sp,#0]\n\t"\

         : :"r"(args[i])\

         );\

         }\

         ASM_PASS_WORD("0", a);\

         ASM_PASS_WORD("1", b);\

         if (size>0)ASM_PASS_WORD("2", temp1);\

         if (size>1)ASM_PASS_WORD("3", temp2);\

}

 

#define ASM_PASS_WORD(num,var) \

     __asm__(\

     "mov r"num",%0\n"\

     "@------------------END PUSH-----------------\n\t"\

     : :"r"(var)\

     )

 

动态链接库定位问题

在使用的动态链接库的时候,编译器在连接过程中会将动态库的位置信息保留到调用动态库的代码中,在运行时使用动态库的函数时系统会有个重定位过程,从而找到函数的具体地址。然而,在我们将代码移植到arm机器上时,遇到了如下的错误:

./MiniJava.so: error in loading shared libraries: R_ARM_PC24 relocation out of range

说是超过24位PC跳转的范围,24位PC的寻址范围是26位的字节数,即64M,而我们的代码远远小于这个数。因此应该有其他原因。

经过上网查找很多的资料,都说在编译时加上-fPIC参数就可以了,也就是让编译器生成Position Independent Code,从而避免重定位时出问题。

但是我这么做以后还是有上面那个错误,在http:///faq.html#2.24 中有人提到这是libc库的一个bug,但是可以用-fPIC参数解决,并提供了一个补丁。然而在我们的系统中,换掉libc库不太现实。还有人提到这是连接器ld对于c++的一个bug,经我们试验,使用c写的动态链接库的确没问题,后面的c写的unzip库编译出来的也是没问题的,它使用的同样是-fPIC参数,编译出来的库也比我们的大,因此我们认为这个应该是c++的问题。因此我们决定使用另一个解决方案:我们在编译动态链接库时使用了-nostdlib参数使它不要把c++库编译进代码,但是这样会又很多的unresolved reference错误。于是,在使用动态库的工程即jvm工程编译时加入-Wl,-export-dynamic和-lgcc参数,关于-Wl,-export-dynamic参数的作用如下:

-export-dynamic

当创建 ELF 文件时,把所有符号加入动态符号表,一般说来, 动态符号表 只包含动态目标库(dynamic object)需要的符号。用 dlopen 的时候需要这个选项。

这样,在编译main的时候使用-Wl,-export-dynamic选项使动态装载的so文件能够使用主程序里符号引用的东西,从而能够正常执行动态链接程序。

所需库的编译

一开始,我们使用的是开源的miniunz代码进行jar文件的解压缩,这个代码需要用到libz库,而我们的Arm Linux上并没有这个库,因此要编译一个在arm linux能用的libz库,因此先用

prefix=/usr/local/arm-linux/arm-linux/ CC=arm-linux-gcc CFLAGS="-O4" ./configure -s

命令生成一个针对arm linux并生成动态库的Makefile,然后使用make编译好即可得到在arm linux下可用的libz库。

后来,由于效率上的原因,我们改用了unzip-5.52的代码,由于这个库没有configure程序,我是手动改Makefile的,将其中的编译器改成了arm-linux-gcc,并修改了相应的目录以及去掉了汇编的CRC校验部分,使用unix的非汇编动态库编译方式,最后成功编译出lunzip.so。

第五章          改进与优化

经过上面的步骤,我们基本上得到了一个在arm linux上可以跑的armjvm,然而,经过测试,这样的代码用递归方式算一个Fibonacci (10),在我们的开发平台——redhat linux 7的机器上用了将近70秒,效率极其低下。这个机器的相关参数如下:

Cpu:Pentium II 267MHz,532MIPS

内存:48M

虽然机器比较差,但是速度还是无法让人忍受的,到了我们的目标平台——只有65MIPS的arm linux上时,要运行将近5分钟。

于是,改进和优化是必须要做的,否则我们的这个项目就无法真正使用了。

1.      解压代码效率分析及重写

通过分析,发现我们的代码在解压jar包时占用了绝大部分时间,但是我们的宿主机上装的SUN公司的JRE在运行这个程序的时候是很快就装载了所有的包的,这其中肯定有很多的可提高之处。

于是我开始分析我们所使用的解压代码,通过测试我们所用的开源库编译出来的解压软件,我发现:在这个RedHat Linux开发平台上,同样解压一个软件包,我们用的这个开源软件和Linux中已有的unzip之间差别并不是很大。但是,当解压某一个单独的文件时,这两者的差别非常大。于是,通过阅读这个开源库的实现,发现了问题出在unzLocateFile函数:

它查找zip(jar采用zip方式压缩)里的文件的方法如下:

     err = unzGoToFirstFile(file);

 

     while (err == UNZ_OK)

     {

         char szCurrentFileName[UNZ_MAXFILENAMEINZIP+1];

         err = unzGetCurrentFileInfo(file,NULL,

              szCurrentFileName,sizeof(szCurrentFileName)-1,

              NULL,0,NULL,0);

         if (err == UNZ_OK)

         {

              if (unzStringFileNameCompare(szCurrentFileName,

                   szFileName,iCaseSensitivity)==0)

                   return UNZ_OK;

              err = unzGoToNextFile(file);

         }

     }

 

可以看出,它是使用循环,从压缩包里一个个文件比较是否是想要的那个文件,如果压缩包非常大、里面的文件非常多的时候,它的效率是非常低的。而我们的arm机器上使用的还是NFS文件系统,即读取jar文件时还需要从网络获得,如果整体搜索将是非常慢的,cpu使用率也会很高。

受unzip命令解压效率很高的启发,我去下载了一个unzip的源码包进行阅读,发现它的代码非常复杂,没法从中找到具体的解决办法,大概它还通过目录结构之类进行查找,但是通过看这个源码包发现了它还提供一个api.c文件供程序员使用,这个文件对它的api进行了简要介绍,于是决定将原来的解压代码改用unzip的api实现。

由于这个源码包没提供详细的api文档,网上也没找到相应资料,我只能根据源码进行api使用尝试,通过看源码里使用一些函数的方法,我自己写了一个测试代码:

#include "unzip.h"

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

int mymsg()

{

     printf("test");

     return 1;

}

int main()

{

     UzpOpts flgopts;

     UzpBuffer retstr;

     //   typedef int   (UZ_EXP MsgFn)     (zvoid *pG, uch *buf, ulg size, int flag);

 

     UzpCB UsrFuncts = {24,mymsg,NULL,NULL,NULL,NULL};

     //      if(!unzipToMemory("test.zip", "test.c", &retstr)){

     if (!UzpUnzipToMemory("test.zip", "java/lang/test.c", &flgopts, &UsrFuncts, &retstr)) {

         printf("failed");

         exit(0);

     }

     else    printf(retstr.strptr);

}

其中,对好几个api进行了试验,最后选择了UzpUnzipToMemory,最适合于运用到我们的代码中。期间,遇到的主要问题是参数不知道该传什么进去,只能去看源码里那个参数类型的声明及注释,了解到flgopts参数用来传递类似于unzip命令时可以跟在后面的各种参数选项,UsrFuncts用于将自己的消息函数传入函数,以便遇到相应情况时显示,开始我不打算自己管理消息,全部传入NULL,结果发生段错误,通过察看源码才发现第一个消息函数是必须要传入的,因为代码里没有判断它是否为空就会调用它。这一步的主要困难就是api使用的尝试了,花了不少时间。期间还因为和libz库版本不一致发生了一些文件解压失败的情况,后来在编译libunzip.so时使用了不依赖libz库而完全由unzip自己实现的方式。

通过使用这个解压方法,程序的效率大大提高了:

运行Fibonacci.class

 

Redhat Linux开发平台上:

Arm Linux平台

使用miniunz库

70s

300s

使用unzip库

8s

35s

 

2.      API的裁剪

另一个导致解压较慢的原因就是Java API太大,完整的rt.jar有30多M,放在NFS上,因此导致解压效率较低,而这些API里,很多都是嵌入式系统上所不需要的功能,因此我们决定将API进行剪裁并重写部分。

第一步,我们确定了我们的虚拟机所支持的API里的包:java.lang,java.io,java. security,java.util。

第二步,我们写了一个awk脚本对Java doc进行分析,找到了这四个包里所有的public和protected变量和方法,以及他们的类型和参数等信息,这也就是我们如果要重写API所需要实现的东西了。

第三步,通过分析这些找到函数,变量,我们通过脚本得到了这几个包里非闭包的函数和变量,即类型或参数用到了这4个包以外的包的类或接口。

我们才刚开始对这些包进行修改,暂时还没有效果可以看到。不过,为了减小rt.jar的大小,我们将压缩包里一些不需要的包删除掉了,留下了java,com,javax,org,sunw,launcher这几个包,使得rt.jar只有4M大小,使用之后,在arm linux下运行Fibonacci.class的时间进一步减小到19s。剩下的瓶颈就主要是执行效率上的了。

第六章          验证ArmJVM的正确性

1.      ArmJVM的使用方法

ArmJVM提供了-version,-showversion,–help,-?,–verbose 命令。

-version命令显示ArmJVM的版本信息,然后退出

-showversion命令显示ArmJVM的版本信息,然后继续运行Java程序

-help,-?命令显示帮助信息

-verbose命令输出详细数据显示运行过程

下面以empty.java生成的empty.class为例,说明MiniJavaVM的使用方法。其中empty.java文件内容如下图所示:

所有生成的文件中,jvm是由jvm工程生成的可执行文件,MiniJava.so是由JavaNativeCall工程生成的动态链接库,libMiniJVM.so是由JavaVM工程生成的动态链接库。

在jvm所在目录运行命令“./jvm -?”或命令“./jvm –help”,运行结果如下:

运行命令“./jvm –version”,运行结果如下:

运行命令“javac empty.java”得到empty.class文件,放到arm上,用命令“./jvm empty”,运行结果如下:

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多