準備 — サンプル .class ファイルを用意する
まず逆コンパイルの練習用に、シンプルな Java クラスをコンパイルして .class を作る。
package com.example;
import java.util.List;
import java.util.stream.Collectors;
public class Sample {
private final String name;
private int count;
public Sample(String name, int count) {
this.name = name;
this.count = count;
}
public String getName() { return name; }
public int getCount() { return count; }
/** 偶数のみフィルタして大文字に変換する */
public static List<String> filterAndUpper(List<String> items) {
return items.stream()
.filter(s -> s.length() % 2 == 0)
.map(String::toUpperCase)
.collect(Collectors.toList());
}
@Override
public String toString() {
return "Sample{name='" + name + "', count=" + count + "}";
}
}
# ディレクトリ作成
mkdir -p src/com/example out
# ソースを保存
# src/com/example/Sample.java に上記コードを記述
# デバッグ情報付きでコンパイル(通常の開発ビルド相当)
javac -g -d out src/com/example/Sample.java
# 確認
ls out/com/example/
# Sample.class Sample$1.class(ラムダの匿名クラスが生成される場合がある)
CFR の基本コマンド
# 基本:標準出力に表示
java -jar cfr.jar out/com/example/Sample.class
# ファイルに保存(--outputdir で出力先を指定)
java -jar cfr.jar out/com/example/Sample.class --outputdir decompiled/
# 出力先のファイルを確認
cat decompiled/com/example/Sample.java
# コメントを省いてすっきりした出力にする
java -jar cfr.jar out/com/example/Sample.class \
--comments false \
--showversion false \
--outputdir decompiled/
出力結果の見方
CFR の出力例を示す。元のソースとほぼ同じ構造で復元されている。
CFR 出力(Sample.java)
/*
* Decompiled with CFR 0.152.
*/
package com.example;
import java.util.List;
import java.util.stream.Collectors;
public class Sample {
private final String name;
private int count;
public Sample(String name, int count) {
this.name = name;
this.count = count;
}
public String getName() {
return this.name;
}
public int getCount() {
return this.count;
}
public static List<String> filterAndUpper(List<String> items) {
return items.stream()
.filter(s -> s.length() % 2 == 0)
.map(String::toUpperCase)
.collect(Collectors.toList());
}
@Override
public String toString() {
return "Sample{name='" + this.name + "', count=" + this.count + "}";
}
}
元のソースと比べると:
- フィールド名・メソッド名・型情報はそのまま復元されている(デバッグ情報ありのため)
- ラムダ式はほぼ元の形で復元されている
- Javadoc コメントは消えている(バイトコードには含まれない)
this.が補完されるなど、わずかに字句が異なる部分がある
デバッグ情報あり・なしの違い
コンパイル時に -g を付けるとデバッグ情報(LineNumberTable・LocalVariableTable)が .class に含まれる。
リリースビルドではしばしばこの情報が省かれており、変数名の復元に影響する。
# デバッグ情報なし(-g:none)
javac -g:none -d out_nodebug src/com/example/Sample.java
# 逆コンパイル
java -jar cfr.jar out_nodebug/com/example/Sample.class
デバッグ情報なし — メソッド引数の比較
// デバッグ情報あり
public Sample(String name, int count) {
this.name = name;
this.count = count;
}
// デバッグ情報なし(引数名が復元できない)
public Sample(String string, int n) {
this.name = string;
this.count = n;
}
💡 変数名が復元できないとき
引数名が string/n/param0 などになる場合は、メソッドの処理内容・型情報・呼び出し元から意味を推測する。IDE の "Rename" 機能でリネームしながら読み進めると効率的。
javap でバイトコードを直接確認する
逆コンパイル結果が読みにくい場合は、JDK 付属の javap でバイトコードを直接確認することで手がかりが得られる。
# クラスの構造のみ表示(public メソッド・フィールド)
javap out/com/example/Sample.class
# private も含めてすべて表示
javap -p out/com/example/Sample.class
# バイトコード命令を表示(詳細解析用)
javap -c out/com/example/Sample.class
# 定数プール・デバッグ情報も含む完全表示
javap -v -p out/com/example/Sample.class
javap -p の出力例(抜粋)
Compiled from "Sample.java"
public class com.example.Sample {
private final java.lang.String name;
private int count;
public com.example.Sample(java.lang.String, int);
public java.lang.String getName();
public int getCount();
public static java.util.List filterAndUpper(java.util.List);
public java.lang.String toString();
}
Procyon / jadx との出力比較
同じ Sample.class を 3 ツールで逆コンパイルした場合の主な差異をまとめる。
| 項目 | CFR | Procyon | jadx |
|---|---|---|---|
| ラムダ式の復元形式 | ほぼ元の形 | 最も元に近い | ほぼ元の形 |
メソッド参照(::) |
復元される | 復元される | ラムダに展開される場合あり |
| コンパイラコメント | あり(--comments false で非表示) |
あり | なし |
| try-with-resources | 復元される | 復元される | 復元される |
| エラー時の出力 | 部分的に出力して継続 | エラー箇所を /* ERROR */ として出力 |
コメントで警告 |
よくある問題と対処
| 問題 | 原因 | 対処 |
|---|---|---|
UnsupportedClassVersionError |
CFR の動作 JVM より新しいバイトコード | JDK を最新版にアップデート |
| 出力が空 / エラーのみ | 難読化・暗号化されている場合 | PART 06 参照 |
| 内部クラスが別ファイルになる | Outer$Inner.class として存在する |
ディレクトリ全体を指定して一括処理する(PART 04) |
| 日本語文字列が化ける | コンソールのエンコーディング不一致 | -Dfile.encoding=UTF-8 を JVM 引数に追加 |
✅ 次の PART では…
PART 04 では JAR ファイルから全クラスを一括逆コンパイルする手順を解説する。CFR でのバッチ処理と jadx GUI の操作フローの両方を扱う。