为了便于理解,以TestHelloWorld类中有一个hello方法为例,学习字节码和源代码之间的关联性。
TestHelloWorld类的hello方法代码如下:
public String hello(String content) {String str = "Hello:";return str + content;}
hello方法是一个非静态方法,返回值是String,hello方法有一个String类型的参数。
编译后的栈指令如下:
{"opcodes": ["ldc #3 <Hello:>","astore_2","new #4 <java/lang/StringBuilder>","dup","invokespecial #5 <java/lang/StringBuilder.<init>>","aload_2","invokevirtual #6 <java/lang/StringBuilder.append>","aload_1","invokevirtual #6 <java/lang/StringBuilder.append>","invokevirtual #7 <java/lang/StringBuilder.toString>","areturn"]}
hello方法字节码解析
虽然hello方法的代码非常简单,但是翻译成指令后就会变得比较难以理解了,有很多细节是隐藏在编译细节中的,比如return str + content;是一个简单的两个字符串相加的操作,但实际上javac编译时会创建一个StringBuilder对象,然后调用append方法来实现str字符串和content字符串相加的。
hello方法字节码解析:
ldc表示的是将int, float或String型常量值从常量池中推送至栈顶,而ldc #3表示的是将常量池中的第三个索引位置压入栈顶,也就是Hello:;astore_2表示的是将栈顶的值存入到局部变量表的第二个位置,局部变量表的索引位置是从0开始的,因为hello方法是一个非静态方法,所以索引0表示的是this对象(如果是static方法那么就意味着没有this对象,索引0就应该表示第一个参数)。索引1表示的是hello方法的第一个参数,也就是String content。如果在方法体中想创建一个新的对象,那么就必须计算这个变量在局部变量表中的索引位置,否则无法存储对象。还有一个需要特别注意的点是long和double是宽类型(wide type)需要占用两个索引位置。astore_2实际上表达的是将栈顶的对象压入到局部变量表中,等价于String arg2 = new String("Hello:");new #4表示的是创建java/lang/StringBuilder类实例;dup表示复制栈顶数值并将复制值压入栈顶,即StringBuilder对象;invokespecial #5,invokespecial表示的是调用超类构造方法,实例初始化方法,私有方法,即调用StringBuilder类的构造方法(<init>),#5在常量池中是一个CONSTANT_METHOD_REF类型的对象,用于表示一个类方法引用,invokespecial #5实际上是在调用的StringBuilder的构造方法,等价于:new StringBuilder();aload_2表示的是加载局部变量表中的第二个变量,也就是读取astore_2存入的值,即Hello:。invokevirtual #6表示的是调用StringBuilder类的append方法,等价于:sb.append("Hello:");aload_1表示的是将局部变量表中的第一个变量压入栈顶,也就是将hello方法的第一个参数content的值压入栈顶;invokevirtual #6,再次调用StringBuilder类的append方法,等价于:sb.append(content);invokevirtual #7,调用StringBuilder类的toString方法,等价于:sb.toString();areturn表示的是返回一个引用类型对象,需要注意的是如果不同的数据类型需要使用正确的return指令;
hello方法的逻辑非常简单,如果只是看源代码的情况下我们可以秒懂该方法的执行流程和逻辑,但是如果我们从字节码层来看就会显得非常复杂不便于阅读;从第3步到第10步实际上只是在做源代码中的str + content字符串相加操作而已。正是因为直接阅读虚拟机的指令对我们是一种非常不好的体验,所以才会有根据字节码逆向生成Java源代码的需求,通过反编译工具我们能够非常好的阅读程序逻辑,从而省去阅读字节码和指令的压力。但是反编译工具不是万能的,某些时候在解析指令的时候可能会报错,甚至是崩溃,所以为了更好的分析类业务逻辑以及学习ASM字节码库,我们需要尽可能的掌握字节码解析和虚拟机指令解析的原理。
