首页 > 其他分享 >字节码原理浅析 —— 基于栈的执行引擎

字节码原理浅析 —— 基于栈的执行引擎

时间:2023-06-25 10:36:12浏览次数:51  
标签:ScoreCalculator 操作数 字节 虚拟机 局部变量 引擎 浅析 calculator

字节码是运行在 JVM 上的,为了能弄懂字节码,需要对 JVM 的运行原理有所了解。这篇文章将以栈帧为切入点理解字节码在 JVM 上执行的细节。

虚拟机

虚拟机常见的实现方式有两种:Stack based 的和 Register based。比如基于 Stack 的虚拟机有Hotspot JVM、.net CLR,这种基于 Stack 实现虚拟机是一种广泛的实现方法。而基于 Register 的虚拟机有 Lua 语言虚拟机 LuaVM 和 Google 开发的安卓虚拟机 DalvikVM。

两者有什么不同呢?举一个计算两数相加的例子:c = a + b 基于 HotSpot JVM 的源码和字节码如下

源码
void bar(int a, int b) {
    int c =  a + b;
}

对应字节码
0: iload_1 // 将 a 压入操作数栈
1: iload_2 // 将 b 压入操作数栈
2: iadd    // 将栈顶两个值出栈,相加,然后将结果放回栈顶
3: istore_3 // 将栈顶值存入局部变量表中第 3 个 slot 中

基于寄存器的 LuaVM 的 lua 源码和字节码如下,查看字节码使用luac -l -l -v -s test.lua 命令

源码
local function my_add(a, b)
 return a + b;
end

对应字节码
1 [3] ADD       2 0 1

基于寄存器的 add 指令直接把寄存器 R0 和 R1 相加,结果保存在寄存器 R2 中。

基于栈和基于寄存器的过程对比如下:

image.png

基于栈和寄存器的指令集各有优缺点,基于栈的指令集移植性更好,代码更加紧凑、编译器实现更加简单,但完成相同功能所需的指令数一般比寄存器架构多,需要频繁的入栈出栈,栈架构指令集的执行速度会相对而言慢一些。

为了理解字节码的细节,我们需要详细了解字节码的执行过程。众所周知,Hotspot JVM 是一个基于栈的虚拟机,每个线程都有一个虚拟机栈,存储了「栈帧」。每次方法调用都伴随着栈帧的创建销毁。

栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构 栈帧随着方法调用而创建,随着方法结束而销毁,栈帧的存储空间分配在 Java 虚拟机栈中,每个栈帧拥有自己的局部变量表(Local Variables)、操作数栈(Operand Stack) 和 指向运行时常量池的引用

image.png

局部变量表

每个栈帧内部都包含一组称为局部变量表(Local Variables)的变量列表,局部变量表的大小在编译期间就已经确定。Java 虚拟机使用局部变量表来完成方法调用时的参数传递,当一个方法被调用时,它的参数会被传递到从 0 开始的连续局部变量列表位置上。当一个实例方法(非静态方法)被调用时,第 0 个局部变量是调用这个实例方法的对象的引用(也就是我们所说的 this )

image.png

操作数栈

每个栈帧内部都包含了一个称为操作数栈的后进先出(LIFO)栈,栈的大小同样也是在编译期间确定。Java 虚拟机提供的一些字节码指令用来从局部变量表或者对象实例的字段中复制常量或者变量到操作数栈,也有一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用时,操作数栈也用来准备调用方法的参数和接收方法返回的结果。

比如 iadd 指令用来将两个 int 类型的数值相加,它要求执行之前操作数栈已经存在两个由前面其它指令放入的 int 型数值,在 iadd 指令执行时,两个 int 值从操作数栈中出栈,相加求和,然后将求和的结果重新入栈。

比如 1 + 2 这样的指令执行过程如下

image.png
整个 JVM 指令执行的过程就是局部变量表与操作数栈之间不断 load、store 的过程

image.png
我们再来看一个稍微复杂一点的例子

public class ScoreCalculator {
    public void record(double score) {
    }

    public double getAverage() {
        return 0;
    }
}
public static void main(String[] args) {
    ScoreCalculator calculator = new ScoreCalculator();

    int score1 = 1;
    int score2 = 2;

    calculator.record(score1);
    calculator.record(score2);

    double avg = calculator.getAverage();
} 

javap 查看字节码输出如下

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
  stack=3, locals=6, args_size=1
     0: new           #2                  // class ScoreCalculator
     3: dup
     4: invokespecial #3                  // Method ScoreCalculator."<init>":()V
     7: astore_1
     
     8: iconst_1
     9: istore_2
     
    10: iconst_2
    11: istore_3
    
    12: aload_1
    13: iload_2
    14: i2d
    15: invokevirtual #4                  // Method ScoreCalculator.record:(D)V
    
    18: aload_1
    19: iload_3
    20: i2d
    21: invokevirtual #4                  // Method ScoreCalculator.record:(D)V
    
    24: aload_1
    25: invokevirtual #5                  // Method ScoreCalculator.getAverage:()D
    28: dstore        4
    
    30: return


  • 0 ~ 7:新建了一个 ScoreCalculator 对象,使用 astore_1 存储在局部变量 calculator 中:astore_1 的含义是把栈顶的值存储到局部变量表下标为 1 的位置上,这里为什么会有一个 dup,我们后面会讲到

  • 8 ~ 11:iconst_1 和 iconst_2 用来将整数 1 和 2 加载到栈顶,istore_2 和 istore_3 用来将栈顶的元素存储到局部变量表 2 和 3 的位置上

  • 12 ~ 15:可以看到 store 指令会把栈顶元素移除,所以下次我们要用到这些局部变量时,需要使用 load 命令重新把它加载到栈顶。比如我们要执行calculator.record(score1),对应的字节码如下

12: aload_1
13: iload_2
14: i2d
15: invokevirtual #4 // Method ScoreCalculator.record:(D)V

可以看到 aload_1 先从局部变量表中 1 的位置加载 calculator 对象,iload_2 从 局部变量表中 2 的位置加载一个整型值,i2d 这个指令用来将整型值转为 double 并将新的值重新入栈,到目前为止参数全部就绪,可以用 invokevirtual 执行方法调用了

  • 24 ~ 28:同样是一个普通的方法调用,流程还是先 aload_1 加载 calculator 对象,invokevirtual 调用 getAverage 方法,并将 栈顶元素存储到局部变量表下标为 4 的位置上 有一点需要注意的是 javap 输出的locals=6,但是我们目前看到的局部变量只有args、calculator、score1、score2、avg这 5 个,为什么这里等于 6 呢?这是因为 avg 为 double 型变量,需要两个槽位(slot) 整个过程局部变量表如下图所示

image.png

其实局部变量表可以通过 javap 用 -l 参数直接输出,但是我们用 javap -v -p -l MyLocalVariableTest 并没有输出任何局部变量表相关的信息。这是因为默认情况下局部变量表属于调试级别的信息,javac 编译的时候并没有编译进字节码,我们可以加上 javac -g 生成字节码的时候同时生成所有的调试信息,如下所示

javac -g  MyLocalVariableTest.java 
javap  -v -p -l   MyLocalVariableTest
LocalVariableTable:
Start  Length  Slot  Name   Signature
    0      31     0  args   [Ljava/lang/String;
    8      23     1 calculator   LScoreCalculator;
   10      21     2 score1   I
   12      19     3 score2   I
   30       1     4   avg   D

从二进制看 class 文件和字节码

public class Get {
    String name;

    public String getName() {
        return name;
    }
}

javap 查看字节码如下

public java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
  stack=1, locals=1, args_size=1
     0: aload_0
     1: getfield      #2 / Field name:Ljava/lang/String;
     4: areturn

image.png
直接从二进制来看下这个 class 文件 xxd Get.class

image.png
我们可以手动用 16 进制编辑器去修改这些字节码文件,只是比较容易出错,所以产生了一些字节码操作的工具,最出名的莫过于 ASM 和 Javassist。我们后面讲到软件破解的时候,会介绍直接修改字节码和通过 ASM 动态修改字节码这两种方式

小结

一起来回顾一下这篇文章的要点:

  • 第一,基于栈和基于寄存器指令集的优劣势;

  • 第二,讲解了 JVM 栈帧的构成(局部变量表、操作数栈、指向运行时常量池的引用),顺带讲解了 javap -l 参数和其在局部变量表中的应用;

  • 第三,从类文件二进制角度看字节码的实现,并引出 ASM 字节码改写技术。

转载: https://mp.weixin.qq.com/s/tOZ7t8Tm1ZLZets3KnNeRw

本文使用 文章同步助手 同步

标签:ScoreCalculator,操作数,字节,虚拟机,局部变量,引擎,浅析,calculator
From: https://www.cnblogs.com/dc-s/p/17502300.html

相关文章

  • 为什么js引擎是单线程?
    为什么js引擎是单线程?js的主要用途是与用户互动,以及操作DOM,这决定它只能是单线程。例:一个线程要添加DOM节点,一个线程要删减DOM节点,容易造成分歧。为了更好使用多CPU,H5提供了webWorker 标准,允许js创建多线程,但是子线程受到主线程控制,而且不得操作DOM。说说进程与线程的区别与......
  • Android面试题合集大全(2023最新版),1600+面试题附答案详解,字节大佬力推
    接触Android已经3年,在工作中遇到疑难问题总是在网上(csdn大牛博客,stackoverflow等)搜索答案,各位大牛大神总是把自己的经验分享出来,帮助我们这些需要帮助的人,由此表示衷心感谢!然而现在自己细想了一下,自己也是时候把遇到的问题并把解决方案分享出来,希望能帮助到有需要的人。现在小公司......
  • Android开发社招10个公司28轮面试面经(含字节、拼多多、美团、滴滴......)
    个人情况学历:二本工作年限:2年半面试结果:拿到了字节、拼多多、美团、滴滴、欧科云链…的offer面经面经奉上,不是很全,希望有帮助字节项目认识的汇编代码类继承中父类和子类的内存布局讲一讲虚拟内存空间线程池中线程数量如何设计信号量机制子类中变量初始化顺序和销毁顺序线程调度算......
  • Mysql-存储引擎
    存储引擎1.存储引擎概述和大多数的数据库不同,MySQL中有一个存储引擎的概念,针对不同的存储需求可以选择最优的存储引擎。存储引擎就是存储数据,建立索引,更新查询数据等等技术的实现方式。存储引擎是基于表的,而不是基于库的。所以存储引擎也可被称为表类型。Oracle,SqlServer......
  • 浅析LRC歌词文件
    浅析LRC歌词文件 [时间:2018-12][状态:Open][关键词:字幕,LRC,歌词,lyric,文件格式]0引言几年前(2010年左右),网络音乐流行与免费的时代,网上有大量的mp3,使用比较常见的播放器,比如酷狗、酷我、千千静听等,下载mp3时总会附带下载一个同名但以lrc为后缀的文本文件,在早几年你可能还会遇......
  • 什么是游戏引擎?
    游戏引擎是开发电子游戏的软件框架,它提供了一系列的工具、资源和接口,用于游戏的设计、开发和运行。游戏引擎通常包括渲染引擎、物理引擎、音频引擎、脚本引擎和网络引擎等主要模块,这些模块集成在一起,提供了一种可定制和可扩展的方式,使游戏开发人员能够创建不同类型的游戏。游戏引......
  • 字节面试准备
    time-wait存在的意义 接口和抽象类的区别 多态的底层原理 线程池 StringBuilder及StringBuffer的区别 视频流功能测试 微信发语音功能测试 多条语音但没有声音怎么测试 python两个队列找出相同的元素deffind_common_elements(lst1,lst2):set1=set(lst......
  • Mysql存储引擎
    原文链接:https://blog.csdn.net/lzb348110175/article/details/106555504本文目录:1.MySQL体系结构2.存储引擎介绍3.MySQL存储引擎特性4.MySQL有哪些存储引擎5.了解MySQL数据存储方式6.MySQL存储引擎介绍6.1CSV存储引擎6.1.1CSV介绍6.1.2使用CSV存储引擎......
  • 搜索引擎如何优化
    1、了解搜索引擎如何抓取网页和如何索引网页。2、Meta标签优化。3、如何选取关键词并在网页中放置关键词。4、了解主要的搜索引擎。5、主要的互联网目录6、按点击付费的搜索引擎。7、搜索引擎登录。8、链接交换和链接广泛度(LinkPopularity)。9、标签的合理使用:标签的语义......
  • 字节、百度、美团、腾讯技术面,面试题及答案分享(Android岗)
    字节(3轮技术面):一面:1.final2.类加载3.双亲委派机制,为什么要使用4.GC5.leackcanary6.hashmap7.concurrenthashmap8.事件分发9.handler算法:1.LeetCode61:旋转链表2.合并两个有序链表二面:1.final修饰int类型的变量能不能改变?怎么改变?2.反射可以改变这个int值吗?怎么改变?反射......