java字节码反编译_打造一个简单的Java字节码反编译器

简介

本文示范了一种反编译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 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 javaCodeStack=new Stack();

然后依次翻译上述指令,实际上就是模拟指令的行为:

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 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 opStack = new Stack<>(/*codeAttribute.getMaxStack()*/);

List 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("") && 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_SUPER

Constant pool:

#1 = Methodref #6.#34 // java/lang/Object."":()V

#2 = Class #35 // com/mypackage/Test

#3 = String #36 // hello

#4 = Methodref #2.#37 // com/mypackage/Test."":(Ljava/lang/String;)V

#5 = Methodref #2.#38 // com/mypackage/Test.sum:(II)I

#6 = Class #39 // java/lang/Object

#7 = Utf8

#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 // "":()V

#35 = Utf8 com/mypackage/Test

#36 = Utf8 hello

#37 = NameAndType #7:#8 // "":(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."":()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 "":(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."":()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();

}

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值