java开发C语言编译器:JVM 的基本操作指令介绍及其程序运行原理

java虚拟机是对实体计算机的模拟,任何用高级语言开发的程序,必须编译成二进制代码后,才能在CPU上运行,而这些二进制代码本质上是一系列基础指令集的集合,这些指令的作用非常简单,例如把数值存到某块内存,或者把信息从某地读出,然后放入到寄存器中。

同理,运行在java虚拟机上的字节码,其本质也是一系列作用单一的指令的集合。我们看看,要完成一个简单的计算功能,例如计算 6 * 9 的结果,虚拟机是如何依赖基础指令来完成相应计算功能的。计算式 6 * 9 被编译成java字节码后,对应的指令如下:

bipush 6
bipush 9
imul
我们前面强调过,jvm是以栈为基础来运行指令的,任何指令的执行,如果需要参数的话,这些参数必须先压入虚拟机的堆栈上,指令才可以正常执行。从上面的代码可以看成,指令imul 的作用是做乘法操作,要相乘的两个数在执行这句指令前,必须都压入到堆栈的顶部,因此指令bipush 6的作用是把数值6压入到堆栈顶部,bipush 9作用是把数值9压入到堆栈顶部,执行这两条指令后堆栈情况如下:
stack: 9, 6

当虚拟机执行指令imul后,会把堆栈顶部的两个数值弹出,把他们相乘,然后把乘积压入堆栈顶部,所以imul指令执行后,堆栈情况如下:
stack: 54

java虚拟机的指令有一些特点,在指令的前面会有一些字符用于表示指令所要操作的数据类型,例如bipush指令,push前面的字符i表示指令只接受整形数据,i的意思是int, 最前面的b表示byte, 也就是说bipush指令只能操作范围在-128到128之间的整数,同理如果指令是sipush,表示该指令只能把short类型的整数压入到堆栈。同理指令imul表示它只能处理两个整形数相乘,如果此时堆栈顶部的两个数字不是整形的话,那么执行imul指令时,虚拟机就会报错。以下我们给出字符对应的数据类型:

Letter Type
a reference 指令处理的是对象引用
b byte or boolean
c char
d double
i int
l long
s short
如果你看到指令fmul, 你就能知道,这条指令的作用是对两个浮点数做乘法。相应指令的具体作用,大家可以查询java虚拟机规范,这里我们不深入探讨。

我们也提到过,java虚拟机的运行环境,不单单有堆栈,它还有一个局部变量队列,用来存储临时变量的信息。因此java虚拟机也提供了相应的操作指令。假设虚拟机堆栈和局部变量队列的数据存储情况如下:

stack: 1
array: _ _ _ 2

也就是堆栈顶部数据为1,在具备变量队列中,下标为3的地方存储了一个整形2,如果执行了指令 istore 3, 那么虚拟机就会把局部变量队列下标为3处所存储的整形数压入堆栈顶部,于是执行完指令后,堆栈和队列的情况如下:

stack: 2, 1
array:

如果接着执行的指令是 iload 2, 它的意思是,把堆栈顶部的数值弹出,然后存储到局部变量队列下标为2的区域处,于是执行完后,虚拟机的堆栈和队列情况如下:

stack: 1
array: _ _ 2

同理load和store前面的字符用于表明指令所能处理的数据,如果想把一个浮点数从队列压入到堆栈,那么对应的指令就是 fstroe . 假设局部变量队列在下标2为1 的地方存储的是对一个对象的引用,要想把这个引用压入到堆栈顶部,对应的指令就是 astore 1. store 前面的a表示的就是对象引用。这里我们只需要理解指令的基本原理,具体的相关指令,大家可以通过查询java虚拟机规范来了解。

有了上面的基础知识后,我们就可以看看,如何将下面的C语言代码编译成java字节码:

void f() {
printf(“The result of 3 multiply 6 is :%d”, 3*6);
}

void main() {
f();
}
我们的编译器在解析上面代码时,读取到数字字符是,会根据表达式:
Unary -> NUMBER
进行解析,因此当编译器在执行上面表达式的解析时,我们就知道当前已经读取到了数字常量,此时我们可以通过push指令把该数字常量压入虚拟机堆栈上。解析上面表达式的代码是在UnaryExecutor中,所以在UnaryExecutor.java中,我们做如下修改:

public class UnaryNodeExecutor extends BaseExecutor implements IExecutorReceiver{
….
public Object Execute(ICodeNode root) {
….
case CGrammarInitializer.Number_TO_Unary:
text = (String)root.getAttribute(ICodeKey.TEXT);
boolean isFloat = text.indexOf(‘.’) != -1;
if (isFloat) {
value = Float.valueOf(text);
root.setAttribute(ICodeKey.VALUE, Float.valueOf(text));
} else {
value = Integer.valueOf(text);
root.setAttribute(ICodeKey.VALUE, Integer.valueOf(text));
}
ProgramGenerator.getInstance().emit(Instruction.IPUSH, value.toString());
break;
….
}
….
在编译器读入代码,解析到一个数字常量时,我们输出push 指令,后面跟着对应的数字常量,所以当我们的编译器解析代码时,读取到数字字符3,那么上面的代码被执行,于是语句:
ProgramGenerator.getInstance().emit(Instruction.IPUSH, value.toString());
就会在java汇编文件中输出一条语句:
sipush 3

同理,当读取字符6时,上面的代码又会被执行,于是在java汇编文件中又再输出一条语句:
sipush 6

当表达式3 * 6 被解析时,由于该算式对应的语法表达式是:
Binary -> Binary * Binary

所以,当我们的编译器执行上面的语法表达式时,我们就可以输出乘法指令imul. 执行上面表达式的解析是在BinaryExecutor.java中,所以代码改动如下:

public class BinaryExecutor extends BaseExecutor{
@Override
public Object Execute(ICodeNode root) {
….
case CGrammarInitializer.Binary_Plus_Binary_TO_Binary:
case CGrammarInitializer.Binary_DivOp_Binary_TO_Binary:
case CGrammarInitializer.Binary_Minus_Binary_TO_Binary:
case CGrammarInitializer.Binary_Start_Binary_TO_Binary:

    ......

     else if (production ==  CGrammarInitializer.Binary_Start_Binary_TO_Binary) {
            String text = root.getChildren().get(0).getAttribute(ICodeKey.TEXT) + " * " + root.getChildren().get(1).getAttribute(ICodeKey.TEXT);
            root.setAttribute(ICodeKey.VALUE, val1 * val2);    
            root.setAttribute(ICodeKey.TEXT,  text);
            System.out.println(text + " is " + (val1 * val2) );

            ProgramGenerator.getInstance().emit(Instruction.IMUL);
        }
        else {
        ....
        ....
        }

        break;
....
}

大家可以看到,在解析两数相乘时,编译器通过代码:
ProgramGenerator.getInstance().emit(Instruction.IMUL);
在java汇编文件中输出指令 imul,到此我们编译器成功输出了以下语句:

sipush 3
sipush 6
imul
上面三条指令执行后,虚拟机的堆栈顶部存有的数值是18.接下来我们需要把数值18打印出来。通过上节我们了解到,要把信息输出到控制台上时,需要调用对象System.out 的 print 函数,也就是需要把System.out对象先压到堆栈上,再把要输出的内容压到堆栈上,也就是所,如果我们想要把结果18打印到控制台上,那么堆栈上的内存必须是这样:

stack: 18, System.out

但现在问题是,数值18处于堆栈顶部,要想实现上面所需的堆栈数据排列,我们需要把数值18先拿开,然后把对象System.out压入堆栈,接着再把数值18重新压入堆栈,因此我们需要先使用istore指令,把数值18从堆栈放入局部变量队列,也就是先执行指令istore 0, 使得虚拟机堆栈和队列的数据存储情况如下:

stack:
array: 18

然后 执行指令getstatic ,把 System.out对象压入堆栈:

stack: System.out
array: 18

最后使用指令 iload 0 , 把位于队列偏移为0处的数值18重新加载会堆栈,执行完指令iload 0 后,虚拟机堆栈和队列上的内容如下:
stack: 18, System.out
array:

此时要输出到控制台的内容和执行输出操作的System.out对象都已经在堆栈顶部,我们再通过指令invokevirtual 调用System.out对象的Print接口就可以实现把数值18输出到控制台上的目的了。

因此编译器在把语句 printf(“The result of 3 multiply 6 is :%d”, 36); 转换成java字节码时,分以下几步走,首先把字符串”The result of 3 multiply 6 is “ 输出到控制台,接着解析表达式 3* 6,生成上头所说的三条java指令,然后再执行上头所说的步骤,把数值18和类对象System.out放入到堆栈顶部,进而再把数值18输出到控制台上。

这里的逻辑有点繁琐,请大家通过视频获得更清楚的讲解和演示。

上头所说的操作都需要在编译器解析printf调用时去具体处理,因此相关代码的实现在ClibCall.java 中,代码如下:

private Object handlePrintfCall() {
ArrayList<Object> argsList = FunctionArgumentList.getFunctionArgumentList().getFuncArgList(false);
String argStr = (String)argsList.get(0);
String formatStr = “”;

    int i = 0;
    int argCount = 1;
    while (i < argStr.length()) {
        if (argStr.charAt(i) == '%' && i+1 < argStr.length() && 
                argStr.charAt(i+1) == 'd') {
            i += 2;
            generateJavaAssemblyForPrintf(formatStr);
            formatStr += argsList.get(argCount);
            argCount++;
            printInteger();
        } else {
            formatStr += argStr.charAt(i);
            i++;
        }
    }

    System.out.println(formatStr);

    generateJavaAssemblyForPrintf("\n");
    return null;
}

private void generateJavaAssemblyForPrintf(String s) {
    ProgramGenerator generator = ProgramGenerator.getInstance();
    generator.emit(Instruction.GETSTATIC, "java/lang/System/out Ljava/io/PrintStream;");
    generator.emit(Instruction.LDC, "\"" + s + "\"");
    String printMethod = "java/io/PrintStream/print(Ljava/lang/String;)V";
    generator.emit(Instruction.INVOKEVIRTUAL, printMethod);
}

private void printInteger() {
    ProgramGenerator generator = ProgramGenerator.getInstance();
    generator.emit(Instruction.ISTORE, "0");
    generator.emit(Instruction.GETSTATIC, "java/lang/System/out Ljava/io/PrintStream;");
    generator.emit(Instruction.ILOAD, "0");
    String printMethod = "java/io/PrintStream/print(I)V";
    generator.emit(Instruction.INVOKEVIRTUAL, printMethod);
}

在handlePrintfCall调用中,在输出数值之前,先通过generateJavaAssemblyForPrintf调用,把字符串先输出,然后调用printInteger接着把数值输出到控制台上,在printInteger中,先把指令istore 0 输出到Java汇编文件里,然后输出指令getstatic , 目的就是把System.out对象压入到堆栈上,然后再次通过输出指令 iload 0 , 把数值18重新压入到堆栈顶部,最后在通过invokevirtual指令,把数值18输出到控制台。

上面的代码执行后,C语言代码被编译成如下java汇编代码:

.class public CSourceToJava
.super java/lang/Object

.method public static main([Ljava/lang/String;)V
invokestatic CSourceToJava/f()V
return
.end method
.method public static f()V
sipush 3
sipush 6
imul
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc “The result of 3 multiply 6 is :”
invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V
istore 0
getstatic java/lang/System/out Ljava/io/PrintStream;
iload 0
invokevirtual java/io/PrintStream/print(I)V
return
.end method

.end class
我们可以看到,代码先是通过三条指令
sipush 3
sipush 6
imul
计算 3*6 , 接着把字符串”The result of 3 multiply 6 is :”压到堆栈打印出来,最后再通过前面所说的指令操作,把堆栈顶部的数值18打印到控制台上。

请大家通过视频获得更详实的讲解和更清晰的调试演示过程。

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:

《java开发C语言编译器:JVM 的基本操作指令介绍及其程序运行原理》 这里写图片描述

    原文作者:望月从良
    原文地址: https://www.jianshu.com/p/5afc54bb1bf1
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞