美文网首页
java 字节码

java 字节码

作者: 古都旧城 | 来源:发表于2020-10-15 10:46 被阅读0次

    一、基本介绍

    1.1、java的平台无关性

    image.png
    • JAVA源代码->Class字节码->JVM解
      释执⾏(依赖于不同的jvm实现跨平台)
    • Java 虚拟机(JVM):负责将字节码⽂
      件翻译成特定平台下的机器码然后运
      ⾏。
    • 不同的平台有不同的JVM实现

    1.2、字节码的语言无关性

    字节码(ByteCode)是构成平台无关性的基石,这也决定了,只要是能转换成字节码,其他语言也可以运行在jvm之上,所以现在不只是java,还有kotlin、groovy等都可以运行在jvm之上。


    image.png

    所以把JVM(Java Virtual Machine Java虚拟机)叫CVM(Class Virtual Machine Class虚拟机)反而更合适一些.

    1.3、Class文件格式

    Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中。

    1.3.1、字节码的两种基本数据类型

    Class文件结构包含两种数据类型:


    image.png
    • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
    • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表

    1.3.2、Class文件表构成

    Class文件格式

    1.3.3、编译测试

    编译下面这个类

    public class JavaCodeTest {}
    

    然后把得到的.class用16进制编辑器直接打开字节码显示是这样的:


    可以看到开头的四个字节是CAFEBABE,后面的字节可以意思对应上方表格进行查看,都是严格按照顺序一一对应的。

    1.4、常用属性介绍

    在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。任何人实现编译器都可以向属性表中写入自己自定义的属性信息,但是java虚拟机运行时会忽略掉他不认识的属性。
    《Java虚拟机规范(Java SE 7)》版中,预定义属性已经增加到21项,如下表所示:


    虚拟机规范预定义的属性

    1.4.1、Code属性

    Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。
    Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。了解Code属性是学习关于字节码执行引擎内容的必要基础,能直接阅读字节码也是工作中分析Java代码语义问题的必要工具和基本技能。
    比如这个类

    public class JavaCodeTest {
        private int a;
    
        public int testAdd() {
            return a + 1;
        }
    }
    

    编译后的方法部分如下所示(下面这种为转义过的,未转义的都如上面介绍的那样,为16进制形式)
    如下所示,具体每一个字段的释义可以参考注释。

    {
    //---略
      public com.canzhang.asmdemo.test.JavaCodeTest();
        //descriptor 对方法参数和返回值进行描述
        descriptor: ()V//`()`表示无参数,`V`表示Void,无返回值
        flags: ACC_PUBLIC//方法修饰符,表示是public的,可以有多个。
        Code://方法体
          stack=1, locals=1, args_size=1//参数个数
              //aload_0指令表示:将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
             0: aload_0
              //invokespecial指令表示:以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法
             1: invokespecial #1  // 这里的`#1`是invokespecial指令的参数,表示指向常量池声明的常量:Method java/lang/Object."<init>":()V
             //指令return,含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束。
             4: return
          //LineNumberTable 是用来描述Java源码行号与字节码行号(字节码偏移量)之间的对应关系,可以配置不生成,不生成就无法获取异常发生源码行号,也无法按照源码的行数来调试程序。
          LineNumberTable:
            line 6: 0
    
      public int testAdd();
        descriptor: ()I
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: aload_0
             1: getfield      #2                  // Field a:I
             4: iconst_1
             5: iadd
             6: ireturn
          LineNumberTable:
            line 10: 0
    }
    
    //---略
    

    疑问点:

    • 第一个方法<init>()是什么?
      java在编译之后会在字节码文件中生成<init>方法,称之为实例构造器
    • 实例构造器<init>()和testAdd(),这两个方法很明显都是没有参数的,为什么Args_size会为1?而且无论是在参数列表里还是方法体内,都没有定义任何局部变量,那Locals又为什么会等于1?

    在任何实例方法里面,都可以通过“this”关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现却非常简单,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。这个处理只对实例方法有效,如果testAdd()方法声明为static,那Args_size就不会等于1而是等于0了。

    附录:

    生成字节码样例参考

    java文件

    public class JavaCodeTest {
        private int a;
    
        public int testAdd() {
            return a + 1;
        }
    }
    
    
    //生成字节码
     javac /Users/canzhang/AndroidStudioProjects/ASMDemo/app/src/main/java/com/canzhang/asmdemo/test/JavaCodeTest.java 
    //反编译字节码
     javap -v  /Users/canzhang/AndroidStudioProjects/ASMDemo/app/src/main/java/com/canzhang/asmdemo/test/JavaCodeTest.class
    

    反编译结果

    Classfile /Users/canzhang/AndroidStudioProjects/ASMDemo/app/src/main/java/com/canzhang/asmdemo/test/JavaCodeTest.class
      Last modified 2020-11-5; size 311 bytes
      MD5 checksum 985828dc144886121bd06e4d19423d65
      Compiled from "JavaCodeTest.java"
    public class com.canzhang.asmdemo.test.JavaCodeTest
      minor version: 0//jdk次版本号
      major version: 52//jdk主版本号
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool://常量池
       #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
       #2 = Fieldref           #3.#16         // com/canzhang/asmdemo/test/JavaCodeTest.a:I
       #3 = Class              #17            // com/canzhang/asmdemo/test/JavaCodeTest
       #4 = Class              #18            // java/lang/Object
       #5 = Utf8               a
       #6 = Utf8               I
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code//属性的名称 对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示
      #10 = Utf8               LineNumberTable//属性的名称
      #11 = Utf8               testAdd
      #12 = Utf8               ()I
      #13 = Utf8               SourceFile
      #14 = Utf8               JavaCodeTest.java
      #15 = NameAndType        #7:#8          // "<init>":()V
      #16 = NameAndType        #5:#6          // a:I
      #17 = Utf8               com/canzhang/asmdemo/test/JavaCodeTest
      #18 = Utf8               java/lang/Object
    {
      public com.canzhang.asmdemo.test.JavaCodeTest();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code://方法体
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 6: 0
    
      public int testAdd();
        descriptor: ()I
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: aload_0
             1: getfield      #2                  // Field a:I
             4: iconst_1
             5: iadd
             6: ireturn
          LineNumberTable:
            line 10: 0
    }
    
    

    常用的几个指令

    • .java--->.class: javac /xxxx/JavaCodeTest.java
    • .class 转换字节码内容:javap -c /xxxx/JavaCodeTest.class
    用法: javap <options> <classes>
    其中, 可能的选项包括:
      -help  --help  -?        输出此用法消息
      -version                 版本信息
      -v  -verbose             输出附加信息
      -l                       输出行号和本地变量表
      -public                  仅显示公共类和成员
      -protected               显示受保护的/公共类和成员
      -package                 显示程序包/受保护的/公共类
                               和成员 (默认)
      -p  -private             显示所有类和成员
      -c                       对代码进行反汇编
      -s                       输出内部类型签名
      -sysinfo                 显示正在处理的类的
                               系统信息 (路径, 大小, 日期, MD5 散列)
      -constants               显示最终常量
      -classpath <path>        指定查找用户类文件的位置
      -cp <path>               指定查找用户类文件的位置
      -bootclasspath <path>    覆盖引导类文件的位置
    

    对应android javac,并不能保证所有都正常编译,因为有很多android sdk的内容是识别不了的,另外android编译过程中也会有自己一些额外处理(比如脱糖、插桩一类的),所以最好直接使用android工程编译后的产物也进行字节码分析,一般工程如下图目录已经存在了编译后的.class文件,我们可以直接取用.class 使用javap转换成可以读懂的文字内容就可以分析了。
    具体目录:app/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/xxx

    image.png

    android studio 便捷命令配置

    为了方便我们使用javap,可以在android studio 配置tool,方便我们使用:


    配置引导

    如图示 依次打开 tools-external tools -点击左下角的添加按钮,按照箭头输入几个关键项就可以了:

    • Name:随便填写
    • Program:$JDKPath$\bin\javap
    • Arguments:-c $FileClass$
    • Working directory:$OutputPath$
      然后点击 ok apply 就可以使用了,使用方法,如下图所示:
      image.png

    注意事项:

    • 使用的时候默认获取的是当前打开的类作为输入入参,比如你想看MainActivity.java对应的字节码文件,那么就打开MainActivity.java就可以了
    • 需要工程编译后才能按照配置的路径,找到对应的.class文件。
    • 如果编译了,依然提示文件不存在,上面的配置项可以尝试清空,使用右侧的 insert 去插入对应路径,上面的几个路径,都是有选项可选的,按照名字选择即可。

    字节码查看器

    Hex Fiend
    下载后直接双击打开,就可以看到对应的字节码,选中还可以高亮对应。

    image.png
    未完待续.....

    参考文章

    kotilin中的某些特性 :https://juejin.im/post/6844903588716609543
    反射原理

    相关文章

      网友评论

          本文标题:java 字节码

          本文链接:https://www.haomeiwen.com/subject/lkenpktx.html