项目框架搭建
分析class文件结构的工具有很多,可分为两类。一类是基于十六进制的分析工具,如010editer、UE;另一类是可视化的class文件结构分析工具,如开源的classpy[1]。为能让读者更好的理解class文件结构,本书将介绍如何使用java语言编写一个解析class文件的项目,通过该项目实现class文件的解析,并从编写项目的过程中掌握class文件结构。
根据表2-1 class文件结构以及class文件的解析流程设计解析项目的技术架构图,如图2.1所示。
图2.1 class文件解析项目的架构设计
根据技术架构图搭建项目的框架。先定义对应class文件结构中各项的类型,如常量池、字段表、方法表、属性表、U2、U4,再定义各项的解析器,并使用责任链模式完成class文件结构各项的解析工作。框架搭建如图2.2所示。
图2.2 class文件解析项目的整体框架
如图2.2所示,handler包用于存放class文件结构各项的解析器,如魔数解析器、版本号解析器;type包存放对应class文件结构各项的类型;util包存放工具类,如根据类的访问标志的字节表示转为字符串表示的工具类。
根据表2-1 class文件结构创建ClassFile类,ClassFile类如代码清单2-2所示。
代码清单2-2 ClassFile类
public class ClassFile { private U4 magic; // 魔数 private U2 minor_version; // 副版本号 private U2 magor_version; // 主版本号 private U2 constant_pool_count; // 常量池计数器 private CpInfo[] constant_pool; // 常量池 private U2 access_flags; // 访问标志 private U2 this_class; // 类索引 private U2 super_class; // 父类索引 private U2 interfaces_count; // 接口总数 private U2[] interfaces; // 接口数组 private U2 fields_count; // 字段总数 private FieldInfo[] fields; // 字段表 private U2 methods_count; // 方法总数 private MethodInfo[] methods; // 方法表 private U2 attributes_count; // 属性总数 private AttributeInfo[] attributes; // 属性表 }
ClassFile类中的每个字段是按照class文件结构中各项的顺序声明的,其中CpInfo、FieldInfo、MethodInfo、AttributeInfo这几个类目前并未添加任何字段,只是一个空的类,代码如下。
public class CpInfo { } public class FieldInfo { } public class MethodInfo { } public class AttributeInfo { }
U2和U4是基本单位,长度分别为两个字节和四个字节。为了便于理解,我们需要为这两种基本单位创建对应的Java类,这两个类都只需要一个字段,类型为Byte数组,长度在构造方法中控制,要求构造方法必须传入数组每个元素的值。为验证解析结果是否正确,以及解析结果的可读性,还需要为这两个类添加一个byte[]转int的方法,以及byte[]转16进制字符串的方法。如代码清单2-3所示。
代码清单2-3 U2和U4
public class U2 { private byte[] value; public U2(byte b1, byte b2) { value = new byte[]{b1, b2}; } public Integer toInt() { return (value[0] & 0xff) << 8 | (value[1] & 0xff); } public String toHexString() { char[] hexChar = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; StringBuilder hexStr = new StringBuilder(); for (int i = 1; i >= 0; i--) { int v = value[i] & 0xff; while (v > 0) { public class U2 { private byte[] value; public U2(byte b1, byte b2) { value = new byte[]{b1, b2}; } public Integer toInt() { return (value[0] & 0xff) << 8 | (value[1] & 0xff); } public String toHexString() { char[] hexChar = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; StringBuilder hexStr = new StringBuilder(); for (int i = 1; i >= 0; i--) { int v = value[i] & 0xff; while (v > 0) { int c = v % 16; v = v >>> 4; hexStr.insert(0, hexChar[c]); } if (((hexStr.length() & 0x01) == 1)) { hexStr.insert(0, '0'); } } return "0x" + (hexStr.length() == 0 ? "00" : hexStr.toString()); } } public class U4 { private byte[] value; public U4(byte b1, byte b2, byte b3, byte b4) { value = new byte[]{b1, b2, b3, b4}; } public int toInt() { int a = (value[0] & 0xff) << 24; a |= (value[1] & 0xff) << 16; a |= (value[2] & 0xff) << 8; return a | (value[3] & 0xff); } public String toHexString() { char[] hexChar = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; StringBuilder hexStr = new StringBuilder(); for (int i = 3; i >= 0; i--) { int v = value[i] & 0xff; while (v > 0) { int c = v % 16; v = v >>> 4; hexStr.insert(0, hexChar[c]); } if (((hexStr.length() & 0x01) == 1)) { hexStr.insert(0, '0'); } } return "0x" + hexStr.toString(); } }
接着我们需要创建一个接口BaseByteCodeHandler,抽象出class文件结构各项的解析器行为。每个解析器应该只负责完成class文件结构中某一项的解析工作,如常量池解析器就只负责解析常量池。BaseByteCodeHandler接口如代码清单2-4所示。
代码清单2-4 class文件结构各项的解释器接口
public interface BaseByteCodeHandler { /** * 解释器的排序值 * @return */ int order(); /** * 读取 * @param codeBuf * @param classFile */ void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception; }
如代码清单2-4所示,BaseByteCodeHandler接口只定义了一个read方法,该方法要求传入class文件的字节缓存[2]和ClassFile对象。read方法将从字节缓存中读取相应的字节数据写入ClassFile对象。由于解析是按顺序解析的,因此BaseByteCodeHandler接口还定义了一个返回排序值的方法,用于实现解析器排序,比如版本号解析器排在魔数解析器的后面。
有了解析器之后,我们还需要实现一个管理和调度解析器工作的总指挥ClassFileAnalysiser,如代码清单2-5所示。
代码清单2-5 ClassFileAnalysiser类
public class ClassFileAnalysiser { private final static List<BaseByteCodeHandler> handlers = new ArrayList<>(); static { // 添加各项的解析器 handlers.add(new MagicHandler()); handlers.add(new VersionHandler()); ...... // 解析器排序,要按顺序调用 handlers.sort((Comparator.comparingInt(BaseByteCodeHandler::order))); } // 将传入的从class文件读取的字节缓存,解析生成一个ClassFile对象 public static ClassFile analysis(ByteBuffer codeBuf) throws Exception { // 重置ByteBuffer的读指针,从头开始 codeBuf.position(0); ClassFile classFile = new ClassFile(); // 遍历解析器,调用每个解析器的解析方法 for (BaseByteCodeHandler handler : handlers) { handler.read(codeBuf, classFile); } return classFile; } }
ClassFileAnalysiser的静态代码块负责实例化各个解释器并排好序。ClassFileAnalysiser暴露analysis方法给外部调用,由analysis方法根据解析器的排序顺序去调用各个解析器的read方法完成class文件结构各项的解析工作,由各项解析器将解析结果赋值给ClassFile对象的对应字段。
analysis方法的入参是class文件内容的字节缓存,从class文件中读取而来。使用ByteBuffer而不直接使用byte[]缓存读取的class文件内容是因为ByteBuffer能更好的控制顺序读取。
现在我们只需要实现将class文件读取到内存中,再调用ClassFileAnalysiser的analysis方法,就能实现将一个class文件解析为一个ClassFile对象了,如代码清单2-6所示。
代码清单2-6 将class文件读取到ByteBuffer并解析
public class ClassFileAnalysisMain { public static ByteBuffer readFile(String classFilePath) throws Exception { File file = new File(classFilePath); if (!file.exists()) { throw new Exception("file not exists!"); } byte[] byteCodeBuf = new byte[4096]; int lenght; try (InputStream in = new FileInputStream(file)) { lenght = in.read(byteCodeBuf); } if (lenght < 1) { throw new Exception("not read byte code."); } // 将字节数组包装为ByteBuffer return ByteBuffer.wrap(byteCodeBuf, 0, lenght).asReadOnlyBuffer(); } public static void main(String[] args) throws Exception { // 读取class文件 ByteBuffer codeBuf = readFile("xxx.class"); // 解析class文件 ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf); // 打印魔数解析器解析出来的Magic System.out.println(classFile.getMagic().toHexString()); } }
当然,这只是整体的框架搭建,class文件结构各项的解释器还没有实现。接下来,我们就按照class文件结构的解析顺序实现各项解析器。
注释:
[1] classpy是一个开源的class文件结构分析工具:https://github.com/zxh0/classpy
[2] class文件字节缓存是指从class文件读入内存的字节缓存,这是一个数组,大小即为class文件的大小。