打造一个简单的Java字节码反编译器
来源:互联网 发布:什么是办公室软件 编辑:程序博客网 时间:2024/05/17 22:21
本文示范了一种反编译Java字节码的方法,首先通过解析class文件,然后将解析的结果转成java代码。但是本文并没有覆盖所有的class文件的特性和指令,只针对部分规范进行解析。
所有的代码代码都是示范性的,追求功能实现,没有太多的软件工程方面的考量。
Class文件格式
一个Java类或者接口被javac编译后会生成一个class文件,class文件可以用下面代码来描述,u2,u4分表表示2个字节的无符号数和4个字节的无符号数。
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
- magic是固定值0xCAFEBABE
- minor_version和major_version分别代表副版本号和主版本号。
- constant_pool_count表示接下来常量池中包含的常量项数量。
- constant_pool表示常量池,常量池中包含了各种不同类型的常量池项,如:字符串常量,类或接口名,方法引用等,每个常量池项的第一个字节表示tag,在解析常量池时,需要先读取tag,然后根据不同的tag类型继续往后面读取固定字节的数据。每个常量池项都有一个编号,外部可以使用这个编号来访问常量池项。
- access_flags表示类或者接口标志,如PUBLIC,FINAL等。
- this_class指向常量池的一个索引号,最终解析出来的是一个类或者接口的名称。
- super_class指向父类,jvm只支持单继承。
- interfaces_count,interfaces分别表示实现的接口数和实现的接口。
- fields_count,fields表示一个类的域。
- methods_count,methods表示一个类或接口包含的方法。
- attributes_count,attributes表示对类的属性。
用Java解析Class文件
本节定义一系列数据结构用来将二进制class数据用java代码来描述。并简述一些基本概念,由于class文件定义项非常多,如果要详细了解,请查看《ava虚拟机规范》 [https://docs.oracle.com/javase/specs/jvms/se8/html/]。
ClassFile
public class ClassFile { private int magic; private int minorVersion; private int majorVersion; private ConstantPool constantPool; private AccessFlags accessFlags; private int thisClass; private int superClass; private int interfacesCount; private int interfaces[]; private int fieldsCount; private FieldInfo fields[]; private int methodsCount; private MethodInfo methods[]; private int attributesCount; private Attribute attributes[];}
ConstantPool
常量池中包含了类的所有符号信息,包括类名,方法名,常量等。常量池项包含了多种类型,每项使用一个tag来识别是哪个常量。定义基类如下:
public abstract class CPInfo { protected ConstantPool constantPool;}
具体常量池定义如下:
public class ConstantUtf8Info extends CPInfo { private String value;}public class ConstantStringInfo extends CPInfo { private int stringIndex;}public class ConstantClassInfo extends CPInfo { private int nameIndex;}
在解析常量池时,需要先读取一个字节的tag来判断这个常量池项是什么类型,然后按类型来读取接下来的数据,因为每个不同类型的项所包含的数据是不定长的,所以这里显然是需要一个大大的switch了。
由于常量池类型多达10几种,这里不一一列出。具体参考《Java虚拟机规范》。定义一个ConstantPool类来简化对常量池的操作,这个类包含了常量池项的数量和常量池项的数组。
public class ConstantPool { private int poolCount; private CPInfo[] pool; public ConstantPool(DataInputStream dataInputStream) throws IOException { this.poolCount = dataInputStream.readUnsignedShort(); this.pool = new CPInfo[this.poolCount]; //注意,从下表为1开始访问常量池 for (int i = 1; i < this.poolCount; i++) { int tag = dataInputStream.readUnsignedByte(); this.pool[i] = CPInfoFactory.getInstance().createCPInfo(tag, dataInputStream, this); } } public int getPoolCount() { return poolCount; } public <T extends CPInfo> T getCPInfo(int index) { return (T) pool[index]; } public ConstantUtf8Info getUtf8Info(int index) { return (ConstantUtf8Info) pool[index]; }}
FieldInfo
FieldInfo
用来描述类里的Field,定义如下:
class FieldInfo { private int accessFlags; //修饰符号 private int nameIndex; //field名称常量在常量池中的索引 private int descriptorIndex; private int attributesCount; private Attribute attributeInfo[];}
MethodInfo
用于描述类中方法的数据结构,methodInfo里面包含了一系列的attribute,方法的实际字节码指令就放在CodeAttribute里面。
class MethodInfo { private AccessFlags accessFlags; private int nameIndex; private int descriptorIndex; private int attributesCount; private Attribute attributes[];}
Attribute
在ClassFile,FieldInfo,MethodInfo里面都定义了一个Attribute数组,Attribute类型也不少,本文只关注MethodInfo里面的CodeAttribute类型。这个类型包含了一个方法的操作数栈大小,本地变量表大小,指令码:
public class CodeAttribute extends Attribute { private int maxStack; private int maxLocals; private int codeLength; private byte code[]; private int exceptionTableLength; private ExceptionData exceptionTable[]; private int attributeCount; private Attribute attributes[];}
Descriptor
Descriptor
是一个字符串,可以用来描述一个方法的参数和返回类型。如下:
(Ljava/lang/Object;[Ljava/lang/Object;)I(II)I
括号中表述参数,括号外表示返回类型。这个Descriptor可以解析成 :
int XXX(Object,Object[]);int XXX(int,int);
L表示引用类型,I表示int类型,[表示数组,具体对应如下:
char2TypeMap.put('B', "byte");char2TypeMap.put('C', "char");char2TypeMap.put('D', "double");char2TypeMap.put('F', "float");char2TypeMap.put('I', "int");char2TypeMap.put('J', "long");char2TypeMap.put('S', "short");char2TypeMap.put('Z', "boolean");
class文件的解析不复杂,但是比较繁琐,本文不全部列出,更多class文件的定义还是要参考《Java虚拟机规范》。
将ClassFile解析成Java代码
ClassFile对象解析出来后,可以开始生成Java代码了。首先构造class的头部:
//生成class头部:public class Test extends Base private String generateClassHead() { StringBuilder javaCode = new StringBuilder(); if (classFile.getAccessFlags().hasFlag(AccessFlags.ACC_PUBLIC)) { javaCode.append("public "); } else if (classFile.getAccessFlags().hasFlag(AccessFlags.ACC_PRIVATE)) { javaCode.append("private "); } else { javaCode.append("protected "); } if (classFile.getAccessFlags().hasFlag(AccessFlags.ACC_INTERFACE)) { javaCode.append("interface "); } else { javaCode.append("class "); } javaCode.append(this.className).append(" "); //解析实现的接口名 if (classFile.getInterfaces().length > 0) { javaCode.append(" implements "); } for (int i = 0; i < classFile.getInterfaces().length; i++) { ConstantClassInfo interfaceClassInfo = classFile.getConstantPool().getCPInfo(classFile.getInterfaces()[i]); javaCode.append(interfaceClassInfo.getName()); boolean isLast = i == (classFile.getInterfaces().length - 1); if (!isLast) { javaCode.append(","); } } return javaCode.toString(); }
class的头部代码构造比较简单,最复杂的在于class body部分,本文只实现了MethodInfo的解析,也就是只生成方法,在构造class body之前,要先看一下,如果为MethodInfo生成代码,
首先解析方法头部,方法头部解析比较简单,就是拼凑方法modifiers,方法名称,方法参数,返回等,方法的头部的解析和类解析的头部有一定的相同点,主要区别就是方法头部解析需要根据Descriptor来解析方法的返回类型,方法的参数列表。并根据ExceptionsAttribute来解析这个方法可能抛出的异常(本文不考虑解析Excpetion),比较简单,这里不贴代码了。
接下来解析方法body,方法body里面包含了具体的指令码。需要解析CodeAttribute,只有方法才包含CodeAttribute,CodeAttribute里才包含字节码指令,下面是一个方法用字节码表示的示范:
public int sum(int, int); descriptor: (II)I flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=3 0: iload_1 1: iload_2 2: iadd 3: ireturn LineNumberTable: line 13: 0 LocalVariableTable: Start Length Slot Name Signature 0 4 0 this Lcom/mypackage/Test; 0 4 1 i I 0 4 2 j I
Code节点就是CodeAttribute,里面包含了stack,locals,args_size,以及几条指令。stack表示执行这个方法所需要的栈大小,这个值在编译时已经确定了,locals表示本地变量表的大小,args_size表示方法参数的数量,由于这个方法是个实例方法,所以第一个传进来的参数是实例自身,也就是this,然后才是方法的两个参数,所以参数为3。
JVM用线程来执行方法,每个线程都包含一个线程栈,线程栈存放的是栈帧,每个栈帧都有自己的操作数栈和本地变量表。当一个方法被执行时候,首先会在栈顶push一个栈帧,栈帧的创建就需要指定操作数栈和本地变量表的大小。接下来方法的参数会被传入一个方法的本地变量表,本地变量表的访问采用下表索引的方式来访问,第0个位置会传入this,第1个位置会传入int,第2个位置会传入int。
在栈帧完全准备好后,就可以开始执行执行字节码指令了,上述iload_1,i_load_2分别将本地变量表的1,2位置的数据push到操作数栈中,iadd随后会pop两个值用来做加法操作,并将结果push到操作数栈,最后ireturn将栈顶数据返回。
在理解字节码的执行方式后,可以开始将字节码一条一条的转化成java代码。这里依然需要借助stack。可以用一个stack来暂时存储转换后的java代码,还是用上面代码做示范:
首先声明一个stack:
Stack<String> javaCodeStack=new Stack<String>();
然后依次翻译上述指令,实际上就是模拟指令的行为:
iload_1 -> javaCodeStack.push("var1"); iload_2 -> javaCodeStack.push("var2");iadd -> javaCodeStack.push(javaCodeStack.pop() + "+" javaCodeStack.pop());ireturn -> javaCodeStack.push("return "+javaCodeStack.pop());
指令执行完毕后,java代码也就翻译完成了,所有有效的java代码都已经放在javaCodeStack里,接下来就是遍历一下javaCodeStack,把里面的字符串打印出来就行了,遍历后得到的结果只有一行java代码,如下:
return var2+var1;
上面只阐述了几种简单指令的翻译方式,但是即使对于复杂的指令,也只需要按照上面的方式来做转换就可以得到java代码。下面代码实现了更多的指令翻译,基本流程就是先得到CodeAttribute,然后准备好操作数栈和本地变量表,接下来就是每次读取一个指令,然后根据指令类型在读取指令的参数(一般是访问常量池的索引号),最后将压入栈的java代码拼接成一个字符串,得到的就是方法体的代码了。
private String generateMethodBodyCode(MethodInfo methodInfo, List<TypeDescriptor> parametersTypeDescriptors, TypeDescriptor returnTypeDescriptor) throws IOException { StringBuilder javaCode = new StringBuilder(); String currentMethodName = classFile.getConstantPool().getUtf8Info(methodInfo.getNameIndex()).getValue(); //寻找CodeAttribute CodeAttribute codeAttribute = findAttribute(methodInfo, Attribute.Code); if (codeAttribute == null) { throw new RuntimeException("无法在Method里找到CodeAttribute"); } Stack<Object> opStack = new Stack<>(/*codeAttribute.getMaxStack()*/); List<String> localVariableNames = new ArrayList<>(codeAttribute.getMaxLocals()); //初始化本地变量表名,首先如果是实例方法,需要把this放入第一个,然后依次将方法参数名放入 boolean isStaticMethod = methodInfo.getAccessFlags().hasFlag(AccessFlags.ACC_STATIC); if (!isStaticMethod) { localVariableNames.add("this"); } for (int x = 0; x < parametersTypeDescriptors.size(); x++) { localVariableNames.add("var" + (x + 1)); } DataInputStream byteCodeInputStream = new DataInputStream(new ByteArrayInputStream(codeAttribute.getCode())); while (byteCodeInputStream.available() > 0) { int opCode = byteCodeInputStream.readByte() & 0xff; switch (opCode) { case OP_aload_0: System.out.println("aload_0"); opStack.push(localVariableNames.get(0)); break; case OP_invokevirtual: { int methodRefIndex = byteCodeInputStream.readUnsignedShort(); System.out.println("invokevirtual #" + methodRefIndex); ConstantMethodRefInfo methodRefInfo = classFile.getConstantPool().getCPInfo(methodRefIndex); ConstantNameAndTypeInfo nameAndTypeInfo = classFile.getConstantPool().getCPInfo(methodRefInfo.getNameAndTypeIndex()); String methodName = classFile.getConstantPool().getUtf8Info(nameAndTypeInfo.getNameIndex()).getValue(); String typeDescriptor = classFile.getConstantPool().getUtf8Info(nameAndTypeInfo.getDescriptorIndex()).getValue(); int methodParameterSize = new DescriptorParser(typeDescriptor).getParameterTypeDescriptors().size(); Object targetClassName; Object parameterNames[] = new Object[methodParameterSize]; for (int x = 0; x < methodParameterSize; x++) { parameterNames[methodParameterSize - x - 1] = opStack.pop(); } targetClassName = opStack.pop(); StringBuilder line = new StringBuilder(); line.append(targetClassName).append(".").append(methodName).append("("); for (int x = 0; x < methodParameterSize; x++) { line.append(parameterNames[x]); if ((x != methodParameterSize - 1)) { line.append(","); } } line.append(");"); opStack.push(line.toString()); break; } case OP_invokespecial: { int methodRefIndex = byteCodeInputStream.readUnsignedShort(); System.out.println("invokespecial #" + methodRefIndex); ConstantMethodRefInfo methodRefInfo = classFile.getConstantPool().getCPInfo(methodRefIndex); ConstantNameAndTypeInfo nameAndTypeInfo = classFile.getConstantPool().getCPInfo(methodRefInfo.getNameAndTypeIndex()); String typeDescriptor = classFile.getConstantPool().getUtf8Info(nameAndTypeInfo.getDescriptorIndex()).getValue(); int methodParameterSize = new DescriptorParser(typeDescriptor).getParameterTypeDescriptors().size(); Object targetClassName; Object parameterNames[] = new Object[methodParameterSize]; if (methodParameterSize > 0) { for (int x = 0; x < methodParameterSize; x++) { parameterNames[methodParameterSize - x - 1] = opStack.pop(); } } targetClassName = opStack.pop(); StringBuilder line = new StringBuilder(); if (currentMethodName.equals("<init>") && targetClassName.equals("this")) { line.append("super"); } else { line.append("new ").append(targetClassName); } line.append("("); for (int x = 0; x < methodParameterSize; x++) { line.append(parameterNames[x]); if ((x != methodParameterSize - 1)) { line.append(","); } } line.append(");"); opStack.push(line.toString()); break; } case OP_getstatic: System.out.println("getstatic"); break; case OP_return: System.out.println("return"); break; case OP_new: { int classIndex = byteCodeInputStream.readUnsignedShort(); System.out.println("new #" + classIndex); ConstantClassInfo classInfo = classFile.getConstantPool().getCPInfo(classIndex); opStack.push(classInfo.getName()); break; } case OP_dup: System.out.println("dup"); Object top = opStack.pop(); opStack.push(top); opStack.push(top); break; case OP_ldc: int stringIndex = byteCodeInputStream.readByte() & 0xff; System.out.println("ldc #" + stringIndex); ConstantStringInfo stringInfo = classFile.getConstantPool().getCPInfo(stringIndex); String value = classFile.getConstantPool().getUtf8Info(stringInfo.getStringIndex()).getValue(); opStack.push(value); break; case OP_iload_1: System.out.println("iload_1"); opStack.push(localVariableNames.get(1)); break; case OP_iload_2: System.out.println("iload_2"); opStack.push(localVariableNames.get(2)); break; case OP_iadd: System.out.println("iadd"); opStack.push(opStack.pop() + "+" + opStack.pop()); break; case OP_ireturn: System.out.println("ireturn"); opStack.push("return " + opStack.pop()); break; case OP_iconst_0: System.out.println("iconst_0"); opStack.push("0"); break; case OP_iconst_1: System.out.println("iconst_1"); opStack.push("1"); break; case OP_iconst_2: System.out.println("iconst_2"); opStack.push("2"); break; case OP_astore_1: { System.out.println("astore_1"); String obj = opStack.pop().toString(); String className = opStack.pop().toString(); localVariableNames.add(1, "localVar1"); opStack.push(className + " localVar1=" + obj); break; } case OP_astore_2: { System.out.println("astore_2"); String obj = opStack.pop().toString(); String className = opStack.pop().toString(); localVariableNames.add(1, "localVar2"); opStack.push(className + " localVar2=" + obj); break; } case OP_astore_3: { System.out.println("astore_3"); String obj = opStack.pop().toString(); String className = opStack.pop().toString(); localVariableNames.add(1, "localVar3"); opStack.push(className + " localVar3=" + obj); break; } case OP_aload_1: System.out.println("aload_1"); opStack.push(localVariableNames.get(1)); break; case OP_pop: System.out.println("pop"); //opStack.pop(); break; default: throw new RuntimeException("Unknow opCode:0x" + opCode + " " + currentMethodName); } } for (Object s : opStack) { javaCode.append(" ").append(s).append("\r\n"); } return javaCode.toString(); }
最后就是组装一个class了,将类头部,方法头部,方法body,全部拼接后,就是最终的java代码了。
总结
本文涉及的代码只实现了class规范的一部分,并不能反编译所有的class文件(需要补全未识别的指令),下面的字节码通过了测试:
public class com.mypackage.Test minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Methodref #6.#34 // java/lang/Object."<init>":()V #2 = Class #35 // com/mypackage/Test #3 = String #36 // hello #4 = Methodref #2.#37 // com/mypackage/Test."<init>":(Ljava/lang/String;)V #5 = Methodref #2.#38 // com/mypackage/Test.sum:(II)I #6 = Class #39 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 (Ljava/lang/String;)V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/mypackage/Test; #14 = Utf8 s #15 = Utf8 Ljava/lang/String; #16 = Utf8 sum #17 = Utf8 (II)I #18 = Utf8 i #19 = Utf8 I #20 = Utf8 j #21 = Utf8 search #22 = Utf8 (Ljava/lang/Object;[Ljava/lang/Object;)I #23 = Utf8 o #24 = Utf8 Ljava/lang/Object; #25 = Utf8 objects #26 = Utf8 [Ljava/lang/Object; #27 = Utf8 main #28 = Utf8 ([Ljava/lang/String;)V #29 = Utf8 args #30 = Utf8 [Ljava/lang/String; #31 = Utf8 test #32 = Utf8 SourceFile #33 = Utf8 Test.java #34 = NameAndType #7:#40 // "<init>":()V #35 = Utf8 com/mypackage/Test #36 = Utf8 hello #37 = NameAndType #7:#8 // "<init>":(Ljava/lang/String;)V #38 = NameAndType #16:#17 // sum:(II)I #39 = Utf8 java/lang/Object #40 = Utf8 ()V{ public com.mypackage.Test(java.lang.String); descriptor: (Ljava/lang/String;)V flags: ACC_PUBLIC Code: stack=1, locals=2, args_size=2 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 line 10: 4 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/mypackage/Test; 0 5 1 s Ljava/lang/String; public int sum(int, int); descriptor: (II)I flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=3 0: iload_1 1: iload_2 2: iadd 3: ireturn LineNumberTable: line 13: 0 LocalVariableTable: Start Length Slot Name Signature 0 4 0 this Lcom/mypackage/Test; 0 4 1 i I 0 4 2 j I public int search(java.lang.Object, java.lang.Object[]); descriptor: (Ljava/lang/Object;[Ljava/lang/Object;)I flags: ACC_PUBLIC Code: stack=1, locals=3, args_size=3 0: iconst_0 1: ireturn LineNumberTable: line 17: 0 LocalVariableTable: Start Length Slot Name Signature 0 2 0 this Lcom/mypackage/Test; 0 2 1 o Ljava/lang/Object; 0 2 2 objects [Ljava/lang/Object; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #2 // class com/mypackage/Test 3: dup 4: ldc #3 // String hello 6: invokespecial #4 // Method "<init>":(Ljava/lang/String;)V 9: astore_1 10: aload_1 11: iconst_1 12: iconst_2 13: invokevirtual #5 // Method sum:(II)I 16: pop 17: new #6 // class java/lang/Object 20: dup 21: invokespecial #1 // Method java/lang/Object."<init>":()V 24: astore_2 25: return LineNumberTable: line 21: 0 line 22: 10 line 23: 17 line 24: 25 LocalVariableTable: Start Length Slot Name Signature 0 26 0 args [Ljava/lang/String; 10 16 1 test Lcom/mypackage/Test; 25 1 2 o Ljava/lang/Object;}SourceFile: "Test.java"
用这个简单的反编译器来执行反编译的结果如下:
public class Test { public Test(java.lang.String var1) { super(); } public int sum(int var1, int var2) { return var2+var1 } public int search(java.lang.Object var1, java.lang.Object[] var2) { return 0 } public static void main(java.lang.String[] var1) { com.mypackage.Test localVar1=new com.mypackage.Test(hello); localVar1.sum(1,2); java.lang.Object localVar2=new java.lang.Object(); }}
- 打造一个简单的Java字节码反编译器
- 打造一个简单的Java字节码反编译器
- 一个很好的java反编译器
- java的反编译器
- java的反编译器
- Java反编译器的问题
- 用Java编写一个简单的编译器
- java反编译器JAD.exe的使用
- java反编译器
- Java反编译器比较
- Java反编译器剖析
- 反编译器 java
- Java反编译器JD
- Java反编译器
- Java反编译器JD
- 实现一个简单的编译器
- 用Java做一个简单的basic编译器
- 打造自己的编译器
- Vue 使用Spread.js没有层级关系(隐藏与显示)
- vim 的使用方法
- android jni 中文乱码,该怎么解决
- Linux上Weblogic服务域的创建与部署
- ApplicationId和包名啥关系
- 打造一个简单的Java字节码反编译器
- CAFFE+1080Ti+CUDA8.0+CUDNN8.0+OPENCV3.0+MATLAB安装教程
- Git详解之一 Git起步
- 什么是知识图谱
- springMVC导出excel
- visual studio 实时调试器问题
- 详解 Spring 3.0 基于 Annotation 的依赖注入实现
- Linux下模拟多线程的并发并发shell脚本
- cookie---照片浏览历史显示