此文是自己學習 java Bytecode 的筆記, 包含:
- class 文件結構
- 使用 Binary 工具 (Binary Viewer/xxd) 查看 class 文件
- 借助 java 工具 (javap) 查看 class 文件
- Java bytecode instruction set
- 手動分析(反編譯)一個class文件
- 使用 java 反編譯工具 (jd-gui) 分析 class 文件
class 文件結構
建議大家可以參考 Wiki 上的 Java_class_file 來學習 class 的文件結構
Class文件是一組以8位字節(jié)為基礎單位的二進制流,各個數(shù)據(jù)項按順序緊密的從前向后排列。根據(jù)Java虛擬機規(guī)范的規(guī)定,class文件只使用兩種存儲結構:無符號數(shù)和表
- 無符號數(shù)屬于基本的數(shù)據(jù)類型,以u1、u2、u4、u8來分別代表1個字節(jié)、2個字節(jié)、4個字節(jié)、8個字節(jié)的無符號數(shù),無符號數(shù)可以用來描述數(shù)字、索引引用、數(shù)量值,或者按照UTF-8編碼構成字符串值。
- 表是由多個無符號數(shù)或者其它表作為數(shù)據(jù)項構成的復合數(shù)據(jù)類型,所有表都習慣性地以"_info"結尾。
整個class文件本質(zhì)上就是一張表,它由下表所示的數(shù)據(jù)項構成
| 類型 | 名稱 | 數(shù)量 |
|---|---|---|
| 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];
}
接著我們簡單看下各個數(shù)據(jù)項的意義
1)文件魔數(shù)
magic項為文件魔數(shù),class文件魔數(shù)為固定值 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) 常量池
我的簡單理解是:常量池表主要存放兩種類型數(shù)據(jù),一個是值,如Java中的文本字符串、被聲明為final的常量值等,另一個是名稱,如類名,變量名,函數(shù)名,變量類型,指令名等(當然這只是簡單化的記憶,準確的請參考網(wǎng)上其他文章的介紹)。
常理池表所對應的數(shù)組長度為constant_pool_count-1,從1開始計數(shù),第0個用于特殊情況下表示”不引用任何一個常量池項目“
關于常量池中各種類型的結構,請參考這篇文章 JVM-CLASS文件完全解析-常量池
4)訪問標志
access_flags,也就是我們Java中定義一個類的時候,前面的那些關鍵字 public final 等。這里直接借用網(wǎng)上( 談談Java虛擬機——Class文件結構 )的圖片,圖侵刪

例如一個 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
當前類實現(xiàn)的接口
7)methods、fields
類中的方法和成員變量
8)attributes
這里的 attributes 是 JVM 里的屬性,Java虛擬機預定了幾個屬性,依然圖侵刪

使用 binary 工具查看 class 文件
我們可以使用查看二進制文件的工具來查看 class 文件, Windows 上有 Binary Viewer, Linux 上則可以使用 xxd 命令。 建議對著“class 文件結構”表格實際操作一遍,可以加深印象。 像下面這一段是我用 xxd class_file 得到的
0000000: cafe babe 0000 0031 0068 0100 063c 696e .......1.h...<in
0000010: 6974 3e01 0028 284c 6a61 7661 2f6c 616e it>..((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,接下來兩個字節(jié)的 0068 是 constant_pool_count,也就是說總共有 103(0x0068=104) 個常量 ……
借助 javap 命令
上面使用原始 binary 工具查看并且手動分析只是學習的時候使用, 借助 JDK 中的 javap 命令, 我們可以方便的查看 class 文件。 我一般帶上 -c(反編譯class文件結構中的Code, 不加上好像只能看到函數(shù)聲明) -v(想看 constant_pool 可以加上它) -private(會把 private 方法變量等顯示出來) 這幾個選項, 具體的選項作用請使用 --help 查看
Usage: javap <options> <classes>
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 <path> Specify where to find user class files
-cp <path> Specify where to find user class files
-bootclasspath <path> Override location of bootstrap class files
Java bytecode instruction set
來到本文的重點, java 字節(jié)碼指令集, 相當于一門匯編語言, 各個指令說明參考 Wiki 文檔 Java bytecode instruction set
總的來說, 這些指令集就是對 operand stack, local variable table, constant pool 這幾個存儲數(shù)據(jù)(operand stack里也有操作碼,但是一起當成數(shù)據(jù)吧)的地方(實在想不出什么詞好)的數(shù)據(jù)進行 push/pop 操作。 下面是幾個我自己比較容易混淆的指令
| instruction | comment |
|---|---|
| <type>store_<n> | store指令一般是 operand stack ---> local variable table, 也就是把 operand stack 的值取出存到 local variable table, 取出的是 operand stack 棧頂元素, 存入的是 local variable table 的第 <n> 個元素, <type>就是要存儲的數(shù)據(jù)的類型, i表示int, l表示long, a表示objectref等等 |
| <type>load_<n> | load指令一般是 local variable table ---> operand stack, 也就是加載 local variable table 的第 <n> 個元素的值到 operand stack 棧頂 |
| <type>constant_<n> | constant指令一般是 constant pool ---> operand stack, 不過這里的 <n> 是數(shù)據(jù)常量值, 不是 index |
| dup | 這個是復制棧頂數(shù)據(jù)并且push, 看的時候發(fā)現(xiàn) new操作(數(shù)組賦值aastore之類的也是) 之后一般都要跟該指令, 看網(wǎng)上分析是說因為 new 之后JVM要進行初始化操作,該操作會消耗掉new出來的objectref但是不返回值, 如果不dup,那么 operand stack里就沒有該objectref了 |
至于其他一些 new, putfield 之類的指令應該比較好理解
實戰(zhàn)分析
平時我們可以使用 jd-gui 這個工具來查看 class 文件, 它可以幫我們反編譯大部分 class 文件。
不過這里我們還是自己手動分析一段簡單的 ByteCode以加深印象。 下面是一個使用 java 動態(tài)代理方式 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, 并且實現(xiàn)了我們自定義的接口 IActivityManager, 有一個 static 成員 java.lang.reflect.Method m1。 然后看下面初始化 static{} 部分,我們列出每條指令和執(zhí)行了該指令之后操作數(shù)棧中的情況
| Bytecode | Operand | Comments |
|---|---|---|
| 0: ldc #71 | ["java.lang.Object"] | 將常量池中第71個常量push到操作數(shù)棧,其中第71個常量為"java.lang.Object" |
| 2: invokestatic #77 | [objClassRef] | 調(diào)用第77個常量對應的靜態(tài)方法Class.forName。從前面 Wiki 里對 invokestatic 指令的說明我們知道,該指令需要消耗操作數(shù)棧頂?shù)囊粋€元素,所以pop 棧頂?shù)?java.lang.Object", 用于執(zhí)行 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] | 這里相當于執(zhí)行 clzArrayRef[0] = objClassRef2, 消耗了棧頂?shù)娜齻€元素, 但是aastore指令返回Void數(shù)據(jù),所以并不 push 任何數(shù)據(jù) |
| 19: invokevirtual #82 | [equalMethodRef] | Method equalMethodRef = objClassRef.getMethod:("equals", clzArrayRef) |
| 22: putstatic #20 | [] | m1 = equalMethodRef |
把我們自己輔助記錄用的中間量 objClassRef、 clzArrayRef、 equalMethodRef 去掉, 可以看到這段指令相當于執(zhí)行:
private static java.lang.reflect.Method m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {Class.forName("java.lang.Object")})