ByteScope

Java class 檔案格式

編譯後的 Java bytecode,由 JVM 執行。

MIME 類型
application/java-vm
副檔名
.class
分類
執行檔與位元碼

Magic number(檔案簽章)

0x00000000CAFEBABE····'CAFEBABE' magic

關於 Java class 檔格式

.class 檔案是一個編譯後的 Java 型別——class、interface、enum 或 record——放在 JVM 自己的容器格式裡,而它幾乎整份都是同一個結構:constant pool。類別名稱、方法名稱、欄位描述子、字串字面值和每一個符號參照都以編號項目的形式住在裡面,檔案其他部分則用索引而不是文字去引用它們。這解釋了為什麼 class 檔在 JAR 裡這麼好壓、為什麼改一個類別名稱會讓檔案到處的位元組都變、以及為什麼 constant pool 之後每個位移都是浮動的:pool 的長度完全取決於這個型別提到了多少名稱。

檔頭結構,一個位元組一個位元組看

位移長度欄位意義
04magicCA FE BA BE,是 James Gosling 在 Java 還沒有名字之前選的,而且尷尬地和 Apple 的 universal binary 撞號。它是整個檔案裡唯一數值固定的欄位。
42minor_version幾乎永遠是 0。65535 這個值有特殊意義:這個類別使用了預覽功能,只會在編譯它的那個 JDK 上載入——預覽功能的類別明明主版本相符卻載不起來,真正的解釋就是這個。
62major_versionclass 檔版本,也是每一次 UnsupportedClassVersionError 的來源。從 Java 5 起它就是功能版本號加 44——Java 8 是 52、Java 17 是 61、Java 21 是 65——執行環境在讀任何其他東西之前,先拿這兩個位元組和自己的上限比。
82constant_pool_countpool 項目數再加一,因為 pool 是從 1 開始編號的,索引 0 保留。一個小工具類別大概幾十個;大型框架類別可以逼近 65535 的上限,而產生出來的程式碼有時真的會撞到。
10variableconstant_pool項目一個接一個,每個以 1 位元組的 tag 開頭、由 tag 決定長度:tag 1 是帶 2 位元組長度前綴的 UTF-8 字串、tag 7 是類別參照、tag 10 是方法參照、tag 8 是字串。tag 5 和 6(long 與 double)各佔掉兩個索引格——這條規則搞壞的手寫解析器,比這個格式其他任何部分都多。
variable2access_flagspool 之後的第一個欄位。0x0001 public、0x0010 final、0x0200 interface、0x4000 enum,而 0x0020(ACC_SUPER)從 1997 年以後編譯的東西幾乎都會設,用來取得舊有的 invokespecial 語意——一個單純的 public class 通常讀到 0x0021。
variable2this_class指向「指名這個型別」的那個 pool 項目的索引,用的是內部形式,分隔符號是斜線不是點。它必須和這個檔案在 JAR 裡的路徑一致,所以把磁碟上的 .class 改名會得到一個同時列出兩種寫法的 NoClassDefFoundError。
variable2super_class父類別項目的索引,通常是 java/lang/Object。只有 java/lang/Object 自己是 0,全世界唯一沒有父類別的類別。
variable2interfaces_count後面接著幾個直接父介面,每個是一個 2 位元組的 pool 索引。再來是欄位、方法與類別屬性,每一個都帶長度前綴,所以從 pool 之後整份檔案只能循序讀——這裡沒有目錄可查。

怎麼跟長得像的格式分開

容易搞混的怎麼分
Mach-O(universal/fat)兩者開頭都是 CA FE BA BE。去讀位元組 4-7:class 檔是 minor_version 接 major_version,major_version 至少是 45(Java 1.1),目前落在六十幾到七十幾;universal binary 那個位置放的是 nfat_arch,一個小小的架構數量,高位那半是 0。位元組 6-7 讀到大於 44 的非零值,就是 class 檔。
Java JARJAR 的位移 0 不會是 class 檔;它是 ZIP,開頭 50 4B 03 04,裡面裝著壓縮過的 class 檔。在 JAR 的位元組裡看到 CA FE BA BE 是理所當然的——只是它不在最前面,而真正用來辨識這個壓縮檔的是中央目錄裡的項目名稱。
Android DEXAndroid 的工具鏈會先編譯出 .class 再把它們丟掉:d8 把它們合併成 DEX 檔,開頭是 'dex' 加一個換行,用的是暫存器式的指令集,不是 JVM 的堆疊機器。所以 APK 裡完全沒有 .class 項目,這也是分辨 APK 與「剛好以 Android 為目標的 JAR」最快的方法。
Java source (.java)會特別提,是因為建置目錄裡兩者混在一起:.java 是純文字,而 .class 開頭那個位元組根本不是合法的 UTF-8 文字。任何工具打開 .class 之後能顯示出可讀的類別與方法名稱,讀的是 constant pool 裡的 UTF-8 項目,不是原始碼。

常見的壞法

  • major_version 比執行環境還新。這根本不是損壞,卻是大家最常在看的東西:JVM 讀位元組 6-7,發現版本超過自己的上限,在解析任何其他東西之前就丟出 UnsupportedClassVersionError。用 release 21 編譯、拿到 Java 17 上跑,得到的就是這個。
  • 被截斷。因為 constant pool 之後的一切都是帶長度前綴、循序排列的,被切掉的 class 檔會在它當時正讀到的那個結構上丟出 ClassFormatError,而訊息很少指得到真正的原因。「檔案短了一截」這件事比那句訊息好判斷得多。
  • constant pool 裡有位元組被改過。把字串字面值改成不同長度卻沒有修正它那 2 位元組的長度前綴,後面每個項目就全部失去同步,解析器會從某個名稱的中間讀出一個 tag,然後報未知的常數 tag。這就是為什麼要修補 class 檔得用函式庫,不能用 hex 編輯器。
  • 文字模式傳輸。老舊建置管線裡的 FTP 老問題到現在還碰得到 class 檔:CR LF 轉換讓第一次出現之後的東西全部位移,而因為 constant pool 是一串帶長度前綴的項目,失敗會表現成一個莫名其妙的 tag,而不是乾脆的 magic 不符。

這個格式現在還在用嗎

class 檔是 JVM 真正的介面:Java、Kotlin、Scala、Groovy 和 Clojure 都編譯到它,所以它們是同一個生態系而不是五個;而這個格式的穩定性,正是 2005 年編譯出來的程式庫今天還能和新編的程式碼連結的原因。它很少被單獨處理——class 都住在 JAR、WAR、模組映像與容器層裡——但每一套 bytecode 工具、混淆器、覆蓋率代理程式,以及在執行期產生代理類別的框架,實際操作的就是它。Android 是顯眼的例外:它在建置期吃掉 class 檔,出貨時換成 DEX。對拿著 hex 編輯器的人來說,好用的性質是 class 檔在很大程度上會自我描述:constant pool 把這個型別碰到的每個類別、方法與欄位都拼了出來,所以不用反編譯就讀得出它的相依性。

在這個站上打開 Java class 檔

這些全部在你的瀏覽器裡跑,檔案不會離開你的電腦。

相關格式

容易和 Java class 檔搞混的格式,可以比對它們的開頭位元組。

格式開頭位元組副檔名
Java JAR50 4B 03 04.jar
Mach-O(universal/fat)CA FE BA BE.dylib
Android DEX64 65 78 0A.dex

常見問題

檔案的 magic number 是什麼?

檔案開頭的前幾個位元組,不管檔名寫什麼,它標明的是真正的格式。PNG 一定以 89 50 4E 47 開頭,ZIP 則是 50 4B 03 04。作業系統、瀏覽器和 file 指令讀的都是這幾個位元組,而不是相信副檔名 — 這就是為什麼把檔案改名並不會改變它實際上是什麼。

我要怎麼自己檢查 magic number?

在 macOS 或 Linux 上,xxd -l 16 檔名 會印出前 16 個位元組;在 Windows 上,PowerShell 的 Format-Hex -Path 檔名 -Count 16。或者把檔案丟進本站的檔案類型辨識工具,它會在本機讀取位元組並告訴你是什麼格式,不會上傳任何東西。

為什麼兩種格式的 magic number 會一樣?

因為其中一種是建立在另一種之上。docx、xlsx、pptx、apk、jar 與 epub 骨子裡都是 ZIP 壓縮檔,所以全都以 ZIP 的簽章開頭;要分辨它們得看裡面的項目。Java class 檔和 macOS 的通用二進位檔都以 CA FE BA BE 開頭,靠緊接其後的欄位區分。

有完全沒有 magic number 的檔案嗎?

有。XML、HTML、JSON、SVG、CSV 這類以文字為基礎的格式沒有固定的位元組簽章,所以改用結構來辨識:開頭的 <?xml 宣告、<svg 元素,或是 { 後面接著帶引號的鍵。這種偵測是合理推測而非確定,因此本參考頁會把這些格式標示為結構偵測。

magic number 相符就代表檔案安全嗎?

不代表。它告訴你的是格式,不是內容是否無害 — 一個貨真價實的 PDF 或 docx 仍然可能夾帶惡意內容。辨識真實類型是有用的第一道檢查,尤其在副檔名和位元組不一致的時候,但它不是惡意軟體掃描。

手上有檔案可能是 Java class 檔?用檔案類型辨識工具查出來 — 它在本機讀取 magic number,不上傳任何東西。或者瀏覽所有格式。