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

智能推荐

c# 调用c++ lib静态库_c#调用lib-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib

deepin/ubuntu安装苹方字体-程序员宅基地

文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang

html表单常见操作汇总_html表单的处理程序有那些-程序员宅基地

文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些

PHP设置谷歌验证器(Google Authenticator)实现操作二步验证_php otp 验证器-程序员宅基地

文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器

【Python】matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距-程序员宅基地

文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距

docker — 容器存储_docker 保存容器-程序员宅基地

文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器

随便推点

网络拓扑结构_网络拓扑csdn-程序员宅基地

文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn

JS重写Date函数,兼容IOS系统_date.prototype 将所有 ios-程序员宅基地

文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios

如何将EXCEL表导入plsql数据库中-程序员宅基地

文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql

Git常用命令速查手册-程序员宅基地

文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...

分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120-程序员宅基地

文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120

【C++缺省函数】 空类默认产生的6个类成员函数_空类默认产生哪些类成员函数-程序员宅基地

文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数

推荐文章

热门文章

相关标签