Java Bytecode 笔记

此文是自己学习 java Bytecode 的笔记, 包含:

  1. class 文件结构
  2. 使用 Binary 工具 (Binary Viewer/xxd) 查看 class 文件
  3. 借助 java 工具 (javap) 查看 class 文件
  4. Java bytecode instruction set
  5. 手动分析(反编译)一个class文件
  6. 使用 java 反编译工具 (jd-gui) 分析 class 文件

class 文件结构


建议大家可以参考 Wiki 上的 Java_class_file 来学习 class 的文件结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项按顺序紧密的从前向后排列。根据Java虚拟机规范的规定,class文件只使用两种存储结构:无符号数和表

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值,或者按照UTF-8编码构成字符串值。
  • 表是由多个无符号数或者其它表作为数据项构成的复合数据类型,所有表都习惯性地以"_info"结尾。

整个class文件本质上就是一张表,它由下表所示的数据项构成

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attribute_count 1
attribute_info attributes attributes_count

如果我们使用类似C语言的结构体来存储的话,一个 class 文件可以定义为:

struct Class_File_Format {
   u4 magic_number;

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

接着我们简单看下各个数据项的意义

1)文件魔数
magic项为文件魔数,class文件魔数为固定值 0XCAFEBABE。

2)版本号
minor_version 和 major_version 主要是标识该 class 文件使用的 JDK 版本号,major_version 从45开始

major_version Dec Hex
Java SE 9 53 0x35
Java SE 8 52 0x34
Java SE 7 51 0x33
Java SE 6.0 50 0x32
Java SE 5.0 49 0x31
JDK 1.4 48 0x30
JDK 1.3 47 0x2F
JDK 1.2 46 0x2E
JDK 1.1 45 0x2D

3) 常量池
我的简单理解是:常量池表主要存放两种类型数据,一个是值,如Java中的文本字符串、被声明为final的常量值等,另一个是名称,如类名,变量名,函数名,变量类型,指令名等(当然这只是简单化的记忆,准确的请参考网上其他文章的介绍)。

常理池表所对应的数组长度为constant_pool_count-1,从1开始计数,第0个用于特殊情况下表示”不引用任何一个常量池项目“

关于常量池中各种类型的结构,请参考这篇文章 JVM-CLASS文件完全解析-常量池

4)访问标志
access_flags,也就是我们Java中定义一个类的时候,前面的那些关键字 public final 等。这里直接借用网上( 谈谈Java虚拟机——Class文件结构 )的图片,图侵删

Java Bytecode 笔记_第1张图片
class_file_access_flags

例如一个 AClass 被定义为public final AClass,则它的 access_flags 的值应为:ACC_PUBLIC | ACC_SUPER | ACC_FINAL = 0x0001|0x0020|0x0010=0x0031。

5)this和super class
this_class 和 super_class ,当前类和父类的名字,指向的是一个常量池的索引

6)interfaces_count和interfaces
当前类实现的接口

7)methods、fields
类中的方法和成员变量

8)attributes
这里的 attributes 是 JVM 里的属性,Java虚拟机预定了几个属性,依然图侵删

Java Bytecode 笔记_第2张图片
class_file_attributes.jpg

使用 binary 工具查看 class 文件


我们可以使用查看二进制文件的工具来查看 class 文件, Windows 上有 Binary Viewer, Linux 上则可以使用 xxd 命令。 建议对着“class 文件结构”表格实际操作一遍,可以加深印象。 像下面这一段是我用 xxd class_file 得到的

0000000: cafe babe 0000 0031 0068 0100 063c 696e  .......1.h.....((Ljava/lan
0000020: 672f 7265 666c 6563 742f 496e 766f 6361  g/reflect/Invoca
0000030: 7469 6f6e 4861 6e64 6c65 723b 2956 0100  tionHandler;)V..

可以看到最开始是 magic number cafe babe,然后 minor_version 是 0000,major_version 是 0031,所以是JDK1.5,接下来两个字节的 0068 是 constant_pool_count,也就是说总共有 103(0x0068=104) 个常量 ……

借助 javap 命令


上面使用原始 binary 工具查看并且手动分析只是学习的时候使用, 借助 JDK 中的 javap 命令, 我们可以方便的查看 class 文件。 我一般带上 -c(反编译class文件结构中的Code, 不加上好像只能看到函数声明) -v(想看 constant_pool 可以加上它) -private(会把 private 方法变量等显示出来) 这几个选项, 具体的选项作用请使用 --help 查看

Usage: javap  
where possible options include:
  -help  --help  -?        Print this usage message
  -version                 Version information
  -v  -verbose             Print additional information
  -l                       Print line number and local variable tables
  -public                  Show only public classes and members
  -protected               Show protected/public classes and members
  -package                 Show package/protected/public classes
                           and members (default)
  -p  -private             Show all classes and members
  -c                       Disassemble the code
  -s                       Print internal type signatures
  -sysinfo                 Show system info (path, size, date, MD5 hash)
                           of class being processed
  -constants               Show final constants
  -classpath         Specify where to find user class files
  -cp                Specify where to find user class files
  -bootclasspath     Override location of bootstrap class files

Java bytecode instruction set


来到本文的重点, java 字节码指令集, 相当于一门汇编语言, 各个指令说明参考 Wiki 文档 Java bytecode instruction set

总的来说, 这些指令集就是对 operand stack, local variable table, constant pool 这几个存储数据(operand stack里也有操作码,但是一起当成数据吧)的地方(实在想不出什么词好)的数据进行 push/pop 操作。 下面是几个我自己比较容易混淆的指令

instruction comment
store_ store指令一般是 operand stack ---> local variable table, 也就是把 operand stack 的值取出存到 local variable table, 取出的是 operand stack 栈顶元素, 存入的是 local variable table 的第 个元素, 就是要存储的数据的类型, i表示int, l表示long, a表示objectref等等
load_ load指令一般是 local variable table ---> operand stack, 也就是加载 local variable table 的第 个元素的值到 operand stack 栈顶
constant_ constant指令一般是 constant pool ---> operand stack, 不过这里的 是数据常量值, 不是 index
dup 这个是复制栈顶数据并且push, 看的时候发现 new操作(数组赋值aastore之类的也是) 之后一般都要跟该指令, 看网上分析是说因为 new 之后JVM要进行初始化操作,该操作会消耗掉new出来的objectref但是不返回值, 如果不dup,那么 operand stack里就没有该objectref了

至于其他一些 new, putfield 之类的指令应该比较好理解

实战分析


平时我们可以使用 jd-gui 这个工具来查看 class 文件, 它可以帮我们反编译大部分 class 文件。

不过这里我们还是自己手动分析一段简单的 ByteCode以加深印象。 下面是一个使用 java 动态代理方式 Proxy.newProxyInstance 生成的类, javap 反编译后,取出其中一小段

public final class com.sun.proxy.$Proxy0 extends java.lang.reflect.Proxy implements com.haha.cfs.IActivityManager
......
Constant pool:
......
   #18 = Class              #17           //  com/sun/proxy/$Proxy0
   #19 = NameAndType        #9:#10        //  m1:Ljava/lang/reflect/Method;
   #20 = Fieldref           #18.#19       //  com/sun/proxy/$Proxy0.m1:Ljava/lang/reflect/Method;
......

  private static java.lang.reflect.Method m1;
    flags: ACC_PRIVATE, ACC_STATIC

......
  static {} throws ;
    flags: ACC_STATIC
    Code:
      stack=10, locals=2, args_size=0
         0: ldc           #71                 // String java.lang.Object
         2: invokestatic  #77                 // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
         5: ldc           #78                 // String equals
         7: iconst_1      
         8: anewarray     #73                 // class java/lang/Class
        11: dup           
        12: iconst_0      
        13: ldc           #71                 // String java.lang.Object
        15: invokestatic  #77                 // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
        18: aastore       
        19: invokevirtual #82                 // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
        22: putstatic     #20                 // Field m1:Ljava/lang/reflect/Method;

这里我们可以看到, 生成的class是$Proxy0, 它继承自 java.lang.reflect.Proxy, 并且实现了我们自定义的接口 IActivityManager, 有一个 static 成员 java.lang.reflect.Method m1。 然后看下面初始化 static{} 部分,我们列出每条指令和执行了该指令之后操作数栈中的情况

Bytecode Operand Comments
0: ldc #71 ["java.lang.Object"] 将常量池中第71个常量push到操作数栈,其中第71个常量为"java.lang.Object"
2: invokestatic #77 [objClassRef] 调用第77个常量对应的静态方法Class.forName。从前面 Wiki 里对 invokestatic 指令的说明我们知道,该指令需要消耗操作数栈顶的一个元素,所以pop 栈顶的"java.lang.Object", 用于执行 Class objClassRef = Class.forName("java.lang.Object"), 然后把结果 objClassRef push 回栈顶
5: ldc #78 [objClassRef] ["equals"]
7: iconst_1 [objClassRef] ["equals"] [1]
8: anewarray #73 [objClassRef] ["equals"] [clzArrayRef] 其中 Class[] clzArrayRef = new Class[1]
11: dup [objClassRef] ["equals"] [clzArrayRef] [clzArrayRef]
12: iconst_0 [objClassRef] ["equals"] [clzArrayRef] [clzArrayRef] [0]
13: ldc #71 [objClassRef] ["equals"] [clzArrayRef] [clzArrayRef] [0] ["java.lang.Object"]
15: invokestatic #77 [objClassRef] ["equals"] [clzArrayRef] [clzArrayRef] [0] [objClassRef2] 其中 Class objClassRef2 = Class.forName("java.lang.Object")
18: aastore [objClassRef] ["equals"] [clzArrayRef] 这里相当于执行 clzArrayRef[0] = objClassRef2, 消耗了栈顶的三个元素, 但是aastore指令返回Void数据,所以并不 push 任何数据
19: invokevirtual #82 [equalMethodRef] Method equalMethodRef = objClassRef.getMethod:("equals", clzArrayRef)
22: putstatic #20 [] m1 = equalMethodRef

把我们自己辅助记录用的中间量 objClassRef、 clzArrayRef、 equalMethodRef 去掉, 可以看到这段指令相当于执行:
private static java.lang.reflect.Method m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {Class.forName("java.lang.Object")})

你可能感兴趣的:(Java Bytecode 笔记)