手动翻译class文件

96
lanzry
2018.03.18 22:03* 字数 1923

一步步翻译class,探究class文件结构。
自己去一点点把class文件格式化,是一件非常有意思的事情。

1. 准备

用来测试的java类:

public class Test {
    public static final String s = "good";

    int i;

    public void set(int i) {
        this.i = i;
    }
}

这样一个简单的java类文件,通过javac工具编译成class后都包含什么东西呢?



用记事本打开Test.class,得到如下:

cafe babe 0000 0034 0018 0a00 0400 1309
0003 0014 0700 1507 0016 0100 0173 0100
124c 6a61 7661 2f6c 616e 672f 5374 7269
6e67 3b01 000d 436f 6e73 7461 6e74 5661
6c75 6508 0017 0100 0169 0100 0149 0100
063c 696e 6974 3e01 0003 2829 5601 0004
436f 6465 0100 0f4c 696e 654e 756d 6265
7254 6162 6c65 0100 0373 6574 0100 0428
4929 5601 000a 536f 7572 6365 4669 6c65
0100 0954 6573 742e 6a61 7661 0c00 0b00
0c0c 0009 000a 0100 0454 6573 7401 0010
6a61 7661 2f6c 616e 672f 4f62 6a65 6374
0100 0467 6f6f 6400 2100 0300 0400 0000
0200 1900 0500 0600 0100 0700 0000 0200
0800 0000 0900 0a00 0000 0200 0100 0b00
0c00 0100 0d00 0000 1d00 0100 0100 0000
052a b700 01b1 0000 0001 000e 0000 0006
0001 0000 0001 0001 000f 0010 0001 000d
0000 0022 0002 0002 0000 0006 2a1b b500
02b1 0000 0001 000e 0000 000a 0002 0000
0007 0005 0008 0001 0011 0000 0002 0012

最早的时候我看见这一堆,绝对要惊呼乱码了。接下来就要把这些16进制的字节一步步翻译成我们看得懂的东西,从而去探究class的文件结构。
假如还看过一点jvm相关知识,翻译着翻译着会心一笑,原来如此。

1.1 javap工具

因为要探究16进制字节形式的class文件的具体结构,肯定是自己去把16进制字节一点点翻译过来会比较印象深刻。
使用javap工具呢,可以把class文件直接翻译成我们看得懂的英文的形式。这样我们也可以用来作为参照,看自己是否翻译对了。还能省去把16进制字节转换成英文utf-8字符串的步骤。

javap -verbose Test.class
Classfile /Users/lanzry/Documents/java/test/Test.class
  Last modified 2018年3月17日; size 336 bytes
  MD5 checksum b4674caa94520a1eeb8363c79b349ef2
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 53
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #3                          // Test
  super_class: #4                         // java/lang/Object
  interfaces: 0, fields: 2, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #4.#19         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#20         // Test.i:I
   #3 = Class              #21            // Test
   #4 = Class              #22            // java/lang/Object
   #5 = Utf8               s
   #6 = Utf8               Ljava/lang/String;
   #7 = Utf8               ConstantValue
   #8 = String             #23            // good
   #9 = Utf8               i
  #10 = Utf8               I
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               set
  #16 = Utf8               (I)V
  #17 = Utf8               SourceFile
  #18 = Utf8               Test.java
  #19 = NameAndType        #11:#12        // "<init>":()V
  #20 = NameAndType        #9:#10         // i:I
  #21 = Utf8               Test
  #22 = Utf8               java/lang/Object
  #23 = Utf8               good
{
  public static final java.lang.String s;
    descriptor: Ljava/lang/String;
    flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: String good

  int i;
    descriptor: I
    flags: (0x0000)

  public Test();
    descriptor: ()V
    flags: (0x0001) 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 1: 0

  public void set(int);
    descriptor: (I)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #2                  // Field i:I
         5: return
      LineNumberTable:
        line 7: 0
        line 8: 5
}
SourceFile: "Test.java"

2. 手动格式化

这样的文件,就像没有标点符号的文言文,令人生畏。我要把它格式化。

# 魔数
cafe babe 
# java版本号(换算成十进制=52,即java8)
0000 0034 

# 常量池大小(换算=24,常量池第0项空着,所以共23个)
0018 
# 常量池
# 16进制是4位,8位是一个字节,所以两个数字是一字节
# 第一个字节是常量的类型,后面跟着该类型常量的属性或者值
0a 0004 0013
09 0003 0014 
07 0015 
07 0016 
01 0001 73 
01 0012 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b
01 000d 436f 6e73 7461 6e74 5661 6c75 65
08 0017 
01 0001 69 
01 0001 49 
01 0006 3c69 6e69 743e 
01 0003 2829 56
01 0004 436f 6465 
01 000f 4c 696e 654e 756d 6265 7254 6162 6c65 
01 0003 73 6574 
01 0004 2849 2956
01 000a 536f 7572 6365 4669 6c65
01 0009 5465 7374 2e6a 6176 61 
0c 000b 000c
0c 0009 000a 
01 0004 5465 7374
01 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 6374
01 0004 676f 6f64

# 当前类的访问修饰符
0021 
# 当前类的索引,指向常量池第3项
0003
# 父类索引
0004
# 接口个数
0000

# 字段表(字段数量开头,两个字段)
0002 
0019 0005 0006 0001 0007 00000002 0008
0000 0009 000a 0000 

# 方法表
0002
# 方法1
0001 000b 000c 0001 000d 0000001d 0001 0001 0000 0005 2ab7 0001 b100 0000 0100 0e00 0000 0600 0100 0000 01
# 方法2
0001 000f 0010 0001 000d 00000022 0002 0002 0000 0006 2a1b b500
02b1 0000 0001 000e 0000 000a 0002 0000 0007 0005 0008 

# 属性表
0001 
0011 00000002 0012

大功告成,把一个class文件,按自己知道的去格式化出来了,这样就清爽很多了。
自己把class文件整理一遍,真是能非常加深印象啊!
你看,一开始是魔数,版本号,常量池,类的访问修饰符,类索引,父类索引,接口索引,字段表,方法表,属性表,over!就这些,非常规整!

当然会看这篇文章的小伙伴,证明你不太明白如何这样整理。参照我接下来的知识就OK了!

接下来,就要看看是如何格式化出来的了!!!

3. 相关的java虚拟机规范

下面会给出参考Java9的虚拟机规范的相关知识点,会附上原英文链接。

3.1 class文件结构

The ClassFile Structure

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];
}

左边一列是u1、u2、u3、u4和_info。
u1、u2是什么呢?其实是无符号数,后面数字表示几个字节。比如u1表示一个字节的无符号数。比如上述第一个u4 magic,意思是class文件结构第一个是一个由4个字节表示的叫做“魔数”的东西。然后接下来两个字节的minor version,2个字节的major version...
_info表示这是一个表。class文件中有很多表,每个表有不同的结构。上述我们能看见的就有常量池表、字段表、方法表等等。表其实就是一个相同数据的集合,和列表的意思一样,每种表的结构是不一样的,具体碰到的时候再去研究。

接下来,就按这张图的顺序,将以上16进制的数据一一解释成我们能看的懂的方式。

3.2 常量池

Constant pool tags

Constant Type Value class file Java SE
CONSTANT_Class 7 45.3 1.0.2
CONSTANT_Fieldref 9 45.3 1.0.2
CONSTANT_Methodref 10 45.3 1.0.2
CONSTANT_InterfaceMethodref 11 45.3 1.0.2
CONSTANT_String 8 45.3 1.0.2
CONSTANT_Integer 3 45.3 1.0.2
CONSTANT_Float 4 45.3 1.0.2
CONSTANT_Long 5 45.3 1.0.2
CONSTANT_Double 6 45.3 1.0.2
CONSTANT_NameAndType 12 45.3 1.0.2
CONSTANT_Utf8 1 45.3 1.0.2
CONSTANT_MethodHandle 15 51.0 7
CONSTANT_MethodType 16 51.0 7
CONSTANT_InvokeDynamic 18 51.0 7
CONSTANT_Module 19 53.0 9
CONSTANT_Package 20 53.0 9

常量池的类型一共以上这些,他们的结构也各不相同,全部列出来太多了。大家有兴趣可以自己去官方文档了解,走各个小节的英文文档入口。

这里把我们翻译的部分的几个类型列一下,一共是0a、09、07、01、08、0c这几个。

3.2.1 The CONSTANT_Fieldref_info, CONSTANT_Methodref_info

16进制 10进制 对应 意思
0a 10 CONSTANT_Methodref 方法符号引用
09 9 CONSTANT_Fieldref_info 字段符号引用

CONSTANT_Methodref_info

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}
CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

0a、09分别是方法和字段,二者的结构一模一样,所以一起写。

例如方法符号引用的原文:0a 0004 0013
按上述结构对应起来,0a表示常量类型(tag),0004表示该方法所在类的索引是常量池第4个,0013表示该方法的NameAndType属性的索引位置是常量池第19个(16进制的13)。

3.2.2 CONSTANT_Class

16进制 10进制 对应 意思
07 7 CONSTANT_Class 类符号引用

The CONSTANT_Class_info Structure

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

例:07 0015
类型是7,表示类符号引用,类名描述符是常量池21(16进制的15)项。
再看常量池21项是这样的:01 0004 5465 7374
具体什么意思呢?接下去

3.2.3 CONSTANT_Utf8_info Structure

16进制 10进制 对应 意思
01 7 CONSTANT_Utf8_info utf-8字符串

The CONSTANT_Utf8_info Structure

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

前一小节例子:01 0004 5465 7374
表示3个字节长的utf-8类型数据,那么5465 7374这三个字节是什么意思?
[16进制到文本字符串的转换,在线实时转换]

很明显了,就是我们的类Test.java,类名就是Test。

3.2.4 The CONSTANT_String_info Structure

16进制 10进制 对应 意思
08 7 CONSTANT_String_info String类型常量

CONSTANT_String_info Structure

CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

例:08 0017

0017恰好是23项,01 0004 676f 6f64,我们再去转换一下:
就是我们测试例子中的常量字符串good了。

3.2.6 The CONSTANT_NameAndType_info Structure

16进制 10进制 对应 意思
0c 12 CONSTANT_NameAndType_info NameAndType类型

CONSTANT_NameAndType_info Structure

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

例:0c 000b 000c
类型是NameAndType,名称索引在第11项,描述索引是第12项。

常量池就介绍到这里,其他的常量池常量类型,大家就可以遇到的时候去官方文档The Constant Pool中查找了。

3.3 字段表

Fields

field_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

access_flags:访问修饰符

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_PRIVATE 0x0002 Declared private; usable only within the defining class.
ACC_PROTECTED 0x0004 Declared protected; may be accessed within subclasses.
ACC_STATIC 0x0008 Declared static.
ACC_FINAL 0x0010 Declared final; never directly assigned to after object construction (JLS §17.5).
ACC_VOLATILE 0x0040 Declared volatile; cannot be cached.
ACC_TRANSIENT 0x0080 Declared transient; not written or read by a persistent object manager.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ENUM 0x4000 Declared as an element of an enum.

name_index、descriptor_index,依旧是常量池的索引。

attribute_info,是一个属性表。依旧会是属性数量,然后属性的列表的形式。后文再介绍。

3.4 方法表

Methods

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

和字段表差别不大。字段和方法的访问修饰符还是有所差别的,下面列出来:

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_PRIVATE 0x0002 Declared private; accessible only within the defining class.
ACC_PROTECTED 0x0004 Declared protected; may be accessed within subclasses.
ACC_STATIC 0x0008 Declared static.
ACC_FINAL 0x0010 Declared final; must not be overridden (§5.4.5).
ACC_SYNCHRONIZED 0x0020 Declared synchronized; invocation is wrapped by a monitor use.
ACC_BRIDGE 0x0040 A bridge method, generated by the compiler.
ACC_VARARGS 0x0080 Declared with variable number of arguments.
ACC_NATIVE 0x0100 Declared native; implemented in a language other than the Java programming language.
ACC_ABSTRACT 0x0400 Declared abstract; no implementation is provided.
ACC_STRICT 0x0800 Declared strictfp; floating-point mode is FP-strict.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.

还是举个例子,做一次翻译:

# 方法1
0001 000b 000c 0001 000d 0000001d 0001 0001 0000 0005 2ab7 0001 b100 0000 0100 0e00 0000 0600 0100 0000 01

0001:ACC_PUBLIC
000b:方法名 = 常量池#11 = Utf8 = <init>(也就是默认的构造方法)
000c:方法描述 = 常量池#12 = Utf8 = ()V
括号内空,表示没有参数;V表示返回值是void。这部分的文档见Method Descriptors
0001:表示属性表只有一个属性。

属性表

000d 
0000001d 
0001 0001 0000 0005 2ab7 0001 b100 0000 0100 0e00 0000 0600 0100 0000 01

000d:#13 = Utf8 = Code,表示是一个Code属性
The Code Attribute

Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

看这个就很有意思了,这里面像发现宝藏一样。比如max_stack、max_locals是不是就是操作数栈、局部变量表的最大大小呢?之类,很有意思。
我们来翻译一遍:

# Code
000d 
# 长度29
0000001d
# max_stack=1 
0001 
# max_locals=1
0001 
# code_length=5
00000005 
2ab7 0001 b1
# 没有异常,所以异常表是0
0000
# 后面又接着1个属性表
0001 
# 14 = Utf8 = LineNumberTable属性
000e 
# LineNumberTable长度
00000006 
# start_pc这个就很容易懂了
0001 
# start_pc
0000
# lineNumber
0001

简单介绍一下The LineNumberTable Attribute,就是字节码对应源代码的行号,这样调试的时候可以根据这个找到当前执行到了哪一行。

LineNumberTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 line_number_table_length;
    {   u2 start_pc;
        u2 line_number; 
    } line_number_table[line_number_table_length];
}

3.5 属性表

Attributes

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

把最后多余出来的属性表作为例子:

# 属性表
0001 
0011 00000002 0012

该属性名字是常量池第17项(16进制的11),01 000a 536f 7572 6365 4669 6c65,01类型是utf-8字符串,转换一下:SourceFile

属性表里面又分很多属性类别,定位到属性名称后,再去官方查找对应文档即可。

The SourceFile Attribute

SourceFile_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 sourcefile_index;
}

值是长度是2个字节。最后一项sourcefile_index索引,0012,对应常量池18项:01 0009 5465 7374 2e6a 6176 61 ,转换一下:Test.java
意思就是记录源文件名字是Test.java。

这里列出所有属性的类型:
Table 4.7-A. Predefined class file attributes (by section)

Attribute Section class file Java SE
ConstantValue §4.7.2 45.3 1.0.2
Code §4.7.3 45.3 1.0.2
StackMapTable §4.7.4 50.0 6
Exceptions §4.7.5 45.3 1.0.2
InnerClasses §4.7.6 45.3 1.1
EnclosingMethod §4.7.7 49.0 5.0
Synthetic §4.7.8 45.3 1.1
Signature §4.7.9 49.0 5.0
SourceFile §4.7.10 45.3 1.0.2
SourceDebugExtension §4.7.11 49.0 5.0
LineNumberTable §4.7.12 45.3 1.0.2
LocalVariableTable §4.7.13 45.3 1.0.2
LocalVariableTypeTable §4.7.14 49.0 5.0
Deprecated §4.7.15 45.3 1.1
RuntimeVisibleAnnotations §4.7.16 49.0 5.0
RuntimeInvisibleAnnotations §4.7.17 49.0 5.0
RuntimeVisibleParameterAnnotations §4.7.18 49.0 5.0
RuntimeInvisibleParameterAnnotations §4.7.19 49.0 5.0
RuntimeVisibleTypeAnnotations §4.7.20 52.0 8
RuntimeInvisibleTypeAnnotations §4.7.21 52.0 8
AnnotationDefault §4.7.22 49.0 5.0
BootstrapMethods §4.7.23 51.0 7
MethodParameters §4.7.24 52.0 8
Module §4.7.25 53.0 9
ModulePackages §4.7.26 53.0 9
ModuleMainClass §4.7.27 53.0 9

到这里就算差不多了。简单地了解一下简单java类的class文件结构,就已经足够了。以后即使是复杂的java类,其实我们也能明白结构是怎么样。即使不知道,还有官方文档The Java® Virtual Machine Specification Java SE 9 Edition啊 哈哈

JVM