【Class 文件结构】— 自己动手解析 Class 文件
如果你对 Class 文件还熟悉的话,你应该知道 Class 文是一组以 8 位字节为单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件之中。也就是说,我们可以像读普通二进制文件一样读取 Class 文件,只不过需要遵循一定的规范(Java 虚拟机规范)。于是便有了用 Java 代码解析 Class 文件的想法。
Class 文件
Java 虚拟机规范定义了 u1、u2、u4 和 u8 来分别表示 1 个字节、2 个字节、4 个字节和 8 个字节。在 Java 语言中也就是分别对应着 byte、short、int 和 long。另外 float 占 4 个字节,double 占 8个字节。
Class 文件格式如下所示
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count - 1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
我们只要按照上表的顺序依次解析就可以,遇到表(以 _info 结尾)这种数据结构,那么就找到该表的结构来解析。
如何解析
解析的第一步就是要解决读取字节的问题。这里我采用的是 DataInputStream
来解析。JDK 中是这么介绍 DataInputStream
A data input stream lets an application read primitive Java data types from an underlying input stream in a machine-independent way.
数据输入流允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型
DataInputStream
中定义了读取readByte()
、 readShort()
、 readInt()
、 readLong()
、 readFloat()
和 readDouble()
。比如我们要读取 magic,数据类型为 u4,也就是占 4 个字节,我们便可以用 readInt()
来读取。
代码示例
首先定义ClassInfo
类来表示 Class 文件
@Data
public class ClassInfo {
private int magic;
private short minorVersion;
private short majorVersion;
private short constantPoolCount;
private ConstantPool cpInfo;
private String accessFlags;
private String classFullyQualifiedName;
private String superClassFullyQualifiedName;
private List<String> interfaceList;
private FieldMethodInfo[] fieldTable;
private FieldMethodInfo[] methodTable;
private AttributeInfo[] attrTable;
}
可以看到ClassInfo
属性分别都对应着 Class 文件中的结构定义。然后就是定义解析类ClassFileAnalyzer
,如下
public class ClassFileAnalyzer {
public static ClassInfo analyze(String classFilePath) {
try {
InputStream input = new FileInputStream(classFilePath);
return analyzeClassFile(new DataInputStream(input));
} catch (FileNotFoundException e) {
throw new RuntimeException("file not found", e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static ClassInfo analyzeClassFile(DataInputStream inputStream) throws IOException {
ClassInfo classInfo = new ClassInfo();
// magic
classInfo.setMagic(inputStream.readInt());
// version
classInfo.setMinorVersion(inputStream.readShort());
classInfo.setMajorVersion(inputStream.readShort());
// constant_pool_count
classInfo.setConstantPoolCount(inputStream.readShort());
// constant_pool
ConstantPool constantPool = new ConstantPool(classInfo.getConstantPoolCount() - 1);
constantPool.analyze(inputStream);
classInfo.setCpInfo(constantPool);
// access_flag
short classAccessFlag = inputStream.readShort();
classInfo.setAccessFlags(new ClassAccessFlag().getAccessFlags(classAccessFlag));
// class_index, super_class_index
String classFQN = ConstantPool.getStringByIndex(inputStream.readShort());
String superFQN = ConstantPool.getStringByIndex(inputStream.readShort());
classInfo.setClassFullyQualifiedName(classFQN);
classInfo.setSuperClassFullyQualifiedName(superFQN);
// interface list
short interfaces = inputStream.readShort();
List<String> interfaceList = new ArrayList<>(interfaces);
for (int i = 0; i < interfaces; i++) {
interfaceList.add(ConstantPool.getStringByIndex(inputStream.readShort()));
}
classInfo.setInterfaceList(interfaceList);
// field_info
FieldMethodInfo[] fieldTable = readFieldOrMethodTable(inputStream, 1);
classInfo.setFieldTable(fieldTable);
// method_info
FieldMethodInfo[] methodTable = readFieldOrMethodTable(inputStream, 2);
classInfo.setMethodTable(methodTable);
// attribute_info
short attrCount = inputStream.readShort();
AttributeInfo[] attrTable = new AttributeInfo[attrCount];
for (int j = 0; j < attrCount; j++) {
AttributeInfo attrInfo = AttributeInfo.readAttributeInfo(inputStream);
attrTable[j] = attrInfo;
}
classInfo.setAttrTable(attrTable);
return classInfo;
}
}
解析的顺序只要按照 Class 文件中结构的顺序就 OK。最后的测试类
@Test
public void test() throws Exception {
ClassFileAnalyzer.analyze("C:/Users/Administrator/Desktop/TestClass.class").print();
}
解析出来的效果图如下
magic: cafebabe
minor version: 0
major version: 51
Access flags: ACC_PUBLIC ACC_SUPER
Constant pool: 46
#1=Methodref java/lang/Object.<init>:()V
#2=Fieldref com/somelogs/bug/TestClass.m:I
#3=Fieldref com/somelogs/bug/TestClass.A:I
#4=Class com/somelogs/bug/TestClass
#5=Class java/lang/Object
#6=Class java/io/Serializable
#7=Utf8 HELLO
#8=Utf8 Ljava/lang/String;
#9=Utf8 ConstantValue
#10=String hello
#11=Utf8 A
#12=Utf8 I
#13=Utf8 B
#14=Utf8 D
#15=Double 3.2
#17=Utf8 C
#18=Utf8 J
#19=Long 23L
#21=Utf8 m
#22=Utf8 <init>
#23=Utf8 ()V
#24=Utf8 Code
#25=Utf8 LineNumberTable
#26=Utf8 LocalVariableTable
#27=Utf8 this
#28=Utf8 Lcom/somelogs/bug/TestClass;
#29=Utf8 add
#30=Utf8 ()I
#31=Utf8 Exceptions
#32=Class java/lang/IllegalArgumentException
#33=Class java/lang/NullPointerException
#34=Utf8 <clinit>
#35=Utf8 SourceFile
#36=Utf8 TestClass.java
#37=NameAndType <init>:()V
#38=NameAndType m:I
#39=NameAndType A:I
#40=Utf8 com/somelogs/bug/TestClass
#41=Utf8 java/lang/Object
#42=Utf8 java/io/Serializable
#43=Utf8 hello
#44=Utf8 java/lang/IllegalArgumentException
#45=Utf8 java/lang/NullPointerException
Class FQN: com/somelogs/bug/TestClass
Super class FQN: java/lang/Object
Interfaces: 1
java/io/Serializable
Fields Count: 5
public static final Ljava/lang/String; HELLO
{type=ConstantValue, value=hello}
public static I A
public static final D B
{type=ConstantValue, value=3.2}
public static final J C
{type=ConstantValue, value=23L}
private I m
Method Count: 3
public ()V <init>
{type=Code, maxStack=1, maxLocals=1, codeLength=5, opcodes=[2a, b7, 0, 1, b1], exceptions=[], attribute=[{type=LineNumberTable, table=[{start=0, lineNumber=10}]}, {type=LocalVariableTable, LocalVariableInfo=[{start=0, length=5, name=this, descriptor=Lcom/somelogs/bug/TestClass;, index=0}]}]}
public ()I add
{type=Code, maxStack=2, maxLocals=1, codeLength=7, opcodes=[2a, b4, 0, 2, 4, 60, ac], exceptions=[], attribute=[{type=LineNumberTable, table=[{start=0, lineNumber=20}]}, {type=LocalVariableTable, LocalVariableInfo=[{start=0, length=7, name=this, descriptor=Lcom/somelogs/bug/TestClass;, index=0}]}]}
{type=Exceptions, exceptions=[java/lang/IllegalArgumentException, java/lang/NullPointerException]}
static ()V <clinit>
{type=Code, maxStack=1, maxLocals=0, codeLength=7, opcodes=[11, 0, e8, b3, 0, 3, b1], exceptions=[], attribute=[{type=LineNumberTable, table=[{start=0, lineNumber=13}]}]}
Attribute Count: 1
{type=SourceFile, fileName=TestClass.java}
总结
Class 文件结构并不复杂,只要你弄清楚它的数据结构以及排列顺序,一切看来起来都是那么顺其自然。ClassFileAnalyzer 的完整代码放在我的 Github 上,希望对你有所帮助。