準備 — サンプル .class ファイルを用意する

まず逆コンパイルの練習用に、シンプルな Java クラスをコンパイルして .class を作る。

Java — サンプルソース(Sample.java)
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 + "}";
    }
}
Shell — コンパイル
# ディレクトリ作成
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 の基本コマンド

Shell — 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 に含まれる。 リリースビルドではしばしばこの情報が省かれており、変数名の復元に影響する。

Shell — デバッグ情報なしでコンパイル
# デバッグ情報なし(-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 でバイトコードを直接確認することで手がかりが得られる。

Shell — 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 ツールで逆コンパイルした場合の主な差異をまとめる。

項目CFRProcyonjadx
ラムダ式の復元形式 ほぼ元の形 最も元に近い ほぼ元の形
メソッド参照(:: 復元される 復元される ラムダに展開される場合あり
コンパイラコメント あり(--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 の操作フローの両方を扱う。

→ PART 04 — JAR から一括抽出へ