加载中...
查阅java字节码
发表于:2022-04-09 | 分类: 程序人生
字数统计: 2.1k | 阅读时长: 9分钟 | 阅读量:

简介

javap是 Java class文件分解器,可以反编译,也可以查看java编译器生成的字节码。用于分解class文件。
本文以64位OpenJDK 14.0.2版本来介绍javap工具。

  • 首先看下javap的使用方法
    用法: javap <options> <classes>
    其中, 可能的选项包括:
      -? -h --help -help               输出此帮助消息
      -version                         版本信息
      -v  -verbose                     输出附加信息
      -l                               输出行号和本地变量表
      -public                          仅显示公共类和成员
      -protected                       显示受保护的/公共类和成员
      -package                         显示程序包/受保护的/公共类
                                       和成员 (默认)
      -p  -private                     显示所有类和成员
      -c                               对代码进行反汇编
      -s                               输出内部类型签名
      -sysinfo                         显示正在处理的类的
                                       系统信息(路径、大小、日期、SHA-256 散列)
      -constants                       显示最终常量
      --module <模块>, -m <模块>       指定包含要反汇编的类的模块
      --module-path <路径>             指定查找应用程序模块的位置
      --system <jdk>                   指定查找系统模块的位置
      --class-path <路径>              指定查找用户类文件的位置
      -classpath <路径>                指定查找用户类文件的位置
      -cp <路径>                       指定查找用户类文件的位置
      -bootclasspath <路径>            覆盖引导类文件的位置
      --multi-release <version>        指定要在多发行版 JAR 文件中使用的版本

常用的命令为javap -p -v -l

Bean对象反编译

  • 先看一下最简单的bean,这里添加了一些注释信息
/**
 * 注释信息
 */
public class Student {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

用javap -p -v -l Student 进行反编译,得到下面的信息

Classfile /Users/shellingford/data/devtool/workspace/test/target/classes/Student.class
  Last modified 2022年4月9日; size 496 bytes
  SHA-256 checksum a17b7f5fcf2f45162bd029d889684a9d41da8f0c59ac057ba4f748df1627abd3
  Compiled from "Student.java"
public class Student
  minor version: 0
  major version: 58
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // Student
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // Student.name:Ljava/lang/String;
   #8 = Class              #10            // Student
   #9 = NameAndType        #11:#12        // name:Ljava/lang/String;
  #10 = Utf8               Student
  #11 = Utf8               name
  #12 = Utf8               Ljava/lang/String;
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               LStudent;
  #18 = Utf8               getName
  #19 = Utf8               ()Ljava/lang/String;
  #20 = Utf8               setName
  #21 = Utf8               (Ljava/lang/String;)V
  #22 = Utf8               SourceFile
  #23 = Utf8               Student.java
{
  private java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: (0x0002) ACC_PRIVATE

  public Student();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LStudent;

  public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #7                  // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LStudent;

  public void setName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #7                  // Field name:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 12: 0
        line 13: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   LStudent;
            0       6     1  name   Ljava/lang/String;
}
SourceFile: "Student.java"

第一部分是class的文件信息,包含了最后修改时间,文件大小,SHA-256摘要等等。

第二部分是类信息,包含了类名、编译版本、访问权限、父类信息、接口数量、字段数量、属性数量。

其中major表示的是java大版本号,例如58表示jdk14, minor主要是一些小的版本更新。在class运行时jvm会校验版本号,如果jvm版本号低于编译的class版本号就会报错

Unsupported major.minor version 58.0

第三部分是常量池

第四部分是类的内容,包含了属性和方法

其中可以看到LineNumberTable,这是方法内的一个偏移量和代码行号转换信息。虽然注释信息不会被写入class,但占用的行会影响代码行号,classes中的行号信息是包含注释在内的行号。

字段和属性

在反编译的第二部分,字段(fields)数量是1,属性(attributes)数量是1 。那字段和属性有什么区别的呢?
我们修改一下Student代码

public class Student {
    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

除了name外,我们额外增加一个age,这个时候字段(fields)数量变成了2 。所以字段(fields)指的就是类的属性,这也和Field类对应。

我们再给Student增加一个接口

import java.io.Serializable;

public class Student implements Serializable {

    private static final long serialVersionUID = -310391221195116443L;
    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

接口数量也变成了1

再看一下字段是怎么展示的

这里的常量可以在常量池中找到,但需要注意的是,只有final修饰的字段才会在字段上直接初始化,其余的都是在构造函数中初始化的。可以看下面的修改

import java.io.Serializable;

public class Student implements Serializable {

    private static final long serialVersionUID = -310391221195116443L;
    private String name = "张三";
    private Integer age = 18;
    private int  no = 1;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

方法

先看一下代码

public class Student  {

    public void study(){
        int score = 1;
        try {
            score = 2;
        }catch(Exception e){
            score = 3;
        }finally{
            score = 4;
        }
    }

}

反编译是这样的

public void study();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_1
         4: iconst_4
         5: istore_1
         6: goto          22
         9: astore_2
        10: iconst_3
        11: istore_1
        12: iconst_4
        13: istore_1
        14: goto          22
        17: astore_3
        18: iconst_4
        19: istore_1
        20: aload_3
        21: athrow
        22: return
      Exception table:
         from    to  target type
             2     4     9   Class java/lang/Exception
             2     4    17   any
             9    12    17   any
      LineNumberTable:
        line 4: 0
        line 6: 2
        line 10: 4
        line 11: 6
        line 7: 9
        line 8: 10
        line 10: 12
        line 11: 14
        line 10: 17
        line 11: 20
        line 12: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           10       2     2     e   Ljava/lang/Exception;
            0      23     0  this   LStudent;
            2      21     1 score   I
      StackMapTable: number_of_entries = 3
        frame_type = 255 /* full_frame */
          offset_delta = 9
          locals = [ class Student, int ]
          stack = [ class java/lang/Exception ]
        frame_type = 71 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 4 /* same */

再看一下异常部分

是由异常表来处理的,其中第一个Exception是代码显示捕获的Exception,剩下2个都是any。
第一个any是由2到4,和Exception范围一样,是用来捕获非Exception的,即便没有显示捕获错误,jvm也会捕获,跳转到17偏移量,最终在21偏移量被继续抛出这个异常。
第二个any也是类似的,只是它捕获的是Catch代码块中抛出的异常,最终也在21偏移量被抛出。

除了异常捕获部分,还能看到有个本地变量表

这里展示了有3个本地变量,分别是代表实例的this,捕获的异常Exception和我们申明的变量score。

接着是StackMapTable部分,这个是为了在验证操作数栈和本地变量类型与操作命令是否相符而存在的,主要是为了提升跳转或者重置栈之后校验的效率。

调用

public class Student  {

    public void study(){
        int score = 1;
        try {
            score = 2;
        }catch(Exception e){
            score = 3;
        }finally{
            score = 4;
        }
    }

    public void display(){
        study();
    }

}

反编译之后,调用使用的invokevirtual命令

当然在jvm中调用方法的指令不止invokevirtual(用于非私有实例方法),还有invokestatic(用于静态方法调用),invokespecial(用于私有实例方法、构造函数、super关键字调用父类实例方法或者构造器、接口默认方法),invokeinterface(用于接口方法),invokedynamic(用于调用动态方法)

下一篇:
上海抗疫
本文目录
本文目录