前言
由于之前接触了集团的故障演练平台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