zoukankan      html  css  js  c++  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 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("
    ");
            }
    
            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."<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();
       }
    }
    
  • 相关阅读:
    mysql 往表中insert的时候如何让主键id按当前表的最大值自动增长?
    visual studio 2013 win7安装笔记
    mysql奇葩之旅
    java JVM常见的四大异常及处理方案
    DDR3_旧版(2):初始化
    DDR3_旧版(1):IP核调取
    【转】AXI_Lite 总线详解
    ZYNQ笔记(7):AXI从口自定义IP封装
    ZYNQ笔记(6):普通自定义IP封装实现PL精准定时中断
    ZYNQ笔记(5):软中断实现核间通信
  • 原文地址:https://www.cnblogs.com/techspace/p/7163879.html
Copyright © 2011-2022 走看看