如何看懂字节码文件

如何看懂字节码文件

测试使用的类


public class Test {

    public int i;
    public long y;

}

基础概念

每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。不仅是Class文件,很多文件格式标准中都有使用魔数来进行身份识别的习惯,譬如图片格式,如GIF或者JPEG等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以随意改动。

紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,因为《Java虚拟机规范》在Class文件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

这里使用nodepad++安装HEX插件【可以用任意具有HEX查看功能的工具】打开class文件观察数据

image-20240522163534770

首先是魔数cafe babe,代表次版本号的第5个和第6个字节值为0x0000,而主版本号的值为0x003d,也即是十进制的61,即JDK17编译的class文件。

常量池

紧接着主、次版本号之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它还是在Class文件中第一个出现的表类型数据项目。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:·被模块导出或者开放的包(Package)·类和接口的全限定名(Fully Qualified Name)·字段的名称和描述符(Descriptor)·方法的名称和描述符·方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)·动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)。

常量池中的每一项常量都是一个表,在 JDK 7 之前共有 11 种结构不同的表,在 JDK 7 中为了更好的支持动态语言调用,又增加了 3 种。都是CONSTANT 开头,info 结尾。我们以下表为基础来解析class文件。

img

其中 CONSTANT_Utf8_info 类型的常量,它的 length 值说明了这个 UTF-8 编码的字符串长度是多少字节,bytes 的值为长度为 length 字节的 UTF-8 缩略编码表示的字符串

由于 Class 文件中方法、字段等都需要引用 CONSTANT_Utf8_info 型常量来描述名称,所以 CONSTANT_Utf8_info 型常量的最大长度也就是 Java 中方法、字段名的最大长度,即 u2 的 65535,也就是说最大 65535 字节,即 64KB。

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不同,这个容量计数是从1而不是0开始的,本例图中第8,9位便是我们常量池的大小。(偏移地址:0x00000008)为十六进制数0x0014,即十进制的20,这就代表常量池中有19项常量,索引值范围为1~19;

表结构起始的第一位是个u1类型的标志位,代表着当前常量属于哪种常量类型,即图中a位置;这里就从图中a开始解读:

常量池的第一项常量,它的标志位(偏移地址:0x0000000A)是0x0a,查表获取类型如下图,此处是CONSTANT_Methodref_info类型的tag,得知tag后跟2个u2类型的index索引,分别是(偏移地址:0x0000000C0x02和(偏移地址:0x0000000E0x03[这两个索引具体是什么我们在下面解析]。

image-20240522170218264

第一个常量池数据结束,目前得到的常量池:

[01] CONSTANT_Methodref_info #2,#3

接着是第二个常量池:它的标志位(偏移地址:0x0000000F)是0x07,依然是查类型表07;此处是CONSTANT_Class_info类型的tag

后跟1个u2类型的index索引(偏移地址:0x0000000C0x04

image-20240522170745861

第二个常量池数据结束,目前得到的常量池:

[01] CONSTANT_Methodref_info #2,#3
[02] CONSTANT_Class_info #4

第三个常量池:它的标志位(偏移地址:0x00000012)是0x0c,依然是查类型表12;此处是CONSTANT_NameAndType类型的tag,后跟2个u2类型的index索引,分别是(偏移地址:0x000000140x05和(偏移地址:0x000000160x06image-20240522171544729

第三个常量池数据结束,目前得到的常量池:

[01] CONSTANT_Methodref_info #2,#3
[02] CONSTANT_Class_info #4
[03] CONSTANT_NameAndType #5,#6

第四个常量池:它的标志位(偏移地址:0x00000017)是0x01,查类型表01;此处是CONSTANT_Utf8_info类型的tag

image-20240522171841207

跟1个u2类型的长度(偏移地址:0x000000190x10,长度是16【length值说明了这个UTF-8编码的字符串长度是多少字节,它后面紧跟着的长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串。UTF-8缩略编码与普通UTF-8编码的区别是:从’\u0001’到’\u007f’之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示,从’\u0080’到’\u07ff’之间的所有字符的缩略编码用两个字节表示,从’\u0800’开始到’\uffff’之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示】,这里后面数据是(偏移地址:0x00000029) java/lang/object

image-20240522172456418

第四个常量池数据结束,目前得到的常量池:

[01] CONSTANT_Methodref_info #2,#3
[02] CONSTANT_Class_info #4
[03] CONSTANT_NameAndType #5,#6
[04] CONSTANT_Utf8_info java/lang/Object

从上面的第二个常量池[02] CONSTANT_Class_info #4知道它的索引是第四个常量池,而第四个常量池便是我们的[04] CONSTANT_Utf8_info java/lang/Object.

jclasslib

以此类推,便可以获得所有的常量池数据;这里虽然抽象但是具体的步骤便是如此,为了让大家能明白,我们使用IntelliJ IDEAjclasslib插件来验证上面的解析。

image-20240522173126549

第四个常量池长度16,字符串便是我们解析的 java/lang/Object

image-20240522173151937

image-20240522173219621

image-20240522173232656

javap

我们再使用jdk提供的命令来验证javap -c -v Test.class

Classfile /D:/project/java/blog/target/classes/com/mystic/ycc/blog/test/Test.class
  Last modified 2024-5-22; size 319 bytes
  MD5 checksum d41bbe1c5d1a6eb390ca59cadaeb48d3
  Compiled from "Test.java"
public class com.mystic.ycc.blog.test.Test
  minor version: 0
  major version: 61
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // com/mystic/ycc/blog/test/Test
   #8 = Utf8               com/mystic/ycc/blog/test/Test
   #9 = Utf8               i
  #10 = Utf8               I
  #11 = Utf8               y
  #12 = Utf8               J
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/mystic/ycc/blog/test/Test;
  #18 = Utf8               SourceFile
  #19 = Utf8               Test.java
{
  public int i;
    descriptor: I
    flags: ACC_PUBLIC

  public long y;
    descriptor: J
    flags: ACC_PUBLIC

  public com.mystic.ycc.blog.test.Test();
    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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/mystic/ycc/blog/test/Test;
}
SourceFile: "Test.java"

这里的1到4的常量池便是上面一步步解析的数据。

未完!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片