📚《深入浅出JVM字节码》
《Java虚拟机字节码:从入门到实战》的开源版本。作者通过自己的实战经验,整合出一套适合新手的高效学习教程。归纳并提炼知识点,制定合理路线,帮助读者更快掌握核心技术。
如果想要深入理解某个属性,我们可再对其进行二次解析。如何使用我们编写的项目对class文件结构、字段结构、方法结构的属性表中的属性进行二次解析呢?我们以字段的ConstantValue属性和方法的Code属性为例。
ConstantValue属性用于通知虚拟机在类或接口初始化阶段为被标志为ACC_STATIC & ACC_FINAL的静态常量字段自动赋值常量。
提示:在接口中声明的字段会被编译器加上ACC_STATIC & ACC_FINAL标志。
字段结构的属性表最多只能有一个ConstantValue属性。字段的类型必须是基本数据类型或者String类,因为从常量池中只能引用到基本类型和String类型的字面量。
ConstantValue属性的结构如下:
public class ConstantValue_attribute {
private U2 attribute_name_index;
private U4 attribute_length;
private U2 constantvalue_index;
}
与常量池的常量结构不同,常量池的常量都有一个tag字段映射到哪种常量结构,而属性表的属性结构只能通过attribute_name_index判断。
以一个例子来说明ConstantValue属性的使用场景。
在一个接口中定义一个字段并赋值,通过分析其Class文件结构,找到这个字段的属性表,看是否有ConstantValue属性。该测试接口代码如下。
public interface TestConstantValueInterface {
int value = 1000;
}
现在,我们需要编写一个属性解析工具类,添加支持解析ConstantValue属性的静态方法,以完成属性的二次解析工作,代码如下。
public class AttributeProcessingFactory {
public static ConstantValue_attribute processingConstantValue(AttributeInfo attributeInfo) {
ConstantValue_attribute attribute = new ConstantValue_attribute();
attribute.setAttribute_name_index(attributeInfo.getAttribute_name_index());
attribute.setAttribute_length(attributeInfo.getAttribute_length());
attribute.setConstantvalue_index(new U2(attributeInfo.getInfo()[0], attributeInfo.getInfo()[1]));
return attribute;
}
}
因为当前只实现了ConstantValue属性的解析,所以单元测试中只对名称为“ConstantValue”的属性进行二次解析,单元测试代码如下。
public class ConstantValueAttributeTest {
@Test
public void testConstantValue() throws Exception {
ByteBuffer codeBuf = ClassFileAnalysisMain.readFile(".../TestConstantValueInterface.class");
ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
// 获取所有字段
FieldInfo[] fieldInfos = classFile.getFields();
for (FieldInfo fieldInfo : fieldInfos) {
// 获取字段的所有属性
AttributeInfo[] attributeInfos = fieldInfo.getAttributes();
if (attributeInfos == null || attributeInfos.length == 0) {
continue;
}
System.out.println("字段:" + classFile.getConstant_pool()
[fieldInfo.getName_index().toInt() - 1]);
// 遍历所有属性
for (AttributeInfo attributeInfo : attributeInfos) {
// 获取属性的名称
U2 name_index = attributeInfo.getAttribute_name_index();
CONSTANT_Utf8_info name_info = (CONSTANT_Utf8_info)
classFile.getConstant_pool()[name_index.toInt() - 1];
String name = new String(name_info.getBytes());
// 如果属性名是ConstantValue,则对该属性进行二次解析
if (name.equalsIgnoreCase("ConstantValue")) {
// 属性二次解析
ConstantValue_attribute constantValue = AttributeProcessingFactory.processingConstantValue(attributeInfo);
// 取得constantvalue_index,从常量池中取值
U2 cv_index = constantValue.getConstantvalue_index();
Object cv = classFile.getConstant_pool() [cv_index.toInt() - 1];
// 需要判断常量的类型
if (cv instanceof CONSTANT_Utf8_info) {
System.out.println("ConstantValue:" + cv.toString());
} else if (cv instanceof CONSTANT_Integer_info) {
System.out.println("ConstantValue:" +
((CONSTANT_Integer_info) cv).getBytes().toInt());
} else if (cv instanceof CONSTANT_Float_info) {
// todo
} else if (cv instanceof CONSTANT_Long_info) {
// todo
} else if (cv instanceof CONSTANT_Double_info) {
// todo
}
}
}
}
}
}
对字段的属性进行二次解析的步骤:
单元测试结果如下图所示。
如图所示,字段名为value的字段其属性表有一个ConstantValue属性,常量值是1000。
一个方法编译后生成的字节码指令存储在方法结构的属性表的Code属性中,但并非每个方法都有Code属性,如声明为native的方法、abstract抽象方法、接口中的非default方法就没有Code属性。实例初始化方法、类或接口的初始化方法都会有Code属性。
这一节我们将通过完成对Code属性的二次解析了解Code属性,了解字节码指令是如何存储的在Code属性中的。
方法结构的属性表中最多只能有一个Code属性,Code属性是种可变长的属性,属性中包含了方法的字节码指令及相关辅助信息。
Code属性的结构:
字段名 | 类型 | 说明 |
---|---|---|
attribute_name_index | u2 | 指向常量池中“Code”常量的索引 |
attribute_length | u4 | 属性的长度(不包括attribute_name_index和attribute_length占用的长度) |
max_stack | u2 | 操作数栈的最大深度 |
max_locals | u2 | 局部变量表的最大深度 |
code_length | u4 | code数组的大小 |
code | u1[] | 存储字节码指令 |
exception_table_length | u2 | 异常表的长度 |
exception_table | exception[] | 异常表 |
attributes_count | u2 | 属性总数 |
attributes | attribute_info[] | 属性表 |
max_stack与max_locals分别对应操作数栈的大小和局部变量表的大小。
code项用一个字节数组存储该方法的所有字节码指令,code_length存储该code数组的大小。
属性也可以有属性表,attributes项便是Code属性的属性表,在Code属性中,属性表可能存在的属性如LineNumerTable属性、LocalVariableTable属性。
LineNumerTable属性:被用来映射源码文件中给定的代码行号对应code[]字节码指令中的哪一部分,在调试时用到,在方法抛出异常打印异常栈信息也会用到。
LocalVariableTable属性:用来描述code[]中的某个范围内,局部变量表中某个位置存储的变量的名称是什么,用于与源码文件中局部变量名做映射。该属性不一定会编译到class文件中,如果没有该属性,那么查看反编译后的java代码将会使用诸如arg0、arg1、arg2之类的名称代替局部变量的名称。
exception_table用于存储方法中的所有try-catch信息,exception_table_length存储该异常表数组的大小。异常表的每一项都是固定的结构体,异常结构如下表格所示。
字段名 | 类型 | 说明 |
---|---|---|
start_pc | u2 | try的开始位置,在code[]中的索引 |
end_pc | u2 | try的结束位置,在code[]中的索引。 |
handler_pc | u2 | 异常处理的起点,在code[]中的索引。 |
catch_type | u2 | 为0相当finally块。不为0时,指向常量池中某个CONSTANT_Class_info常量的索引,表示需要捕获的异常,只有[start_pc,end_pc)范围内抛出的异常是指定的类或子类的实例,才会跳转到handler_pc指向的字节码指令继续执行。 |
为Code属性结构创建Java类。
public class Code_attribute {
private U2 attribute_name_index;
private U4 attribute_length;
private U2 max_stack;
private U2 max_locals;
private U4 code_length;
private byte[] code;
private U4 exception_table_length;
private Exception[] exception_table;
private U2 attributes_count;
private AttributeInfo[] attributes;
// 异常表中每项的结构
public static class Exception {
private U2 start_pc;
private U2 end_pc;
private U2 handler_pc;
private U2 catch_type;
}
}
对Code属性进行二次解析主要是想拿到字节码信息,属性表和异常表我们就不解析了,异常表在本书第三章再详细介绍。
对Code属性二次解析代码如下。
public class AttributeProcessingFactory{
public static Code_attribute processingCode(AttributeInfo attributeInfo) {
Code_attribute code = new Code_attribute();
ByteBuffer body = ByteBuffer.wrap(attributeInfo.getInfo());
// 操作数栈大小
code.setMax_stack(new U2(body.get(),body.get()));
// 局部变量表大小
code.setMax_locals(new U2(body.get(),body.get()));
// 字节码数组长度
code.setCode_length(new U4(body.get(),body.get(),body.get(),body.get()));
// 解析获取字节码
byte[] byteCode = new byte[code.getCode_length().toInt()];
body.get(byteCode,0,byteCode.length);
code.setCode(byteCode);
return code;
}
}
现在编写单元测试,先将class文件解析为一个ClassFile对象,然后再遍历该ClassFile中的方法表,获取每个方法中的Code属性,再对Code属性进行二次解析。
单元测试代码如下。
public class CodeAttributeTest {
@Test
public void testCodeAttribute() throws Exception {
ByteBuffer codeBuf = ClassFileAnalysisMain.readFile("RecursionAlgorithmMain.class");
ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
// 获取方法表
MethodInfo[] methodInfos = classFile.getMethods();
// 遍历方法表
for (MethodInfo methodInfo : methodInfos) {
// 获取方法的属性表
AttributeInfo[] attributeInfos = methodInfo.getAttributes();
if (attributeInfos == null || attributeInfos.length == 0) {
continue;
}
System.out.println("方法:" + classFile.getConstant_pool()
[methodInfo.getName_index().toInt() - 1]);
// 遍历属性表
for (AttributeInfo attributeInfo : attributeInfos) {
// 获取属性的名称
U2 name_index = attributeInfo.getAttribute_name_index();
CONSTANT_Utf8_info name_info = (CONSTANT_Utf8_info)
classFile.getConstant_pool()[name_index.toInt() - 1];
String name = new String(name_info.getBytes());
// 对Code属性二次解析
if (name.equalsIgnoreCase("Code")) {
// 属性二次解析
Code_attribute code = AttributeProcessingFactory.processingCode(attributeInfo);
System.out.println("操作数栈大小:" + code.getMax_stack().toInt());
System.out.println("局部变量表大小:" + code.getMax_locals().toInt());
System.out.println("字节码数组长度:" + code.getCode_length().toInt());
System.out.println("字节码:");
for (byte b : code.getCode()) {
System.out.print((b & 0xff) + " ");
}
System.out.println("\n");
}
}
}
}
}
例子中使用到的RecursionAlgorithmMain类代码如下。
public class RecursionAlgorithmMain {
private static volatile int value = 0;
static int sigma(int n) {
value = n;
System.out.println("current 'n' value is " + n);
return n + sigma(n + 1);
}
public static void main(String[] args) throws IOException {
new Thread(() -> sigma(1)).start();
System.in.read();
System.out.println(value);
}
}
以RecursionAlgorithmMain的sigma方法为例,首先使用javap查看sigma方法的字节码,目的是对比我们编写的解析工具解析的结果,验证解析结果是否正确。
使用编写好的解析工具解析sigma方法的Code属性,结果如下图所示。
这里我们还需要将字节码转为十六进制输出,方便与字节码指令表对照。
byte[]转十六进制字符串工具类的实现代码如下。
public class HexStringUtils {
public static String toHexString(byte[] codes) {
StringBuilder codeStrBuild = new StringBuilder();
int i=0;
for (byte code : codes) {
// toHexString将字节转十六进制,实现略...
codeStrBuild.append(toHexString(code)).append(" ");
if(++i==9){
i=0;
codeStrBuild.append("\n");
}
}
return codeStrBuild.toString();
}
}
将字节码转为十六进制字符串后输出的结果如下图所示。
其中,字节码部分的0x1A对应iload_0指令,0xB3对应putstatic指令,由于putstatic指令需要一个操作数,因此0xB3后面的0x00 02就是指令所需的操作数。
发布于:2021 年 07 月 24 日
作者: 吴就业
链接: https://github.com/wujiuye/JVMByteCodeGitBook
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
📚目录
订阅
订阅新文章发布通知吧,不错过精彩内容!
输入邮箱,提交后我们会给您发送一封邮件,您需点击邮件中的链接完成订阅设置。