lss233 17 min read
新技能学习:教你如何阅读Java字节码

Java字节码是由 .java 文件通过Java编译器编译成 .class 文件所包含的代码,我们通常使用的Java程序就是通过Java虚拟机执行Java字节码来得到的效果。
既然我们可以口算MD5、手算二维码,这篇文章就来讲讲眼看Java字节码吧!

0x0000 - 准备工作

在开始学习之前,请先准备好下列工具:
1. 一款支持HEX格式的编辑器,如 Sublime Text(推荐)、Ultra Editor等。
2. 一个Java环境开发环境,用来编译一个.class文件。

你需要知道这些基本知识:

  • HEX在这里指的是十六进制
  • 在计算机中,1个字节可以表示成2个连续的16进制数字,如 000AFF等。
  • C语言、C++、Shell、Python、Java语言及其他相近的语言使用字首0x表示一个16进制数字的开始,如0x0000
  • 了解大端(Big-Endian)模式的字节序(Endian),详细介绍可以看这里

注:本文主要参考资料来自Java Virtual Machine Specification(JVM规范) 第4章,本文内容仅是皮毛。为了方便读者自行了解,本文将不会翻译一些专有名词。

接下来,我们先从简单的开始。

0x0001 - HelloWorld.class

写一个HelloWorld.java:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

然后编译得到 HelloWorld.class文件,我们使用HEX编辑器打开,你大概会看到这样的内容:

cafe babe 0000 0031 0022 0a00 0600 1409
0015 0016 0800 170a 0018 0019 0700 1a07
001b 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 0c4c 4865 6c6c 6f57
6f72 6c64 3b01 0004 6d61 696e 0100 1628
5b4c 6a61 7661 2f6c 616e 672f 5374 7269
6e67 3b29 5601 0004 6172 6773 0100 135b
4c6a 6176 612f 6c61 6e67 2f53 7472 696e
673b 0100 0a53 6f75 7263 6546 696c 6501
000f 4865 6c6c 6f57 6f72 6c64 2e6a 6176
610c 0007 0008 0700 1c0c 001d 001e 0100
0b48 656c 6c6f 2057 6f72 6c64 0700 1f0c
0020 0021 0100 0a48 656c 6c6f 576f 726c
6401 0010 6a61 7661 2f6c 616e 672f 4f62
6a65 6374 0100 106a 6176 612f 6c61 6e67
2f53 7973 7465 6d01 0003 6f75 7401 0015
4c6a 6176 612f 696f 2f50 7269 6e74 5374
7265 616d 3b01 0013 6a61 7661 2f69 6f2f
5072 696e 7453 7472 6561 6d01 0007 7072
696e 746c 6e01 0015 284c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b29 5600 2100
0500 0600 0000 0000 0200 0100 0700 0800
0100 0900 0000 2f00 0100 0100 0000 052a
b700 01b1 0000 0002 000a 0000 0006 0001
0000 0001 000b 0000 000c 0001 0000 0005
000c 000d 0000 0009 000e 000f 0001 0009
0000 0037 0002 0001 0000 0009 b200 0212
03b6 0004 b100 0000 0200 0a00 0000 0a00
0200 0000 0300 0800 0400 0b00 0000 0c00
0100 0000 0900 1000 1100 0000 0100 1200
0000 0200 13

如果你完全不了解字节码,可能觉得这一团乱七八糟毫无头绪。

提示: 由于本人使用Maven编译生成的这个文件,它默认会尝试使用比较低的Java编译器编译。因此如果你的文件和本文不同,在这里下载我的HelloWorld.class,或者使用和本人一样的Maven配置构建。

别急,让我们先来了解一下它的结构。

在Java中,一个.class文件的结构是这样的:

ClassFile {
    4字节             Java魔值;
    4字节             编译器次要版本;
    2字节             编译器主要版本;
    2字节             常量池数;
   很多字节        常量池;
    2字节             访问修饰符;
    2字节             当前类;
    2字节             父类;
    2字节             接口数;
   很多字节        接口;
    2字节             字段数;
   很多字节        字段;
    2字节             方法数;
   很多字节        方法;
    2字节             属性数;
   很多字节        属性;
}

Java魔值

魔值(Magic Number)是一段特定的用来标识某种文件或者协议的格式。
Java作为一种咖啡,它的魔值非常容易辨认 —— CA FE BA BE,没错,Cafe Babe
这4个字节告诉你它是一个Java 类文件,所有的.class文件都会以这4个字节开始。
关于它的由来,还有一个有趣的背景故事。如果你感兴趣,可以点击这里

主要版本和次要版本

接下来的四个字节,是两个由2字节组成的编译器版本号。
在本文中,他们是 0000 0031
0000说明次要版本号为0, 0031转成十进制之后是49,说明这个类文件的编译器版本是49.0。对应到Java版本则是Java 5
关于版本号的对应,可以参考这里

常量池(Constant Pool)

接下来的两个字节,常量池数,是整个类文件中非常重要的两个字节。它会告诉我们常量池的大小。

常量池(Constant Pool)是类文件中最长,也是最重要的部分。你的类名、引用、字符串等等,基本上一个类里的所有东西都在这里面了。
值得注意的是,常量池中的常量的索引是从1开始的,它的数量只有常量池数 - 1那么多。在我们这个文件中,常量池数是34,实际上有33个常量。
字节码之间会通过直接表示索引的方式引用某个常量。
常量通常由1个字节的标签和多个字节的值来组成。

解读常量池

常量池内包含的信息量巨大而且复杂,第一次解读可能会发生困难,没关系,我们一个个来看。

#1

我们先来看看第1个常量:

0a00 0600 14
  • 0a 表示这个常量是一个方法引用(Methodref)
  • 00 06 表示它所在的类索引
  • 00 14 则指向这个方法的名字与返回值类NameAndType

同样,使用09表示的字段(Fieldref),0b表示的接口方法(InterfaceMethodref)也是同样的结构。

#2

所以我们继续看下一组,

09 0015 0016

表示的是一个字段。

#3

接下来一组

0800 17
  • 08表示这是一个String常量。
  • 00 17 表示这个String的文本索引。这个索引应该对应的是一个Utf8常量。

#4

下一组

0a 0018 0019

和前面的内容一样,咱就不说了。

#5-6

接下来两组

07 00 1a
07 00 1b
  • 07 表示这是一个Class常量。
  • 00 1a和00 1b`则是他们名字的索引。他们应该指向的都是Utf8常量。
    还记得 #1 吗?它就指向了 #6 这个类。
    但是我们目前还没有办法知道这具体是个什么类,继续往下看吧。

#7-19

接下来几组比较长:

01 0006 3c69 6e69 743e
01 0003 2829 56
01 0004 436f 6465
01 000f 4c69 6e65 4e75 6d62 6572 5461 626c 65
01 0012 4c6f 6361 6c56 6172 6961 626c 6554 6162 6c65
01 0004 7468 6973 
01 000c 4c48 656c 6c6f 576f 726c 643b
01 0004 6d61 696e
01 0016 285b 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 2956 
01 0004 6172 6773 
01 0013 5b4c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b
01 000a 536f 7572 6365 4669 6c65 
01 000f 4865 6c6c 6f57 6f72 6c64 2e6a 6176 61
  • 第一个字节01表示他们是Utf8常量。这些常量直接存放文本,我们可以用记事本格式直接阅读。
    备受好评的 hhclass 汉化工具就是通过修改这些常量实现的。
  • 接下来是由2个字节组成的一个数,它表示文本的长度。
    拿第一个来说,它是0006,表示接下来6个字节都是文本内容。
  • 剩下的字节全部都是 Utf8 编码的文本。一般情况下英文字母由1个字节组成,其他文本如汉字会用到2个字节。
    还是第一个例子,3c69 6e69 743e表示的文本是<init>
    关于Utf8转文本的方法,你可以使用在线工具(因为是英文字母,所以记得在每个字节后面加上空格),或者使用HEX编辑器新建一个txt文档然后粘贴进去,再用别的文本编辑器打开它。
    <init>是一个类的初始化方法,也就是构造函数在Java字节码中的名字。

这些文本通常都会被其他字节码引用,不过目前还没有遇到。

#20

0c 0007 0008
  • 0c表示这是一个 NameAndType 常量,用来表示一个字段或者方法。
  • 0007是它的名字,对应索引 #7 ,也就是<init>
  • 0008是它的描述,对应索引 #8 ,是()V。它包含了这个方法的参数。V表示void

这个常量被 #1 引用了,结合前面的内容,我们大致可以推出这么一个方法:

void main();

#21

0700 1c

又一个类。

#22

0c 001d 001e

又一个NameAndType

#23

01 000b 4865 6c6c 6f20 576f 726c 64

又是一个字段。它被 #3 引用了,转成文本是:Hello Word。没错,就是我们在代码里写的那个 String

轮到你了

不知不觉,我们已经看完了一半的类文件。常量池中剩下的10个常量,你自己能阅读下来了吗?
根据JVM规范,常量池中所有的Tags定义如下:

常量类型 标识
Utf81
Integer3
Float4
Long5
Double6
Class7
String8
Fieldref9
Methodref10
InterfaceMethodref11
NameAndType12
MethodHandle15
InvokeDynamic18

其中,Utf8ClassStringFieldrefMethodrefNameAndType 我们已经介绍过了。来看看剩下几种常量。

Integer 和 Float

这两种类型的常量从开始到结束共有 5 个字节。

  • 第1位字节表示 TagInteger3Float4
  • 第2-5位字节表示它的值,这些值使用 Big-Endian 的格式存储。
Long 和 Double

这两种类型的常量从开始到结束共有 9 个字节。

  • 第1位字节表示 TagLong5Double6
  • 第2-5位字节表示高位字节(high_bytes)。
  • 第6-9位字节表示低位字节(low_bytes)。

他们的值采用这样的算法可以得到:

((long) high_bytes << 32) + low_bytes
MethodHandle

这两种类型的常量从开始到结束共有 4 个字节。

  • 第1位字节是 15
  • 第2位字节是引用类型(reference_kind),它的值通常为 1-9中的一个。
  • 第3-4位字节是引用目标的索引(reference_index)。

MethodHandle通常会用来表示字节码中一个方法的行为。

引用类型参考表
描述 目标类型
1REF_getFieldFieldref
2REF_getStaticFieldref
3REF_putFieldFieldref
4REF_putStaticFieldref
5REF_invokeVirtualMethodref
6REF_invokeStaticMethodref,InterfaceMethodref(5.20及以上)
7REF_invokeSpecialMethodref,InterfaceMethodref(5.20及以上)
8REF_newInvokeSpecialMethodref
9REF_invokeInterfaceInterfaceMethodref

如果想深入了解,请移步至JVM规范 第5章

InvokeDynamic

这两种类型的常量从开始到结束共有 5 个字节。

  • 第1位为 18
  • 第2-3位为启动方法属性BootstrapMethods。关于属性(Attribute)的介绍,稍后会提到。
  • 第4-5位为NameAndType

验证你的结果

Java自带一个Class文件分析工具javap。执行 javap -verbose HelloWorld.class 就可以看到这个类的常量池内容,看看和你读的是不是一样的吧。

访问修饰符(Access Flag)

访问修饰符指的是:

名称对象描述
ACC_PUBLIC0x0001所有类型public
ACC_FINAL0x0010final,不允许有子类
ACC_SUPER0x0020类和接口使用新的invokespecial语义
ACC_INTERFACE0x0200接口interface
ACC_ABSTRACT0x0400类和接口abstract
ACC_SYNTHETIC0x1000所有类型该类不由用户代码生成
ACC_ANNOTATION0x2000注解类型@interface
ACC_ENUM0x4000枚举类型enum

一个类型可以有多个访问修饰符,这些修饰符的值决定了他们相互之间的并存性。
在我们的 HelloWorld.class中,访问修饰符为0021,它由0x0001 + 0x0020得到,表示这是一个 Super Public 类。

当前类(this_class)和父类(super_class)

往下读2个字节是当前类的索引,0005 指向 #5,名字指向 #26,是 HelloWorld

再下2个字节是父类的索引。我们可以看到,是0006。在常量池中找一下,#6 ,发现它的名字在 #27,转成文本得到:java/lang/Object 看来没错。
值得注意的是,在字节码中,类的名字不是包名.类名,而是包名/类名

字段(Field)、接口(Interface)、方法(Method)

从这里开始到文件结尾,接下来的格式都是

  • 2字节的类型数量
  • 多个字节的值(取决于类型数量):
    • 2字节访问修饰符(access_flag)
    • 2字节名称索引(name_index)
    • 2字节描述索引(descriptor_index)
    • 2字节属性数量(attribute_count)
    • 多个字节属性的值(取决于属性数量):
      • 2字节属性名称索引(attribute_name_index)
      • 4字节属性长度(attribute_length)
      • 多字节属性的值(取决于属性的长度)

我们的 HelloWorld 没有字段和接口,所以我们可以看见下4个字节都是00000000
这说明字段和接口的数量都是0。在其他类文件中,如果存在字段和接口,那么在2个字节的 数量 之后,就是关于他们的定义。

继续往下读2个字节,是我们的方法数。 我们可以看到它是0002
我们的 HelloWorld 中有 1 个 main方法,还有类自身存在的构造方法 <init>
让我们来看看。

遵循前面的规律,我们开始看这个类的方法。

  • 接下来的2个字节就是第1个方法的访问修饰符: 0001,含义是public
  • 2个字节的名称索引:0007#7, 含义是 <init>
  • 2个字节的描述索引:0008#8, 含义是 ()V
  • 2个字节的属性数量: 0001,有1个:
    • 2个字节的属性名称索引:0009#9,含义是 Code
    • 4个字节的属性长度: 0000 002f。因为我们没有定义它的代码,这个类是编译器自动生成的。

方法描述(Method Descriptors)

它可以告诉我们这个方法的参数和返回值。
它的格式是

( {参数值描述} ) 返回值描述

这些描述ParameterDescriptor 是字段类型FieldType
返回值如果是 void, 那么它的值是V
字段类型是这样定义的:

类型
B byte
C char
D double
F float
I int
J long

举JVM规范中的例子

(IDLjava/lang/Thread;)Ljava/lang/Object;

相当于源码中的

Object 方法名(int 参数1, double 参数2, Thread 参数3){
/** 代码 **/
}

方法属性(Method Attributes)

一个方法通常会有的属性有Code
Code属性包含了这个方法的字节操作,这些内容在JVM规范中有专门的一节介绍
通过解读这些字节,你可以还原出整个方法内的源码。

Code通常还会包含2个属性:LocalVariableTableLineNumberTable
它们分别被用来标记某段字节操作的变量和所在的行号。

属性(Attribute)

关于属性(Attribute),这里有详细的介绍。
由于内容较长,本文不打算继续展开讲解。
本文主要起到抛砖引玉的作用,如果你感兴趣,可以点这里继续了解。

0xffffff - 总结

到目前为止,我们已经差不多把这个.class文件解剖干净了。
我们把字节码稍微归类,打上注释,可以得到下面的这个样子:

cafe babe              // Java Magic Number

0000 0031              // Java Compiler Version

0022                   // Constant Pool Size

                       // Constant Pool Begin
/* # 1 */ 0a 0006 0014
/* # 2 */ 09 0015 0016 
/* # 3 */ 08 0017
/* # 4 */ 0a 0018 0019
/* # 5 */ 07 001a
/* # 6 */ 07 001b
/* # 7 */ 01 0006 3c69 6e69 743e
/* # 8 */ 01 0003 2829 56
/* # 9 */ 01 0004 436f 6465
/* #10 */ 01 000f 4c69 6e65 4e75 6d62 6572 5461 626c 65
/* #11 */ 01 0012 4c6f 6361 6c56 6172 6961 626c 6554 6162 6c65 
/* #12 */ 01 0004 7468 6973 
/* #13 */ 01 000c 4c48 656c 6c6f 576f 726c 643b
/* #14 */ 01 0004 6d61 696e 
/* #15 */ 01 0016 285b 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 2956
/* #16 */ 01 0004 6172 6773 
/* #17 */ 01 0013 5b4c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b 
/* #18 */ 01 000a 536f 7572 6365 4669 6c65
/* #19 */ 01 000f 4865 6c6c 6f57 6f72 6c64 2e6a 6176 610c 00
/* #20 */ 07 0008 
/* #21 */ 07 001c
/* #22 */ 0c 001d 001e 
/* #23 */ 01 000b 4865 6c6c 6f20 576f 726c 64
/* #24 */ 07 001f
/* #25 */ 0c 0020 0021 
/* #26 */ 01 000a 4865 6c6c 6f57 6f72 6c64
/* #27 */ 01 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 6374 
/* #28 */ 01 0010 6a61 7661 2f6c 616e 672f 5379 7374 656d
/* #29 */ 01 0003 6f75 74
/* #30 */ 01 0015 4c6a 6176 612f 696f 2f50 7269 6e74 5374 7265 616d 3b
/* #31 */ 01 0013 6a61 7661 2f69 6f2f 5072 696e 7453 7472 6561 6d
/* #32 */ 01 0007 7072 696e 746c 6e
/* #33 */ 01 0015 284c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b29 56
                       // Constant Pool End
                       
0021                   // Class Access Modifier: Super,Public
0005                   // This Class: HelloWorld
0006                   // Super Class: java/lang/Object

0000                   // Fields Count: 0
                       // Fields Begin
                       // Fields End
0000                   // Interfaces Count: 0
                       // Interfaces Begin
                       // Interfaces End
0002                   // Methods Count: 2
                       // Methods Begin
0001                   /// Method 1 Access Flag: Public
0007                   /// Method 1 Name: <init>
0008                   /// Method 1 Descriptor: ()V
                       /// Method 1 : public HelloWorld();
0001                   /// Method 1 Attribute Count: 1
                       /// Method 1 Attribute Begin
0009                   //// Method 1 Attribute 1 Name: Code
0000 002f              //// Method 1 Attribute 1 Length: 47
0001                   //// Method 1 Attribute 1 [Code] Max Stack: 1
0001                   //// Method 1 Attribute 1 [Code] Max Local Variables Count: 1
0000 0005              //// Method 1 Attribute 1 [Code] Code length: 5
                       //// Method 1 Attribute 1 [Code] Code Begin
2a
b7
00 
01
b1 
                       //// Method 1 Attribute 1 [Code] Code End
0000                   //// Method 1 Attribute 1 [Code] Exception Table Length: 0
                       //// Method 1 Attribute 1 [Code] Exception Table Begin
                       //// Method 1 Attribute 1 [Code] Exception Table End
0002                   //// Method 1 Attribute 1 [Code] Attribute Count: 2
                       //// Method 1 Attribute 1 [Code] Attribute Begin
000a                   ///// MA 1 Attribute 1 Name: LineNumberTable
0000 0006              ///// MA 1 Attribute 1 Length: 6
0001                   ///// MA 1 Attribute 1 [LNT] Line Number Table Length: 1
                       ///// MA 1 Attribute 1 [LNT] Line Number Table Begin
0000                   ////// MA 1 Attribute 1 [LNT] Code reference index: 0
0001                   ////// MA 1 Attribute 1 [LNT] Line Number: 1
                       ///// MA 1 Attribute 1 [LNT] Line Number Table End
                       
000b                   ///// MA 1 Attribute 2 Name: LocalVariableTable
0000 000c              ///// MA 1 Attribute 2 Length: 12
0001                   ///// MA 1 Attribute 2 [LVT] Local Variable Table Length: 1
                       ///// MA 1 Attribute 2 [LVT] Local Variable Table Begin
0000                   ////// MA 1 Attribute 2 [LVT] start_pc: 0
0005                   ////// MA 1 Attribute 2 [LVT] length: 5
000c                   ////// MA 1 Attribute 2 [LVT] Name: this
000d                   ////// MA 1 Attribute 2 [LVT] Descriptor: LHelloWorld;
0000                   ////// MA 1 Attribute 2 [LVT] Index: 0
                       /// Method 1 Attribute End
                       
0009                   /// Method 2 Access Flag: Public Static
000e                   /// Method 2 Name: main
000f                   /// Method 2 Descriptor: ([Ljava/lang/String;)V
                       /// Method 2 : public static void main(String[] 变量1);
0001                   /// Method 2 Attribute Count: 1
                       /// Method 2 Attribute Begin
0009                   //// Method 2 Attribute 1 Name: Code
0000 0037              //// Method 2 Attribute 1 Length: 55
0002                   //// Method 2 Attribute 1 [Code] Max Stack: 2
0001                   //// Method 2 Attribute 1 [Code] Max Local Variables Count: 1
0000 0009              //// Method 2 Attribute 1 [Code] Code length: 9
                       //// Method 2 Attribute 1 [Code] Code Begin
b2
00
02
12
03
b6
00
04
b1
                       //// Method 2 Attribute 1 [Code] Code End
0000                   //// Method 2 Attribute 1 [Code] Exception Table Length: 0
                       //// Method 2 Attribute 1 [Code] Exception Table Begin
                       //// Method 2 Attribute 1 [Code] Exception Table End

0002                   //// Method 2 Attribute 1 [Code] Attribute Count: 2
                       //// Method 2 Attribute 1 [Code] Attribute Begin
000a                   ///// MA 2 Attribute 1 Name: LineNumberTable
0000 000a              ///// MA 2 Attribute 1 Length: 10
0002                   ///// MA 2 Attribute 1 [LNT] Line Number Table Length: 2
                       ///// MA 2 Attribute 1 [LNT] Line Number Table Begin
0000                   ////// MA 2 Attribute 1 [LNT] 1 Code reference index: 0
0003                   ////// MA 2 Attribute 1 [LNT] 1 Line Number: 3
0008                   ////// MA 2 Attribute 1 [LNT] 2 Code reference index: 8
0004                   ////// MA 2 Attribute 1 [LNT] 2 Line Number: 4
                       ///// MA 2 Attribute 1 [LNT] Line Number Table End
                       
000b                   ///// MA 2 Attribute 2 Name: LocalVariableTable
0000 000c              ///// MA 2 Attribute 2 Length: 12
0001                   ///// MA 2 Attribute 2 [LVT] Local Variable Table Length: 1
                       ///// MA 2 Attribute 2 [LVT] Local Variable Table Begin
0000                   ////// MA 2 Attribute 2 [LVT] start_pc: 0
0009                   ////// MA 2 Attribute 2 [LVT] length: 9
0010                   ////// MA 2 Attribute 2 [LVT] Name: args
0011                   ////// MA 2 Attribute 2 [LVT] Descriptor: [Ljava/lang/String;
0000                   ////// MA 2 Attribute 2 [LVT] Index: 0
                       /// Method 2 Attribute End
                       
0001                   // Class Attribute Count: 1
0012                   // Class Attribute Name: SourceFile
0000 0002              // Class Attribute Length: 2
0013                   // Class Attribute [SourceFile] Source File: HelloWorld.java

恭喜你,你现在距离成为一台JVM虚拟机更进了一步了!
现在试着自己阅读一个类文件吧。

本文首发于Lss233's.Blog(),未经作者Lss233允许严禁转载。