class文件可视化工具
Author:guanjt(解析class文件),Liujr(UI化)
1.目录结构
|—JVM
|—ch03
|—classpath
|—classfile
|—cmd
|—main
文件夹含义:
cmd —— 捕获控制台输入
main —— 主函数入口
classfile —— *.class文件搜寻
classpath —— *.class文件解析
2.class文件分析
2.1 测试java文件
public class ClassFileTest{
public static final boolean FLAG = true;
public static final byte BYTE = 123;
public static final short SHORT = 12345;
public static final char x = 'X';
public static final int INT = 123456789;
public static final long LONG = 12345678901L;
public static final float PI = 3.14f;
public static final double E = 2.71828;
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
2.2 解析文件
Go与java的数据类型的对比
Go语言类型 | Java语言类型 | 说明 |
int8 | byte | 8比特有符号整数 |
uint8 | N/A | 8比特无符号整数 |
int16 | short | 16比特有符号整数 |
uint16 | char | 16比特无符号整数 |
int32 (别名rune) | int | 32比特有符号整数 |
uint32 | N//A | 32比特无符号整数 |
int64 | long | 64比特有符号整数 |
uint64 | N/A | 64比特无符号整数 |
float32 | float | 32比特IEEE-754浮点数 |
float64 | double | 64比特IEEE-754浮点数 |
2.2.1 class文件解析效果(javap工具)
2.2.2 读取数据(class_reader)
解析class文件可以当作字节流处理读取。定义的文件位Class_reader.go。主要是提供几个字节流读取方法(针对class文件的类型来设计的)供我们使用。
func readUint8() // 读取u1类型的数据
func readUint16() // 读取u2类型的数据。
func readUint32() // 读取u4类型数据。
func readUint64() // 读取uint64位的数据(java虚拟机规范中没有定义u8)类型数据
func readUint16s() // 读取uint16表,表的大小由开头的uint16数据类型。
func readBytes() // 读取指定数量的字节
2.2.3 整体结构(class_file)
定义结构体
type ClassFile struct {
magic uint32
minorVersion uint16
majorVersion uint16
constantPool ConstantPool
accessFlags uint16
thisClass uint16
superClass uint16
interfaceClass []uint16
fields [] *MemberInfo
methods [] *MemberInfo
attributes [] AttributeInfo
}
magic:魔数,很多文件格式都会规定满足该格式的文件必须以某几个固定字节开头,就叫做魔数。如果解析的class文件不符合格式要求就会抛出异常。(开头四个字节)如图所示:
0xCAFEBABE 这个就代表了可以识别的class文件
实现代码:
// 读取魔数,如果不是class文件特定的开头,就抛出异常。
func (self *ClassFile) readAndCheckMagic(reader *ClassReader){
magic := reader.readUint32()
if magic != 0xCAFEBABE {
panic("Java.lang.ClassFormatError:magic")
}
}
minorVersion,majorVersion 版本号:主版本号和次版本号。主版本号是M次版本号是m,则组合起来就是M.m,特定的JAVA虚拟机只能支持特定范围内的版本号的class文件(比如JDK8就是52.0)具体如图所示
00 为次版本号,0x34 为主版本号
然后紧接着就是常量池:
0038 代表了该java文件中所包含的容量大小。0038 => 55 代表有55个常量存在。后面开始就是第一个常量。后面两个字节代表了类型。然后根据类型我们去查找后面几个字节是代表的位置。基本类型和实现代码如下。
switch (tag) {
case TagInfo.CONSTANT_UTF8:
constantMemberInfo.setConstantName("CONSTANT_Utf8_info");
constantMemberInfo.setConstantSize(3);
constantMemberInfo.setConstantType("u2;u1;");
return constantMemberInfo;
case TagInfo.CONSTANT_INTEGER:
constantMemberInfo.setConstantName("CONSTANT_Integer_info");
constantMemberInfo.setConstantSize(4);
constantMemberInfo.setConstantType("u4;");
return constantMemberInfo;
case TagInfo.CONSTANT_FLOAT:
constantMemberInfo.setConstantName("CONSTANT_Float_info");
constantMemberInfo.setConstantSize(4);
constantMemberInfo.setConstantType("u4;");
return constantMemberInfo;
case TagInfo.CONSTANT_LONG:
constantMemberInfo.setConstantName("CONSTANT_Long_info");
constantMemberInfo.setConstantSize(8);
constantMemberInfo.setConstantType("u8;");
return constantMemberInfo;
case TagInfo.CONSTANT_DOUBLE:
constantMemberInfo.setConstantName("CONSTANT_Double_info");
constantMemberInfo.setConstantSize(8);
constantMemberInfo.setConstantType("u8;");
return constantMemberInfo;
case TagInfo.CONSTANT_CLASS:
constantMemberInfo.setConstantName("CONSTANT_Class_info");
constantMemberInfo.setConstantSize(2);
constantMemberInfo.setConstantType("u2;");
return constantMemberInfo;
case TagInfo.CONSTANT_STRING:
constantMemberInfo.setConstantName("CONSTANT_String_info");
constantMemberInfo.setConstantSize(2);
constantMemberInfo.setConstantType("u2;");
return constantMemberInfo;
case TagInfo.CONSTANT_FIELD_REF:
constantMemberInfo.setConstantName("CONSTANT_Fieldref_info");
constantMemberInfo.setConstantSize(4);
constantMemberInfo.setConstantType("u2;u2;");
return constantMemberInfo;
case TagInfo.CONSTANT_METHOD_REF:
constantMemberInfo.setConstantName("CONSTANT_Methodref_info");
constantMemberInfo.setConstantSize(4);
constantMemberInfo.setConstantType("u2;u2;");
return constantMemberInfo;
case TagInfo.CONSTANT_INTERFACE_METHOD_REF:
constantMemberInfo.setConstantName("CONSTANT_InterfaceMethodref_info");
constantMemberInfo.setConstantSize(4);
constantMemberInfo.setConstantType("u2;u2;");
return constantMemberInfo;
case TagInfo.CONSTANT_NAME_AND_TYPE:
constantMemberInfo.setConstantName("CONSTANT_NameAndType_info");
constantMemberInfo.setConstantSize(4);
constantMemberInfo.setConstantType("u2;u2;");
return constantMemberInfo;
}
目前我只写了以上这些,支持到JDK8。
TagInfo对应如下:
/**
* UTF-8编码的Unicode字符串
*/
public final static int CONSTANT_UTF8 = 1;
/**
* int类型的字面值
*/
public final static int CONSTANT_INTEGER = 3;
/**
* float类型的字面值
*/
public final static int CONSTANT_FLOAT = 4;
/**
* long类型的字面值
*/
public final static int CONSTANT_LONG = 5;
/**
* double类型的字面值
*/
public final static int CONSTANT_DOUBLE = 6;
/**
* 类类型常量
*/
public final static int CONSTANT_CLASS = 7;
/**
* 字符串常量
*/
public final static int CONSTANT_STRING = 8;
/**
* 对一个字段的符号引用
*/
public final static int CONSTANT_FIELD_REF = 9;
/**
* 对一个类中方法的符号引用
*/
public final static int CONSTANT_METHOD_REF = 10;
/**
* 对一个接口中方法的符号引用
*/
public final static int CONSTANT_INTERFACE_METHOD_REF = 11;
/**
* 对一个字段或方法的部分符号引用
*/
public final static int CONSTANT_NAME_AND_TYPE = 12;
如图:第一个两个字节是0A = 10,然后我们对应找到taginfo 就是CONSTANT_METHOD_REF,也就是对于一个类中方法的引用符号。然后我们就去switch里面找,对应了哪几个数据类型。
结果是对应了2个u2类型的数据。也就是2个字节。
然后再取后面两个字节对应的数据,然后展示出来即可。以此类推,循环55次,把常量池读取完。然后分析后面的东西。
读取效果如图:
每一个都是经过解析。最后可视化展示到ui界面上来,界面是用fx做的,是女朋友帮我写的(因为我不会fx哈哈哈)。
AccessFlag,读取完成之后,后面两个字节就是accflag主要是为了标识类的类型,是公有还是私有之类的。0021 = 0020 | 0001
标志名 public类型(目前界面上的计算还未完成)
标志名 | 标志值 | 标志含义 | 针对的对像 |
ACC_PUBLIC | 0x0001 | public类型 | 所有类型 |
ACC_FINAL | 0x0010 | final类型 | 类 |
ACC_SUPER | 0x0020 | 使用新的invokespecial语义 | 类和接口 |
ACC_INTERFACE | 0x0200 | 接口类型 | 接口 |
ACC_ABSTRACT | 0x0400 | 抽象类型 | 类和接口 |
ACC_SYNTHETIC | 0x1000 | 该类不由用户代码生成 | 所有类型 |
ACC_ANNOTATION | 0x2000 | 注解类型 | 注解 |
ACC_ENUM | 0x4000 | 枚举类型 | 枚举 |
依次类推依次向后读取,目前软件可视化工作已经完成到了,计算inferface_count阶段,预计下周能够完成剩余字段的解析。
(工具还没完成,暂时不传哈哈哈哈哈)