💻

ChatGPTにJavaプログラムの逆コンパイルをさせてみた

2025/01/29に公開

はじめに

こんにちは、水島です。今回はふと思いついた一発ネタを投稿させてもらいたいと思います。昨今、どこを見ても大規模言語モデル(Large Language Model)の話題でもちきりです。その上、数ヶ月前に最高性能だったモデルがあっという間に陳腐化したり、新しいアプローチが毎日のようにarXivに投稿されたりと非常に忙しない昨今です。

そんなLLM真っ盛りのとある朝、お風呂に入りながら、私はふと考えていました。

「そういえば、LLMに逆コンパイルさせてみたらどうなるんだろう?」と。

これまでトランスパイルをはじめとしたプログラミング言語処理をLLMに色々やらせてはきましたが、考えてみたら逆コンパイルは今までやったことがなかったのでした。思いついたら吉日ということで、Javaプログラムから生成されたクラスファイルの逆アセンブルを元に、元のJavaプログラムを復元できるか試してみました。

使ったのはWeb版のChatGPT(GPT-4o)で、実験した日付は2025年1月29日(水)です。

お手並み拝見 - フィボナッチ関数

これくらいならたぶんいけるよね、ということでまずは手始めのフィボナッチ関数。再帰関数の例題でよくあるやつです。

Compiled from "Fib.java"
public class Fib {
  public Fib();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static int fib(int);
    Code:
       0: iload_0
       1: iconst_2
       2: if_icmpge     7
       5: iconst_1
       6: ireturn
       7: iload_0
       8: iconst_1
       9: isub
      10: invokestatic  #7                  // Method fib:(I)I
      13: iload_0
      14: iconst_2
      15: isub
      16: invokestatic  #7                  // Method fib:(I)I
      19: iadd
      20: ireturn

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: bipush        10
       5: invokestatic  #7                  // Method fib:(I)I
       8: invokedynamic #19,  0             // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
      13: invokevirtual #23                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      16: return
}

クラスファイルの逆アセンブルはjavapコマンドでできますが詳細は割愛します。さて、このテキストに対してCould you decompile it? Note that you must not provide extra explanation: <ファイル>という形のプロンプトを発行してみました。

GPT-4oの回答は以下でした。

Here is the decompiled Java code for your given bytecode:

public class Fib {
    
    public Fib() {
        super();
    }

    public static int fib(int n) {
        if (n < 2) {
            return 1;
        }
        return fib(n - 1) + fib(n - 2);
    }

    public static void main(String[] args) {
        System.out.println("Fibonacci of 10: " + fib(10));
    }
}

This code represents a simple recursive Fibonacci number generator, printing the result of fib(10).

fibの実装は正しいのですが、mainにもともとない文字列が紛れ込んでしまっています。この結果は少々予想外でした。機械語の逆コンパイルならともかくクラスファイルのアセンブリ言語を逆コンパイルするくらい難なくこなすと思っていたのです。

さて、fibのロジックは再現できているのにmainの簡単な処理をミスる理由は……と考えると思い当たるのはinvokedynamic命令です。ここに"fib(10) = "相当の情報が書かれていないのでミスってしまったのではないかということですね。

というわけで、javap -c -verbose(クラスファイルの詳しい情報まで出力する)を使って再度トライしてみました。元のデータはかなり長くなるのでこちらから参照してください。

これを使って再度トライしてみると、

Here is the decompiled Java code for Fib.class:

public class Fib {
    public Fib() {
    }

    public static int fib(int n) {
        if (n < 2) {
            return 1;
        }
        return fib(n - 1) + fib(n - 2);
    }

    public static void main(String[] args) {
        System.out.println("fib(10) = " + fib(10));
    }
}

これは元のコードと同じです(デフォルトコンストラクタにおけるsuper()の有無は意味を変えないので無視していいでしょう)。考えてみれば当たり前の話ですが、入力データにない情報をLLMが復元できるわけもなく、逆に必要な情報を提供してあげればいい話だったということですね。

もうちょっと複雑な例を - 数当てゲーム

この例は現在の高性能LLMにとってちょっと簡単過ぎたかもしれません。ということでもうちょっと難易度をあげてみましょう。次に逆コンパイルしてもらうのは数当てゲームです。1から100の間の数が隠されていて、ユーザーがある数Nを推測したときに、

  • もっと大きい
  • もっと小さい
  • 正解

のいずれかを表示して、ユーザーができるだけ少ない回数で正解に到達できるようにするゲームです。

これもロジックは逆コンパイルできたものの、文字列関連でinvokedynamicを使っていて、そのままだと文字列情報が隠れてしまったので、同様に-verboseをつけて逆アセンブルしました。

GPT-4oの回答は以下です:

import java.util.Random;
import java.util.Scanner;

public class HighAndLow {
    public HighAndLow() {
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Random random = new Random();
        int target = random.nextInt(100) + 1;
        int attempts = 0;

        System.out.println("数当てゲームを開始します!1 から 100 の間の数を当ててください。");

        while (true) {
            System.out.print("あなたの推測: ");
            int guess = scanner.nextInt();
            attempts++;

            if (guess < target) {
                System.out.println("もっと大きいです!");
            } else if (guess > target) {
                System.out.println("もっと小さいです!");
            } else {
                System.out.println("正解!" + attempts + " 回の試行で当たりました。");
                break;
            }
        }

        scanner.close();
    }
}

これもやはりそのまま動きます。ちなみに、ローカル変数名などはデバッグ情報としてクラスファイルに埋め込まれているのですが、それらのメタ情報も活用して逆コンパイルしているのはさすがですね。

これならどうだ - Quine

Quine(クワイン)はプログラムの一種で、自身のソースコードと完全に同じ文字列を出力するプログラムのことです。Quineのコンテストなどもあるくらいなので、知っている方も意外と多いかもしれません。

とはいえ、自分でQuineのプログラムを考えるのは骨が折れるので、MITライセンスで配布されているjava-quineから例を拝借することにしました。

これをコンパイルして逆アセンブルしたものがこちら、GPT-4oの回答は以下です:

class S {
    static String s;

    static {
        s = "class S{public static void main(String[]a){System.out.print((s+=(char)34)+s+';'+'}');}static String s=";
    }

    public static void main(String[] a) {
        System.out.print((s += (char) 34) + s + ';' + '}');
    }
}

出力文字列が同じという意味では「正しい」のですが、Quineの条件を満たせていません。そんなことはどこにも書いていないのだから当然といえば当然なのですが。

というわけで、Quineであることを示唆してこの逆コンパイル結果がどうなるかを見て見ましょう。

プロンプトは

This program must be quine. Could you rewrite it ?

です。GPT-4oからの回答は……

Yes! Your original Java program is intended to be a quine, meaning it prints its own source code. However, it seems slightly off. Below is a corrected and properly formatted quine in Java:

class S {
    static String s = "class S {\n    static String s = %c%s%c;\n\n    public static void main(String[] a) {\n        System.out.printf(s, 34, s, 34);\n    }\n}";

    public static void main(String[] a) {
        System.out.printf(s, 34, s, 34);
    }
}

さて、コードとしては実行できそうですが、果たしてQuineになっているのでしょうか。というわけで、コンパイル&実行してできたファイルとのdiffをとってみることにしました。

$ diff -u S.java T.java # T.javaはSの実行結果
--- S.java	2025-01-29 13:47:38
+++ T.java	2025-01-29 13:48:09
@@ -1,7 +1,13 @@
 class S {
-    static String s = "class S {\n    static String s = %c%s%c;\n\n    public static void main(String[] a) {\n        System.out.printf(s, 34, s, 34);\n    }\n}";
+    static String s = "class S {
+    static String s = %c%s%c;
 
     public static void main(String[] a) {
         System.out.printf(s, 34, s, 34);
     }
-}
+}";
+
+    public static void main(String[] a) {
+        System.out.printf(s, 34, s, 34);
+    }
+}

悲しいことにQuineにはなっていないようです。また、出力されたT.javaをよく見ると文字列リテラルが閉じられておらず、これをさらにコンパイルしようとした時点で詰まってしまいます。

Quineは「ソースコードのレイアウトそれ自体もプログラムの仕様の一部である」という特殊なプログラムなので、GPT-4oにはまだハードルが高かったのかもしれません。

まとめ

GPT-4oにクラスファイル(逆アセンブルされたもの)の逆コンパイルをしてもらうという実験をしたわけですが、最近のLLMならこれくらいできるよねというのを予想通りこなしてきて、さすがという印象です。

Quineにはまだ歯が立たないようですが、ひょっとしたらo1やo1 pro、DeepSeek-R1のような、より高度なモデルなら解ける可能性もあります。

ともあれ、ますます賢くなっていくLLMから今後も目が離せないのは間違いありません。

nextbeat Tech Blog

Discussion