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

技术标签: jvm  java  编译器  c语言  

更详细的讲解和代码调试演示过程,请参看视频
用java开发C语言编译器

更详细的讲解和代码调试演示过程,请参看视频
如何进入google,算法面试技能全面提升指南

如果你对机器学习感兴趣,请参看一下链接:
机器学习:神经网络导论

更详细的讲解和代码调试演示过程,请参看视频
Linux kernel Hacker, 从零构建自己的内核

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”, 3*6); 转换成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打印到控制台上。

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

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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/tyler_download/article/details/72926989

智能推荐

王斌老师的博客_王斌 github-程序员宅基地

文章浏览阅读480次。http://blog.sina.com.cn/s/blog_736d0b9101018cgc.html_王斌 github

ACM OJ Collection_htt//acm.wydtang.top/-程序员宅基地

文章浏览阅读737次。原文来自:http://blog.csdn.net/hncqp/article/details/4455263 ACM OJ Collection(排名不分先后):中国:浙江大学(ZJU):http://acm.zju.edu.cn/北京大学(PKU):htt_htt//acm.wydtang.top/

ios 自己服务器 苹果支付_修复苹果IOS支付-程序员宅基地

文章浏览阅读467次。更新记录1.0.0(2019-07-01)插件简介专门用来修复苹果IOS支付时出现"您已购买此App内购买项目。此项目将免费恢复"。问题描述首先在IOS平台里面创建“APP内购买项目”,选择的是“消耗型项目”,然后用uni-app官方的支付api进行支付,多支付几次,有时候就会出现提示“您已购买此App内购买项目。此项目将免费恢复”,特别是在沙盒测试里面支付很大几率出现,我明明选的是消耗型项目,应..._ios开发苹果支付恢复权益

spring MVC mock类单元测试(controller)_mvcmock-程序员宅基地

文章浏览阅读5.6k次。Spring从J2EE的Web端为每个关键接口提供了一个mock实现:MockHttpServletRequest几乎每个单元测试中都要使用这个类,它是J2EE Web应用程序最常用的接口HttpServletRequest的mock实现。MockHttpServletResponse此对象用于HttpServletRespons_mvcmock

【我的世界Minecraft-MC】常见及各种指令大杂烩【2022.8版】_summon生成掉落物-程序员宅基地

文章浏览阅读8.5k次,点赞7次,收藏22次。execute as @a at @s run clear @s minecraft:dark_oak_planks{display:{Name:“{“text”:“第三关[阴森古堡]”,“color”:“red”,“italic”:false}”,color:“16711680”},Enchantments:[{id:“protection”,lvl:1}],Unbreakable:1b} 1。Lore:[“{“text”:“免费”,“color”:“blue”,“italic”:false}”]..._summon生成掉落物

CentOS 7安装教程(图文详解)_centos 安装-程序员宅基地

文章浏览阅读10w+次,点赞487次,收藏2.1k次。CentOS 7安装教程: 准备: 软件:VMware Workstation 镜像文件:CentOS-7-x86_64-bin-DVD1.iso (附:教程较为详细,注释较多,故将操作的选项进行了加粗字体显示。) 1、文件--新建虚拟机--自定义 2、..._centos 安装

随便推点

Github项目分享——免费的画图工具drow,前端插件化面试_draw github画图-程序员宅基地

文章浏览阅读333次,点赞3次,收藏3次。项目介绍一款很好用的免费画图软件,支持ER图、时序图、流程图等等在项目的releases就可以下载最新版本同时支持在线编辑。_draw github画图

如何开始学习人工智能?入门的学习路径和资源是什么?_人工智能学习路径-程序员宅基地

文章浏览阅读930次。嗨,大家好!如果你对人工智能充满了好奇,并且想要入门这个领域,那么你来对地方了。本文将向你介绍如何从零基础开始学习人工智能,并逐步掌握核心概念和技能。无论你是大学生、职场新人还是对人工智能感兴趣的任何人,都可以按照以下学习路径逐步提升自己。_人工智能学习路径

Unity3D 导入资源_unity怎么导入压缩包-程序员宅基地

文章浏览阅读4.3k次,点赞2次,收藏8次。打开Unity3D的:window-asset store就会出来这样的界面:我们选择一个天空纹理,注意这里的标签只有一个,如果有多个就会显示所有标签的内容:找个比较小的免费的下载一下试试,比如这个:下载以后:点击import就会出现该窗口:然后再点击最底下的import:就导入到我们这里来了。从上面可以切换场景:..._unity怎么导入压缩包

jqgrid 服务器端验证,javascript – jqgrid服务器端错误消息/验证处理-程序员宅基地

文章浏览阅读254次。在你以前的问题的the answer的最后一部分,我试着给出你当前的问题的答案.也许我表示不够清楚.您不应该将错误信息放在标准成功响应中.您应该遵循用于服务器和客户端之间通信的HTTP协议的主要规则.根据HTTP协议实现网格中的加载数据,编辑行和与服务器的所有Ajax通信.每个HTTP响应都有响应第一行的状态代码.了解这个意义非常重要.典型的JSON数据成功请求如下HTTP/1.1 200 OK...._decode message error

白山头讲PV: 用calibre进行layout之间的比对-程序员宅基地

文章浏览阅读4k次,点赞8次,收藏29次。我们在流片之后,通常还是有机会对layout进行局部小的修改。例如metal change eco或者一些层次的局部修改。当我们修改之后,需要进行与之前gds的对比,以便确认没有因为某些..._calibre dbdiff

java exit方法_Java:如何测试调用System.exit()的方法?-程序员宅基地

文章浏览阅读694次。问题我有一些方法应该在某些输入上调用567779278。不幸的是,测试这些情况会导致JUnit终止!将方法调用放在新线程中似乎没有帮助,因为System.exit()终止了JVM,而不仅仅是当前线程。是否有任何常见的处理方式?例如,我可以将存根替换为System.exit()吗?[编辑]有问题的类实际上是一个命令行工具,我试图在JUnit中测试。也许JUnit根本不适合这份工作?建议使用互补回归测..._检查system.exit