开发者社区> 何度> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

JAVA-ASM学习笔记之java字节码简介

简介: 前言 由于之前接触了集团的故障演练平台monkeyking,对JavaAgent与ASM产生了兴趣。前段时间稍微总结了下JavaAgent的简单内容《JavaAgent学习笔记》。这次则想学习一下ASM(Java字节码操纵框架)。但是在学习的过程中,发现一直对类似如下的代码持续懵逼中,看来想要了解ASM,还是得学下JAVA字节码。 static ClassWriter createCl
+关注继续查看

前言

由于之前接触了集团的故障演练平台monkeyking,对JavaAgent与ASM产生了兴趣。前段时间稍微总结了下JavaAgent的简单内容《JavaAgent学习笔记》。这次则想学习一下ASM(Java字节码操纵框架)。但是在学习的过程中,发现一直对类似如下的代码持续懵逼中,看来想要了解ASM,还是得学下JAVA字节码。

    static ClassWriter createClassWriter(String className) {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        //声明一个类,使用JDK1.8版本,public的类,父类是java.lang.Object,没有实现任何接口  
        cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);
        //初始化一个无参的构造函数  
        MethodVisitor constructor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        constructor.visitVarInsn(Opcodes.ALOAD, 0);
        //执行父类的init初始化  
        constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        //从当前方法返回void    
        constructor.visitInsn(Opcodes.RETURN);
        constructor.visitMaxs(1, 1);
        constructor.visitEnd();
        return cw;
    }

class文件结构

public class Tester {
    private int intfiled = 1;
    private String strfiled = "test";
    public void run() {
        System.out.println("This is my first ASM test");
    }
    public void loop(){
        List<String> list = new ArrayList<>();
    }

} 

首先我们将以上代码使用javap -verbose Tester指令解析该类文件,将会得到以下内容,然后我们再根据class文件结构一起来研究一下。

Classfile /Users/archersblood/ideaworkspace/test/target/classes/Tester.class
  Last modified Dec 15, 2017; size 816 bytes
  MD5 checksum bb7e0325a8414bf572a09f35d0a36b24
  Compiled from "Tester.java"
public class Tester
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #11.#31        // java/lang/Object."<init>":()V
   #2 = Fieldref           #10.#32        // Tester.intfiled:I
   #3 = String             #33            // test
   #4 = Fieldref           #10.#34        // Tester.strfiled:Ljava/lang/String;
   #5 = Fieldref           #35.#36        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = String             #37            // This is my first ASM test
   #7 = Methodref          #38.#39        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #8 = Class              #40            // java/util/ArrayList
   #9 = Methodref          #8.#31         // java/util/ArrayList."<init>":()V
  #10 = Class              #41            // Tester
  #11 = Class              #42            // java/lang/Object
  #12 = Utf8               intfiled
  #13 = Utf8               I
  #14 = Utf8               strfiled
  #15 = Utf8               Ljava/lang/String;
  #16 = Utf8               <init>
  #17 = Utf8               ()V
  #18 = Utf8               Code
  #19 = Utf8               LineNumberTable
  #20 = Utf8               LocalVariableTable
  #21 = Utf8               this
  #22 = Utf8               LTester;
  #23 = Utf8               run
  #24 = Utf8               loop
  #25 = Utf8               list
  #26 = Utf8               Ljava/util/List;
  #27 = Utf8               LocalVariableTypeTable
  #28 = Utf8               Ljava/util/List<Ljava/lang/String;>;
  #29 = Utf8               SourceFile
  #30 = Utf8               Tester.java
  #31 = NameAndType        #16:#17        // "<init>":()V
  #32 = NameAndType        #12:#13        // intfiled:I
  #33 = Utf8               test
  #34 = NameAndType        #14:#15        // strfiled:Ljava/lang/String;
  #35 = Class              #43            // java/lang/System
  #36 = NameAndType        #44:#45        // out:Ljava/io/PrintStream;
  #37 = Utf8               This is my first ASM test
  #38 = Class              #46            // java/io/PrintStream
  #39 = NameAndType        #47:#48        // println:(Ljava/lang/String;)V
  #40 = Utf8               java/util/ArrayList
  #41 = Utf8               Tester
  #42 = Utf8               java/lang/Object
  #43 = Utf8               java/lang/System
  #44 = Utf8               out
  #45 = Utf8               Ljava/io/PrintStream;
  #46 = Utf8               java/io/PrintStream
  #47 = Utf8               println
  #48 = Utf8               (Ljava/lang/String;)V
{
  public Tester();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field intfiled:I
         9: aload_0
        10: ldc           #3                  // String test
        12: putfield      #4                  // Field strfiled:Ljava/lang/String;
        15: return
      LineNumberTable:
        line 4: 0
        line 5: 4
        line 6: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  this   LTester;

  public void run();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String This is my first ASM test
         5: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 8: 0
        line 9: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   LTester;

  public void loop();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #8                  // class java/util/ArrayList
         3: dup
         4: invokespecial #9                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 11: 0
        line 12: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   LTester;
            8       1     1  list   Ljava/util/List;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8       1     1  list   Ljava/util/List<Ljava/lang/String;>;
}
SourceFile: "Tester.java"

还有使用sublime打开Tester.java看到它的十六进制字节码内容:

cafe babe|0000 0034|0031 0a|00 0b|00 1f|09
000a 0020 0800 2109 000a 0022 0900 2300
2408 0025 0a00 2600 2707 0028 0a00 0800
1f07 0029 0700 2a01 0008 696e 7466 696c
6564 0100 0149 0100 0873 7472 6669 6c65
6401 0012 4c6a 6176 612f 6c61 6e67 2f53
7472 696e 673b 0100 063c 696e 6974 3e01
0003 2829 5601 0004 436f 6465 0100 0f4c
696e 654e 756d 6265 7254 6162 6c65 0100
124c 6f63 616c 5661 7269 6162 6c65 5461
626c 6501 0004 7468 6973 0100 084c 5465
7374 6572 3b01 0003 7275 6e01 0004 6c6f
6f70 0100 046c 6973 7401 0010 4c6a 6176
612f 7574 696c 2f4c 6973 743b 0100 164c
6f63 616c 5661 7269 6162 6c65 5479 7065
5461 626c 6501 0024 4c6a 6176 612f 7574
696c 2f4c 6973 743c 4c6a 6176 612f 6c61
6e67 2f53 7472 696e 673b 3e3b 0100 0a53
6f75 7263 6546 696c 6501 000b 5465 7374
6572 2e6a 6176 610c 0010 0011 0c00 0c00
0d01 0004 7465 7374 0c00 0e00 0f07 002b
0c00 2c00 2d01 0019 5468 6973 2069 7320
6d79 2066 6972 7374 2041 534d 2074 6573
7407 002e 0c00 2f00 3001 0013 6a61 7661
2f75 7469 6c2f 4172 7261 794c 6973 7401
0006 5465 7374 6572 0100 106a 6176 612f
6c61 6e67 2f4f 626a 6563 7401 0010 6a61
7661 2f6c 616e 672f 5379 7374 656d 0100
036f 7574 0100 154c 6a61 7661 2f69 6f2f
5072 696e 7453 7472 6561 6d3b 0100 136a
6176 612f 696f 2f50 7269 6e74 5374 7265
616d 0100 0770 7269 6e74 6c6e 0100 1528
4c6a 6176 612f 6c61 6e67 2f53 7472 696e
673b 2956 0021 000a 000b 0000 0002 0002
000c 000d 0000 0002 000e 000f 0000 0003
0001 0010 0011 0001 0012 0000 0042 0002
0001 0000 0010 2ab7 0001 2a04 b500 022a
1203 b500 04b1 0000 0002 0013 0000 000e
0003 0000 0004 0004 0005 0009 0006 0014
0000 000c 0001 0000 0010 0015 0016 0000
0001 0017 0011 0001 0012 0000 0037 0002
0001 0000 0009 b200 0512 06b6 0007 b100
0000 0200 1300 0000 0a00 0200 0000 0800
0800 0900 1400 0000 0c00 0100 0000 0900
1500 1600 0000 0100 1800 1100 0100 1200
0000 5300 0200 0200 0000 09bb 0008 59b7
0009 4cb1 0000 0003 0013 0000 000a 0002
0000 000b 0008 000c 0014 0000 0016 0002
0000 0009 0015 0016 0000 0008 0001 0019
001a 0001 001b 0000 000c 0001 0008 0001
0019 001c 0001 0001 001d 0000 0002 001e


Magic Number

     首先是魔数,一共4字节,而且固定不变,它的值是 0XCAFE BABE,用来标明是class文件类型,为了保证文件的安全性,将文件类型写在文件内部来保证不被篡改。
                  
    
      CAFE BABE,看来是当时开发者的恶趣味吧

Version

    Version字段分为minor_verion(主版本)和major_version(次版本),比如,我们使用java -version

java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

其中1.8为主版本,0_121为次版本,然后我们在org.objectweb.asm.Opcodes可以看到各主版本的数值

    int V1_1 = 3 << 16 | 45;
    int V1_2 = 0 << 16 | 46;
    int V1_3 = 0 << 16 | 47;
    int V1_4 = 0 << 16 | 48;
    int V1_5 = 0 << 16 | 49;
    int V1_6 = 0 << 16 | 50;
    int V1_7 = 0 << 16 | 51;
    int V1_8 = 0 << 16 | 52;
    int V1_9 = 0 << 16 | 53;

由于我使用的是maven工程,pom设置了jdk版本为1.8。所以对应就是主版本数值为52,对应的字节码0000 0034,3*16+4=52。

Constant Pool

常量池是Class文件中的资源仓库。常量池中主要存储2大类常量:字面量和符号引用。字面量如文本字符串的常量值等等,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符。简单的说字面量是值,符号引用是名称。

常量池的大小我们可以看到是0x0031,为3*16+1 = 49,然后有效值-1。刚好就是我们类解析之后的结果48个常量值。接着是0x0a,值是10,说明是tag为10的Methodref类型的数据。我们也可以结合之前对Tester.class文件的类解析的结果对照来理解常量池。

   #1 = Methodref          #11.#31        // java/lang/Object."<init>":()V
  #11 = Class              #42            // java/lang/Object
  
  #16 = Utf8               <init>
  #17 = Utf8               ()V
  
  #31 = NameAndType        #16:#17        // "<init>":()V
  
  #42 = Utf8               java/lang/Object

比如第一行,Methodref类型的数据,包含的11(0x000b)和31(ox001f=16+15)两个索引项,11索引指向Class,表明这个init方法是Object的,并且再次指向42的该类的全局限定名java/lang/Object。接着另一部分,31指向一个NameAndType(字段或方法描述),然后 NameAndType的方法名称指向16表明是一个类初始化方法,17的指向“()V”表示是个无参方法,如果是有参数,则在“()”放入参数类型全局限定名。然后是0x09,说明是Fieldref,然后依次类推。接着是一些UTF-8类型的,比如:“<init>”,java字节码保存的是十六进制的UTF8码,可以用以下代码进行转码,得出:3C696E69743E。这段字节码之后的01 0003 2829 56 则说明是tag为1的UTF-8,长度为3,内容是282956,正好是“()V”。最后的常量是(Ljava/lang/String;)V,值为284C6A6176612F6C616E672F537472696E673B2956,如下图。至此,常量池结束。


    public static void main(String[] args) throws Exception {
        String str = "<init>";
        printHexString(str.getBytes());
    }

    public static void printHexString(byte[] b) {
        for (int i = 0; i < b.length; i++) {
            String hex = Integer.toHexString(b[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            System.out.print(hex.toUpperCase());
        }
    }​

另外,在jdk7开始,又引入了以下三种。
CONSTANT_MethodHandle_info
CONSTANT_MethodType_info
CONSTANT_InvokeDynamic_info

Access Tag

常量池结束之后是访问标记符0x0021,ACC_PUBLIC(0x0001)+ACC_SUPER (0x0020)。public大家都懂,super在这里不做详细介绍,有兴趣的话可以可以看一下这篇文章《ACC_SUPER和早期的invokespecial》

    int ACC_PUBLIC = 0x0001; // class, field, method
    int ACC_PRIVATE = 0x0002; // class, field, method
    int ACC_PROTECTED = 0x0004; // class, field, method
    int ACC_STATIC = 0x0008; // field, method
    int ACC_FINAL = 0x0010; // class, field, method, parameter
    int ACC_SUPER = 0x0020; // class
    int ACC_SYNCHRONIZED = 0x0020; // method
    int ACC_OPEN = 0x0020; // module
    int ACC_TRANSITIVE = 0x0020; // module requires
    int ACC_VOLATILE = 0x0040; // field
    int ACC_BRIDGE = 0x0040; // method
    int ACC_STATIC_PHASE = 0x0040; // module requires
    int ACC_VARARGS = 0x0080; // method
    int ACC_TRANSIENT = 0x0080; // field
    int ACC_NATIVE = 0x0100; // method
    int ACC_INTERFACE = 0x0200; // class
    int ACC_ABSTRACT = 0x0400; // class, method
    int ACC_STRICT = 0x0800; // method
    int ACC_SYNTHETIC = 0x1000; // class, field, method, parameter, module *
    int ACC_ANNOTATION = 0x2000; // class
    int ACC_ENUM = 0x4000; // class(?) field inner
    int ACC_MANDATED = 0x8000; // parameter, module, module *
    int ACC_MODULE = 0x8000; // class

The class name

这个类文件的the class name 部分的值是0x000a。对应常量池索引为10。

  #10 = Class              #41            // Tester
  #41 = Utf8               Tester

Super class name

这个类文件的super class name 部分的值是0x000b。对应常量池索引为11。

  #11 = Class              #42            // java/lang/Object
  #42 = Utf8               java/lang/Object

Interface

这部分数据为0x0000,说明未实现任何接口。

Fields

private int intfiled = 1;
private String strfiled = "test";

 
这部分数据起始为0x0002,说明是有2个字段。下图是field的结构图 。首先第一个fields结构体是 开头是access_flags值为0x0002,说明是private,然后是name_index0x000c,名称指向常量池12,“intfiled”。接着是descriptor0x000d,指向13 “I”,说明是这个字段是整数类型。接着是属性数0x0000,说明没有属性值。接着是第二个字段。开头是access_flags值依旧为0x0002,说明是private,然后是name_index0x000e,名称指向常量池14为“strfiled”。接着是descriptor0x000f,执向15“Ljava/lang/String”,说明这个字段是个String。然后又是0x0000,说明没有属性值。

Method


然后是麻烦的Method区域,这里放置的是这个类的所有方法。它的结构体与字段相同。首先看到的是0x0003,说明这个类共3个方法。一个默认的init(构造方法)还有run()与loop()。

public Tester() {
}

public void run() {
System.out.println("This is my first ASM test");
}

public void loop() {
new ArrayList();
}

第一个方法。开头是accessflags值为0x0001,说明是public,然后是name_index0x0010,名称指向常量池16 "<init>",  接着是descriptor0x00011,指向17 “()V”,说明是这个无参方法。接着是属性数0x0001,说明有1个属性。然后根据这个属性的结构体。name_index是0x0012,指向18“code”,然后0x0000 0042,说明code属性长度是4*16+2=66。code部分解析更加复杂,其结构体如下图。具体解析这里不再详细介绍(里面包含了操作符之类的信息)。
attribute:  code: 
第二个方法。开头是accessflags值为0x0001,说明是public,然后是name_index0x0017,名称指向常量池23 "run",  接着是descriptor0x00011,指向17 “()V”,说明是这个无参方法。接着是属性数0x0001,说明有1个属性。然后根据这个属性的结构体。name_index是0x0012,指向18“code”,然后是0x0000 0037,说明长度是3*16+7 = 55(u1)
第三个方法。开头是accessflags值为0x0001,说明是public,然后是name_index0x0018,名称指向常量池24 "loop",  接着是descriptor0x00011,指向17 “()V”,说明是这个无参方法。接着是属性数0x0001,说明有1个属性。然后根据这个属性的结构体。name_index是0x0012,指向18“code”,然后是0x0000 0053,说明长度是5*16+3 = 83(u1)

Attributes

这个字段一般是记录文件属性的。它的结构为

信息开头是0x0001,说明是数组长度为1,然后是0x001d,索引为29,常量池指向"SourceFile", 然后是长度0x0000 0002,最后就是0x001e,索引为30,常量池内容为“Tester.java”。这样一个.class文件就解析完毕了。


经过完整的解析,一个.class文件的结构就清晰展现了出来。

cafe babe                                		魔数
0000 0034                                		版本
0031  											常量池
	0a00 0b00 1f09                       
	000a 0020 0800 2109 000a 0022 0900 2300
	2408 0025 0a00 2600 2707 0028 0a00 0800
	1f07 0029 0700 2a01 0008 696e 7466 696c
	6564 0100 0149 0100 0873 7472 6669 6c65
	6401 0012 4c6a 6176 612f 6c61 6e67 2f53
	7472 696e 673b 0100 063c 696e 6974 3e01
	0003 2829 5601 0004 436f 6465 0100 0f4c
	696e 654e 756d 6265 7254 6162 6c65 0100
	124c 6f63 616c 5661 7269 6162 6c65 5461
	626c 6501 0004 7468 6973 0100 084c 5465
	7374 6572 3b01 0003 7275 6e01 0004 6c6f
	6f70 0100 046c 6973 7401 0010 4c6a 6176
	612f 7574 696c 2f4c 6973 743b 0100 164c
	6f63 616c 5661 7269 6162 6c65 5479 7065
	5461 626c 6501 0024 4c6a 6176 612f 7574
	696c 2f4c 6973 743c 4c6a 6176 612f 6c61
	6e67 2f53 7472 696e 673b 3e3b 0100 0a53
	6f75 7263 6546 696c 6501 000b 5465 7374
	6572 2e6a 6176 610c 0010 0011 0c00 0c00
	0d01 0004 7465 7374 0c00 0e00 0f07 002b
	0c00 2c00 2d01 0019 5468 6973 2069 7320
	6d79 2066 6972 7374 2041 534d 2074 6573
	7407 002e 0c00 2f00 3001 0013 6a61 7661
	2f75 7469 6c2f 4172 7261 794c 6973 7401
	0006 5465 7374 6572 0100 106a 6176 612f
	6c61 6e67 2f4f 626a 6563 7401 0010 6a61
	7661 2f6c 616e 672f 5379 7374 656d 0100
	036f 7574 0100 154c 6a61 7661 2f69 6f2f
	5072 696e 7453 7472 6561 6d3b 0100 136a
	6176 612f 696f 2f50 7269 6e74 5374 7265
	616d 0100 0770 7269 6e74 6c6e 0100 1528
	4c6a 6176 612f 6c61 6e67 2f53 7472 696e
	673b 2956 
0021 											访问标记符
000a                                            类名
000b 											父类名
0000                							被实现接口
0002 											字段
	 0002 000c 000d 0000 
	 0002 000e 000f 0000 
0003											方法
	 0001 0010 0011 0001 0012 0000 0042 0002
	 0001 0000 0010 2ab7 0001 2a04 b500 022a
     1203 b500 04b1 0000 0002 0013 0000 000e
	 0003 0000 0004 0004 0005 0009 0006 0014
     0000 000c 0001 0000 0010 0015 0016 0000

	 0001 0017 0011 0001 0012 0000 0037 0002
	 0001 0000 0009 b200 0512 06b6 0007 b100
     0000 0200 1300 0000 0a00 0200 0000 0800
     0800 0900 1400 0000 0c00 0100 0000 0900
	 1500 1600 00

       00 0100 1800 1100 0100 1200 0000 5300 
     0200 0200 0000 09bb 0008 59b7 0009 4cb1 
     0000 0003 0013 0000 000a 0002 0000 000b 
     0008 000c 0014 0000 0016 0002 0000 0009 
     0015 0016 0000 0008 0001 0019 001a 0001 
     001b 0000 000c 0001 0008 0001
     0019 001c 
0001                                       		类属性
	 0001 001d 0000 0002 001e

 

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
线程 - Java 多线程编程(下)
线程 - Java 多线程编程(下)
25 0
线程 - Java 多线程编程(上)
线程 - Java 多线程编程(上)
62 0
Java多线程那些事,对Java并发编程2w余字的总结,超详细(从入门到完全掌握)
Java多线程那些事,对Java并发编程2w余字的总结,超详细(从入门到完全掌握)
79 0
java多线程中的死锁、活锁、饥饿、无锁都是什么鬼?
死锁、活锁、饥饿是关于多线程是否活跃出现的运行阻塞障碍问题,如果线程出现了这三种情况,即线程不再活跃,不能再正常地执行下去了。
68 0
五分钟带你玩转多线程(一)java多线程基础知识简介
线程概念 进程:是一个执行中的程序,如打开网易云音乐,网易云音乐就是一个进程 线程:是进程的组成,一个进程包含多个线程,是jvm最小调度单元。如网易云音乐听歌是一个线程,评价是一个线程。
50 0
Java的并发编程中的多线程问题到底是怎么回事儿?
原创: Hollis 在我之前的一篇《再有人问你Java内存模型是什么,就把这篇文章发给他。》文章中,介绍了Java内存模型,通过这篇文章,大家应该都知道了Java内存模型的概念以及作用,这篇文章中谈到,在Java并发编程中,通常会遇到三个问题,即原子性问题、一致性问题和有序性问题。
1057 0
+关注
何度
coder
4
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载