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

news2024/12/26 1:02:41

aa55cf09d092408a841a30387f58d89e.jpg


 概要

 

字节码是运行在 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 中。

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

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

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

栈帧

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

64d77e54c522494ca2f57cc75dcaf44b.png

局部变量表

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

83eb77a611c9417d98a033315dc606a6.png

操作数栈

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

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

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

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

b5298025769e4da7b778c291f9cbfc45.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) 整个过程局部变量表如下图所示

dd213a34202a4ec48a7db423f2bc76a5.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

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

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

 

总结

本文主要讲了以下几个要点:

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

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

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

 

 

今天的分享到此结束,若文章对你有所帮助,欢迎点赞收藏关注,感谢🙏

 

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/704936.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一次线上事故,我顿悟了异步的精髓

在高并发的场景下&#xff0c;异步是一个极其重要的优化方向。 前段时间&#xff0c;生产环境发生一次事故&#xff0c;笔者认为事故的场景非常具备典型性 。 写这篇文章&#xff0c;笔者想和大家深入探讨该场景的架构优化方案。希望大家读完之后&#xff0c;可以对异步有更深…

Linux 环境变量 命令行参数

文章目录 问题引入环境变量环境变量相关操纵&#xff08;附源码&#xff09;命令行参数问题解释 问题引入 Linux下&#xff0c;为什么我们自己写的可执行文件需要写明路径才可以执行&#xff0c;而系统的命令不需要&#xff1f; 如何使自己的命令也可以不带路径执行&#xff…

MioIO笔记01

视频地址&#xff1a;分布式文件系统MinIO教程&#xff0c;2021最新版【通俗易懂】_哔哩哔哩_bilibili 目录 1【分布式文件存储系统Minio实战】 P001【1.课程介绍】05:08 P002【2.Minio优点和基础概念】19:29 P003【3.Minio的EC码和文件存储结构】11:33 2【Minio环境搭建】…

【youcans动手学模型】ShuffleNet 模型

欢迎关注『youcans动手学模型』系列 本专栏内容和资源同步到 GitHub/youcans 【youcans动手学模型】ShuffleNet 模型 1. ShuffleNet 网络模型1.1 模型简介1.2 论文介绍 2. 在 PyTorch 中定义 ShuffleNet V1 模型类2.1 分组卷积与通道混洗2.2 ShuffleNet 单元2.3 自定义 Shuffle…

0、如何用运python成为顶级黑客

前言 黑客攻击可以导致个人、组织或公司的机密信息被窃取、破坏或泄露&#xff0c;造成财务损失、声誉受损、系统崩溃等各种问题 一、为什么Python编程语言适合网络安全工作&#xff1f; Python编程语言在网络安全工作中具有许多优势&#xff0c;以下是一些详细介绍&#xf…

性能测试中的随机数性能问题探索

目录 缘起 多线程 单线程 末了 总结&#xff1a; 在软件测试中&#xff0c;经常会遇到随机数。我简单分成了两类&#xff1a; 简单取随机数&#xff1b; 从一个集合中随机取值。 其实第二个场景包含在第一个场景内。对于接口测试来说&#xff0c;通常我们直接使用第二种…

springboot定时任务详解

文章目录 一、基于注解&#xff08;静态&#xff09;1、添加依赖2、创建定时任务3、参数说明 二、基于接口&#xff08;动态&#xff09;1、添加依赖2、添加数据库记录3、创建定时器4、启动测试 三、Quartz1、添加依赖2、编写任务类3、编写配置类4、启动项目 在我们开发项目过程…

css基础知识十四:什么是响应式设计?响应式设计的基本原理是什么?如何做?

一、是什么 响应式网站设计&#xff08;Responsive Web design&#xff09;是一种网络页面设计布局&#xff0c;页面的设计与开发应当根据用户行为以及设备环境(系统平台、屏幕尺寸、屏幕定向等)进行相应的响应和调整 描述响应式界面最著名的一句话就是“Content is like wat…

AD20|原理图导入Pcb时三极管引脚报错 Unkown Pin:Pin Q1-B

完成原理图绘制后&#xff0c;将其导入到Pcb中进行布局时&#xff0c;出现报错;Unknown Pin: Pin Q1-B. 原因是&#xff1a;引脚名称不一致 在原理图中&#xff0c;三个引脚分别定义B、C、E&#xff1b; 而在常见的TO—92A封装中&#xff0c;使用1、2、3作为三个引脚的名称&am…

实习工作之定时任务

需求&#xff1a;在当天晚上12点定时将过期数据的状态置为冻结状态 版本一代码实现[相当于是一个死代码&#xff0c;因为不能自动调度] public void updateStatus() throws CommonException, ParseException {String date_str "2023-07-01 00:00:00";Date expireT…

为什么我从 Manjaro Linux 跳到 EndeavourOS

我使用 Manjaro Linux 已经两年了,但随着时间的推移,我最初对 Manjaro 的喜爱逐渐减少,我对它的感觉越来越不舒服。这就是我转向 EndeavourOS 的原因。 我根本不是你所说的 Distrohopper 我早在 20 世纪 90 年代中期就开始使用 Linux,当时使用的是 RedHat Linux。2003 年…

网络请求库-axios的入门指南

网络请求库axios-入门指南 网络请求库-axios认识axios库axios发送请求axios基本使用axios基本演练配置baseURLaxios创建实例axios发送多个请求&#xff08;了解&#xff09; axios请求和响应拦截器axios请求封装&#xff08;简洁&#xff09; 网络请求库-axios 认识axios库 有…

使用 Maya Mari 设计 3D 波斯风格道具(p2)

今天瑞云渲染小编给大家带来了Simin Farrokh Ahmadi 分享的Persian Afternoon 项目过程&#xff0c;解释了 Maya 和 Mari 中的建模、纹理和照明过程。由于篇幅较长&#xff0c;分上下两篇来分别阐述。接着会继续讲述Persian Afternoon 项目过程的纹理和灯光渲染方面内容。 纹理…

Apikit 自学日记:安装、使用浏览器插件测试

Apikit 研发管理和自动化测试产品中&#xff0c;提供了多种发起 API 测试的方式&#xff1a; 服务器测试&#xff1a;通过 Apikit 官方远程服务器发送请求&#xff0c;不需要安装任何插件&#xff0c;但是无法访问本地服务器(localhost)、内网、局域网。 插件测试&#xff1a;…

linux创建软链接

目录 软链接的作用创建软链接删除软链接修改链接 软链接的作用 就如同电影里演的一样&#xff0c;为保护价值昂贵的钻石&#xff0c;做一个和真正的钻石一模一样的假钻石来掩人耳目&#xff0c;假钻石的信息和真的一模一样&#xff0c;看到假的钻石就如同看到真的一样。Linux中…

《基于AidLux的自动驾驶智能预警应用方案》

YOLOP模型转ONNX ONNX是开放式神经网络(Open Neural Network Exchange)的简称&#xff0c;ONNX的规范及代码主要由微软&#xff0c;亚马逊&#xff0c;Facebook和IBM等公司共同开发&#xff0c;以开放源代码的方式托管在Github上。目前官方支持加载ONNX模型的框架有&#xff1…

记录实现QT和qml model/view 交互时候遇到的坑

使用QT的model/view 建立全局静态变量 打印发现有值输出 但是界面无日志显示 原因是一开始使用调用函数传参传的logModel参数加了const参数修饰 这个错误太傻了&#xff0c;找了半天一直怀疑外部类的问题 其实setContextProperty是这么用的 传QObject类型的引用之后它内部会…

【无标题】下载redis工具RESP.app(RedisDesktopManager)

1、安装前提&#xff1a; Windows&#xff1a; 1、Install Microsoft Visual C 2015-2019 x64 (If you have not already). 2、Download Windows Installer from http://redisdesktop.com/download. (Requires subscription)指导手册地址&#xff1a; https://docs.resp.app/…

DataSecurity Plus金融行业案例

摘要&#xff1a;DataSecurity Plus是一款强大的数据安全解决方案&#xff0c;为金融机构提供全面的数据保护和合规性监控。本文将介绍DataSecurity Plus在金融行业的使用案例&#xff0c;包括文件审计、数据分类和合规性报告等功能的应用。 DataSecurity Plus 文件审计&#…

《The book of why》读书笔记 -- introduction:mind over data

最近在浅学causal learning&#xff0c;拜读《The Book of Why》&#xff0c;记录一些笔记&#xff0c;下为书的introduction部分 下面这段话我觉得写得非常好 This book tells the story of a science that has changed the way we distinguish facts from fiction and yet ha…