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工具)

java spc分析软件 jar包 java class分析工具_无符号整数

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文件不符合格式要求就会抛出异常。(开头四个字节)如图所示:

java spc分析软件 jar包 java class分析工具_java spc分析软件 jar包_02

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)具体如图所示

java spc分析软件 jar包 java class分析工具_无符号整数_03

00 为次版本号,0x34 为主版本号

然后紧接着就是常量池:

java spc分析软件 jar包 java class分析工具_数据_04

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里面找,对应了哪几个数据类型。

java spc分析软件 jar包 java class分析工具_java spc分析软件 jar包_05

结果是对应了2个u2类型的数据。也就是2个字节。

然后再取后面两个字节对应的数据,然后展示出来即可。以此类推,循环55次,把常量池读取完。然后分析后面的东西。

读取效果如图:

java spc分析软件 jar包 java class分析工具_无符号整数_06

每一个都是经过解析。最后可视化展示到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

枚举类型

枚举

java spc分析软件 jar包 java class分析工具_java spc分析软件 jar包_07

依次类推依次向后读取,目前软件可视化工作已经完成到了,计算inferface_count阶段,预计下周能够完成剩余字段的解析。

(工具还没完成,暂时不传哈哈哈哈哈)