LLMデコンパイラは「コンパイルが通る」ほど正しくない——再コンパイル可能性が隠す挙動乖離と、脆弱性が「消える」問題

LLMデコンパイラは「ビルドが通る・同梱テストが通る」だけで評価されていますが、新研究はビルド率が上がるほど挙動一致率が下がる逆説を実証。公開済み脆弱性の最大1割がデコンパイルで「消える」問題を、Decompile-Divergeの仕組みとともに解説します。

LLMデコンパイラは「コンパイルが通る」ほど正しくない——再コンパイル可能性が隠す挙動乖離と、脆弱性が「消える」問題

対象論文: When LLM Decompilers Recompile More and Preserve Less(arXiv:2609.05370v1, cs.CR, 2026-09-04)

デコンパイルとは何か——脆弱性解析の「翻訳機」が抱える本質的な制約

デコンパイル(逆コンパイル)とは、コンパイル済みの機械語バイナリから高水準言語(主にC風のソースコード)を復元する技術です。脆弱性検出や既知バグのトリアージ、レガシーシステムの保守、マルウェア解析のいずれも、まずバイナリを「読める形」に戻すこの作業が第一歩になります。復元されたコードは、実コードベースへの組み込みやファジングの入力生成にも再利用されるため、その品質が解析全体の成否を左右します。

伝統的なデコンパイラである Ghidra や Hex-Rays(IDA Pro のプラグイン)は、解析を解決できない箇所を可視のプレースホルダーとして残すのが特徴です。型が不明なら undefined4 や _DWORD、変数名が推測できなければ uVar1 や DAT_* といった仮名が現れ、制御フローは goto の多用で表現されます。その結果、伝統的ツールの出力は「そのままコンパイルできない疑似コード」になることが多くあります。しかし逆に言えば、どこが分かっていてどこが分からないのかが目に見えるという利点があります。

flowchart LR
    A[機械語バイナリ] --> B[伝統的デコンパイラ<br/>Ghidra / Hex-Rays]
    B --> C[疑似コード]
    C --> D["不明箇所を可視化<br/>(undefined4, _DWORD, uVar1)"]
    C --> E["慣用的なCとして再コンパイル<br/>(多くは不可)"]
    style D fill:#fdd,stroke:#c00
    style E fill:#dfd,stroke:#0a0

ここで押さえておきたいのは、デコンパイラが行うのは「逆変換」ではなく、情報が失われた後の推測の復元だという点です。コンパイル過程で変数名・型情報・コメントは原則として捨てられるため、デコンパイラは文脈からそれらを推定するしかありません。この「推測」が後の章で見る挙動の乖離(デコンパイル結果が元のプログラムと異なる振る舞いをする現象)の温床になります。伝統的なツールは推測できない部分を正直に「不明」と表示することで、解析者が誤解するリスクを抑えてきました。ところが次章で説明するように、LLMベースのデコンパイラはこの「正直さ」を大きく変えてしまいます。

LLMデコンパイラの登場——「見せる不明」から「隠す発明」へ

2024年以降、大規模言語モデル(LLM)を使ったデコンパイラが急速に発展し、クリーンで慣用的なCコードを生成できるようになりました。LLMデコンパイラは大きく2系統に分けられます。1つはリファインメント型で、Ghidra など伝統的デコンパイラの出力をフロントエンドとして使い、それをLLMで洗練してコンパイル可能なCに書き換えます。代表例は LLM4Decompile、SK2Decompile、Idioms、DeGPT です。もう1つはエンドツーエンド型(E2E)で、アセンブリコードから直接Cを生成します。Nova、SLaDe、AutoDecompiler、GLM-5.2 が該当します。

系統 方式 代表システム
リファインメント型 伝統的デコンパイラの出力をLLMで洗練 LLM4Decompile / SK2Decompile / Idioms / DeGPT
エンドツーエンド型(E2E) アセンブリから直接Cを生成 Nova / SLaDe / AutoDecompiler / GLM-5.2

ところが、こうしたLLMデコンパイラの評価方法には重大な偏りがあります。現在の評価はほぼ再コンパイル可能性(recompilability)と再実行可能性(re-executability)だけで判定されているのです。つまり「生成したCがビルドを通ること」と「同梱されたI/Oテストをパスすること」だけを見て、正しさを測っています。代表的なベンチマーク(HumanEval-Decompile、ExeBench、AnghaBench、MBPPのC変換版)は、いずれも短い関数・平坦なスカラー引数・まばらなI/Oアサーションを中心に構成されており、関数の挙動全体をカバーしていません。

なぜこの評価が危険なのでしょうか。同梱テストのカバレッジが狭すぎると、挙動の一致(behavioral agreement)、すなわち「どんな入力でも元の関数と同じ結果を返すか」を保証できないからです。さらに学習目標自体にも問題があります。LLM4Decompile は参照トークン列への next-token 一致(表面的な類似度)を最適化し、SK2Decompile は「コンパイルできるか+プレースホルダー識別子のJaccard一致」という構造報酬を使っています。どちらの目的関数も、「テストで見せなかった入力での挙動一致」を直接は測っていません。つまり「ビルドが通る=正しく復元できている」という評価指標は、LLMに「挙動を保つ」代わりに「コンパイルを通す」方向を学習させている可能性が高いのです。次章では、この落とし穴が実際の関数でどう顕在化するかを、具体的な数値で見ていきます。

「再コンパイルできる=正しい」はなぜ危険か——ExeBenchのdecode6が示す落とし穴

「再コンパイルできる」ことと「正しく復元できている」ことがどれだけ乖離しうるか、本論文が提示する最も印象的な具体例が ExeBench の decode6 関数です。decode6 は幅6の列バンドと可変幅の最終バンドに対して線形インデックスを写像する関数で、バンド幅の分岐処理を含みます。この関数に同梱されているI/Oテストはわずか10ペアで、引数はすべて0〜124の狭い範囲に限定されており、最終バンドの分岐もオーバーフローも一切踏まない範囲しか検証していません。

この10ペアの同梱テストを、3つのデコンパイル手法がすべて10/10でパスした上で、論文側が動的に生成した100個の拡張入力で検証した結果が下表です。

手法 同梱テスト(10件) 拡張入力100件での一致率 主な失敗モード
raw Hex-Rays 10/10 100/100 参照の演算を忠実に保持
SK2Decompile 10/10 70/100 固定ストライド6と可変バンド幅Dを混同し (100,16,8) で (8,-2) を出力(参照は (12,6))
LLM4Decompile 10/10 66/100 バンド判定の除算を乗算に書き換え64ビットオーバーフローを導入し、(0,10^10,10^9) で (≈10^10, -2×10^18) を出力

3つのシステムすべてが「再コンパイル成功+同梱テスト10/10パス」を満たしているにもかかわらず、拡張入力では一致率が66〜70%にまで落ち込んでいます。しかもSK2Decompileの失敗は出力が負の座標になるような明らかな誤りであり、LLM4Decompileの失敗は64ビット整数のオーバーフローという深刻なバグの持ち込みです。同梱テストだけを見ていれば、これらの問題は一切検出できません。

flowchart LR
    subgraph 同梱テスト
        A1[10ペア<br/>引数0〜124のみ]
        A2[全部パス ✅]
    end
    subgraph 拡張入力100件
        B1[未踏の分岐・<br/>オーバーフローを含む]
        B2[Hex-Rays 100 ✅<br/>SK2Decompile 70 ❌<br/>LLM4Decompile 66 ❌]
    end
    A1 --> A2 --> C[評価は「正しい」と判定]
    B1 --> B2 --> D[実際は乖離]
    style A2 fill:#dfd,stroke:#0a0
    style B2 fill:#fdd,stroke:#c00

論文では、全候補のリプレイにより「同梱スイートは関数挙動の乖離の3〜45パーセントを捕捉できない」ことも示されています。つまり同梱テストを全部パスしたからといって、関数が正しく復元できている証拠にはなりません。次章で説明する Decompile-Diverge は、この「固定テストの盲点」をなくすための新しい評価方法です。

Decompile-Diverge——固定テストに頼らない挙動比較オラクルの仕組み

Decompile-Diverge は、手書き・固定のテストを使わずに関数ごとに自動で挙動比較オラクルを構築する評価手法です。「同梱テストの範囲外の入力で、デコンパイル結果が参照の挙動からどれだけ乖離するか」を系統的に測れます。Ghidra や Hex-Rays のような従来手法もLLMベースの手法も、同じオラクルで比較できるのが特徴です。

パイプラインは次の5段階で構成されます。

flowchart TD
    A[参照C関数] --> B[コンパイル<br/>参照バイナリ]
    B --> C[AFL++でファジング<br/>入力コーパスを動的成長]
    B --> D[デコンパイラで復元<br/>候補C]
    D --> E[再コンパイル<br/>候補バイナリ]
    C --> F[同一入力を<br/>両者で再実行]
    E --> F
    F --> G{ダイジェスト比較}
    G -->|一致| H[Matched]
    G -->|乖離| I[Divergence]
    G -->|クラッシュ消失| J[Crash Absence<br/>CVEトラックのみ]
  1. 参照C関数をコンパイルしてバイナリ化します。
  2. AFL++ で参照バイナリをファジングし、入力コーパスを動的に成長させます。これにより、固定テストでは踏まれない分岐やエッジケースが自動的に探索されます。
  3. デコンパイラが復元したCを再コンパイルして候補バイナリを作ります。
  4. 同一入力を参照と候補の両方で再実行し、境界付き観測可能ポスト状態(戻り値・書き込んだバイト列・書き込み可能なグローバル変数・.data/.bss範囲の内容)をダイジェストとして比較します。
  5. AddressSanitizerビルドでクラッシュ・ハングを検出し、非計装の-O0ビルドで状態比較を行います。

判定ラベルは3種類です。Matched は全入力で参照と一致した場合、Divergence は1入力以上で乖離した場合(出力変更・クラッシュ・非終了を含む)、Crash Absence は参照が示すべきクラッシュが候補で起きない場合です。Crash Absence はCVEトラックのみで使われ、Divergence より優先されます。頑健性への配慮も徹底しており、ポインタ値はリロケーションの影響をマスクするよう正規化され、GitHub・CVEトラックでは4回再実行しても再現する乖離だけを判定に使うことでフレーク(不安定な失敗)を排除しています。

報告値の解釈にも注意が必要です。Matched率は「真の挙動一致率の上界」であり、Divergence率・Crash Absence率は「真の乖離率の下界」です。つまり「Matched だから安全」と読むのではなく、「ここまででも既に乖離がこれだけある」と保守的に受け取るのが正しい読み方です。次章では、このオラクルで8つのシステムを検証した結果を見ます。

検証結果——ビルド率が上がるほど、挙動一致率は下がる

評価対象は、伝統的デコンパイラのGhidra/Hex-Raysと、LLMベースの8システム9構成です。リファインメント型(フロントエンド出力をLLMで洗練する方式)にはLLM4Decompile、Idioms、SK2Decompile、DeGPT-Qwen、エンドツーエンド型(アセンブリから直接Cを生成する方式)にはAutoDecompiler、Nova、SLaDe、GLM-5.2(両役割で評価)が並びます。データは確立コーパス4種(HumanEval-Decompile n=161、ExeBench n=1538、AnghaBench n=215、MBPP C変換 n=597)に加え、GitHubの実関数291個(中央値23 SLOC・132ライブラリ)と公開済み脆弱性を含むCVE関数287個(中央値47 SLOC・94プロジェクト)を収集しました。ファジング(自動生成した大量の入力を実行して挙動を探る手法)のカバレッジはライン84〜99%、ブランチ74〜97%に達し、固定テストの盲点を補っています。

確立コーパスでは「ビルドが通る」と「挙動が一致する」が明確に分離されました。乖離(Divergence)は、同じ入力を参照実装と候補の両方で実行したとき、戻り値・書き込みバイト・グローバル状態などに差が出ることを指します。LLM4DecompileはHumanEvalでPass率85%(Ghidraの81%を上回る)を記録する一方、乖離率は13〜21%で、Ghidraの1〜4%を大きく上回りました。SK2DecompileもAnghaBenchで乖離率25%(Hex-Raysは0%)です。さらに、同梱テストを全てパスしたのに乖離する割合(Div||P)は、NovaがHumanEvalで13%、LLM4DecompileがExeBenchで8%に達しました。プール全体では、同梱テストを全パスした12,133候補のうち4.9%が乖離し、その77%はクラッシュを伴わない出力変更でした。

GitHub実関数の結果はさらに明確です。LLM4DecompileはGhidraのビルド率75%を90%へ引き上げる一方、挙動一致率(Matched)は74%から62%へ低下しました(差28ポイントのうち23ポイントは状態変更、5ポイントは新規クラッシュ)。ペア比較(McNemar検定 p=.002)でも、再コンパイル可能性ではリファイナーが優位、挙動一致ではフロントエンドのGhidraが優位と逆転します。

システム 種別 Build率 Matched率
Ghidra フロントエンド 75% 74%
LLM4Decompile リファインメント 90% 62%
Nova エンドツーエンド 11% 3%
SLaDe エンドツーエンド ほぼビルド不能 —
xychart-beta
    title "GitHub実関数: Build率とMatched率"
    x-axis [Ghidra, LLM4Decompile]
    y-axis "率(%)" 0 --> 100
    line [75, 90]
    line [74, 62]

ビルド率を稼いだシステムほど別の指標で失敗しています。NovaはGitHubでBuild率11%・Matched率3%と調整済みコーパス外への汎化に失敗し、SLaDeはほぼビルド不能でした。SK2Decompileはビルド率をHex-Raysの41%にまで削り、Idiomsは2%しかビルドできません。グラフの通り、ビルド率(上昇線)と挙動一致率(下降線)は逆方向に動きます。再コンパイル可能性だけを報いる評価は、挙動を犠牲にビルド率を稼ぐ方向へLLMデコンパイラを誘導している——これが検証の核心的な結論です。

もっと深刻な問題——公開済み脆弱性が「跡形もなく消える」Crash Absence

ビルド率と挙動一致率の逆転より深刻なのが、公開済み脆弱性(CVE)がデコンパイルの過程で再現できなくなる現象です。本論文のCVEトラックでは、公開済み脆弱性を含む287関数を、AddressSanitizer(ASan)+UBSanビルドと攻撃者制御のファイル入力ドライバで再現しました。入力コーパスはPoCを含むAFL++リプレイコーパス14,323個+手動PoCを合わせた最終21,119個(関数あたり平均74個)で、参照バイナリが確実にクラッシュする入力が揃っています。

この環境で、LLM4Decompileは183ビルドのうち25で参照のクラッシュを失いました。比率は25/287=8.7%(95%信頼区間 [6.0%, 12.5%])です。leave-one-project-outのブートストラップ検定でも、この結果が特定プロジェクトに依存しない頑健な傾向であることが確認されています。つまり公開済み脆弱性の最大1割程度が、LLMデコンパイラの出力では「クラッシュしないコード」に化けてしまうのです。

システム Crash Absence(クラッシュ消失)
raw Ghidra 0件(消失なし)
Hex-Rays 3件(ストライド誤り/未定義動作由来)
LLM4Decompile 25件(8.7%)
flowchart LR
    A[CVE関数の参照バイナリ] --> B[クラッシュ発生 ⚡]
    A --> C[LLMデコンパイラで復元]
    C --> D[再コンパイル]
    D --> E[クラッシュ再現せず ✅]
    style B fill:#fdd,stroke:#c00
    style E fill:#dfd,stroke:#0a0

さらに、クラッシュを保ったまま出力やグローバル変数を変える Divergence も、LLM4Decompile に10件、GLM-5.2-E2E に8件確認されています。クラッシュだけを観測するオラクルでは、この手の静かな破壊は見逃してしまいます。ここでの実務的含意は明確です。「クラッシュが再現できない」=「バグが直った」ではない、ということです。デコンパイル段階で脆弱性が消えているだけという可能性を常に排除しなければなりません。次章では、なぜこうした「消滅」が起きるのかをソースレベルで解析した結果を見ます。

なぜ乖離は起きるのか——ソースレベル解析が明かす「自信に満ちた発明」

乖離はなぜ起きるのでしょうか。本論文は全関数-候補ペア40,345組をソースレベルで解析し、出力に含まれるトークンを「フロントエンド由来」「参照由来」「導入(introduced)」の3種類に分類しました。「導入」とは、コメント・文字列を除去した後でも、フロントエンド入力と参照ソースの両方に存在しない識別子のことを指します(Cキーワード・標準識別子・デコンパイラ固有の語彙は除外)。

人手検査60件+対照20件で見逃しゼロという検証を経て明らかになったのは、発明トークンの12%は実体のない完全な虚構であり、残りは実在エンティティの改名・再束縛であるという事実です。後者の典型例が、プロジェクト固有のマクロやラッパー関数を標準ライブラリの呼び出しに置き換えるパターンです。発明はさらに3つの類型に整理できます。

類型 内容 具体例
導入フィールド(Fld) 生オフセット参照を構造体のフィールド参照に書き換える際に捏造 *(_DWORD*)(a1+40) を架空の構造体フィールドに
導入型(Typ) 存在しない型を新造 u32, Mat4 など
導入被呼関数・ガード(Cal/guard) 分岐条件や関数呼び出し・定数を書き換え 比較演算子や定数の変更

この「自信に満ちた発明」は統計的にも乖離と強く結びついています。発明を含む関数は含まない関数より乖離率が高く、GitHub実関数では 53% vs 20%(オッズ比4.6、Fisher検定 p<10^-6)、CVE関数では 59% vs 31%(p<10^-4)でした。関数長の三分位内で比較してもギャップは持続するため、単に「関数が複雑だから」という説明では片付きません。

失敗モードの構造変化も重要な発見です。伝統的デコンパイラは「可視の不明物」で失敗します(Ghidra の失敗の43%、Hex-Rays の28%がフロントエンド由来語彙)。一方リファインメント型・E2E型は「導入シンボル」で失敗し、その割合は失敗スプライスの42〜72%に達します。例外は DeGPT-Qwen で、その場編集により語彙除去が16%にとどまり、Ghidra に近い失敗プロファイル(乖離率7%以下)を示しました。保守的な書き換えほど挙動を保つという教訓がここに現れています。さらにデータセット依存も顕著で、調整済みコーパスでは平坦なスカラー署名が多いため導入フィールドは2%以下ですが、CVE関数では SK2Decompile / Nova / GLM-5.2-E2E で出現率が56〜72%に急増します。実世界の複雑なコードほど、LLMは「推測の上塗り」を激しく行うのです。

実務への示唆——LLMデコンパイラをどこまで信用できるか

ここまでの検証結果を踏まえると、LLMデコンパイラの出力は「読みやすい」と「正しい」を分けて考える必要があります。信用してよい場面と、絶対にそのまま使ってはいけない場面を整理します。

信用してよい場面は、読みやすさ優先の概観把握、シンボル名の復元、ストリッピング済みバイナリの人手レビュー補助、既知のクラッシュ解析の起点づくりです。人間が最終判断を下し、分からない箇所を別手段で確認できる状況なら、LLMデコンパイラは強力な補助ツールになります。

危険な場面は、脆弱性トリアージの最終判断、マルウェア解析の挙動結論、ファジング基盤としての再コンパイル、パッチ有無の判定です。前章までの数値が示すように、「ビルドが通る・同梱テストが通る」は挙動一致の証拠になりません。公開済み脆弱性の最大1割でクラッシュが消える以上、セキュリティ上の結論をLLMデコンパイラの出力だけで下すのは危険です。

flowchart TD
    A[LLMデコンパイラの出力] --> B{用途は?}
    B -->|概観把握・シンボル復元・<br/>レビュー補助| C[人手で最終判断 → 信用可]
    B -->|脆弱性トリアージ・挙動結論・<br/>ファジング基盤・パッチ有無| D{検証する}
    D --> E[フロントエンド出力とdiff → 導入トークン確認]
    D --> F[型幅・定数・分岐ガードの変更を確認]
    D --> G[差分ファジングで挙動検証]

検証なしに使わないための実務チェックリストは3点です。1つ目は、元のフロントエンド出力とdiffを取ること。導入トークン(発明)が増えていないかを確認します。2つ目は、未初期化スタック配列・型幅・定数・分岐ガードの変更がないかを目視確認すること。3つ目は、重要な関数について元バイナリとの差分ファジング(Decompile-Diverge 的なアプローチ)で挙動を検証することです。

評価の未来にも言及が必要です。指標を「再コンパイルできるか」から「参照と挙動が一致するか」へ移行し、リファインメント型の学習報酬にも挙動一致を組み込むべきだ、と論文は主張します。リファインメント型は「ビルド率を稼ぐ」方向に過適合しやすい構造のため、報酬設計の変更は避けて通れません。アナリスト側も「読みやすい=正しい」という直感への警戒を続ける必要があります。

よくある質問

デコンパイルって具体的に何をするもの?

機械語(アセンブリ)をC風の高レベルソースに戻す技術です。コンパイル時に失われた変数名や型情報を文脈から推測しながら復元するため、完全な再現ではなく「読める形への近似」と考えてください。脆弱性検出・マルウェア解析・レガシーコードの保守など、バイナリ解析作業の第一歩として広く使われています。

「再コンパイルできる」と「正しく復元できている」はなぜ違うの?

再コンパイル可能性は「ビルドが通ること」だけを保証し、関数の挙動全体までは保証しません。同梱テストの範囲外の入力で挙動が変わっても、テストをパスしている限り検出されないのです。本論文では、同梱テストを全パスした候補のうち最大13%が他の正当入力で乖離しました(プール全体では4.9%)。

LLMデコンパイラで脆弱性が「消える」って本当?

本当です。本論文のCVEトラックで、LLM4Decompileの出力のうち8.7%(25/287)で参照のクラッシュが再現しなくなりました(Crash Absence)。クラッシュが消えるのは「脆弱性が直った」からではなく、デコンパイル時の書き換えでクラッシュを引き起こす処理が失われた可能性が高いです。raw Ghidra ではこの消失はゼロでした。

Decompile-Divergeはどう使えるの?

手書きテストなしで、関数ごとに挙動比較オラクルを自動構築するベンチマーク手法です。AFL++で参照バイナリをファジングして入力コーパスを作り、デコンパイル結果を再コンパイルした候補と同一入力で比較します。デコンパイラの選定時の性能比較、学習時の報酬計算、実務での差分ファジング検証に利用できます。

実務でLLMデコンパイラを使うなら、何に気をつければいい?

出力をそのまま信用せず、フロントエンド出力とのdiffで「導入された型・フィールド・定数・分岐ガード」を確認してください。重要な関数は元バイナリとの差分ファジングで挙動を検証し、脆弱性・マルウェアの最終判断には使わないこと。概観把握やシンボル復元のような補助用途にとどめるのが安全です。

まとめ

本論文の貢献は、固定テストに依存しない挙動比較オラクル Decompile-Diverge を提案し、GitHub実関数291個+CVE関数287個という新規ベンチマークセットでLLMデコンパイラを検証した点にあります。その結果が示したコアメッセージは、「LLMデコンパイラの再コンパイル可能性は誤った方向を報いる」という逆説です。実際、LLM4DecompileはGhidraのビルド率を75%から90%へ引き上げた一方、挙動一致率(Matched)は74%から62%へ低下しました。さらに、ビルドが通り同梱テストを全パスした候補12,133個のうち4.9%は他の正当入力で乖離し、単一システムでは最大13%に達しました。「コンパイルが通る」ことは挙動一致の証拠になり得ません。

セキュリティ上の含意はさらに深刻です。CVEトラックでは、公開済み脆弱性を含む関数の最大1割(LLM4Decompileで25/287、8.7%)で参照のクラッシュが再現しなくなりました(Crash Absence)。これはバグの修正ではなく、デコンパイル時に可視のプレースホルダー(undefined4 や _DWORD などの「不明マーカー」)が「自信に満ちた発明」——導入されたフィールド・型・呼び出し・分岐ガード——へ置き換わることで、脆弱性が跡形もなく消え去った結果です。クラッシュが再現できないことを「バグが直った」と解釈するのは危険です。

今後の方向性は明確です。デコンパイラの評価指標を「コンパイルできるか」から「参照と挙動が一致するか」へ移行し、挙動一致そのものを学習報酬に組み込むべきです。実務者は「読みやすい=正しい」という直感に警戒し、重要な関数は元バイナリとの差分ファジングで挙動を検証しながら、LLMデコンパイラによる読みやすさの向上はあくまで人手レビューの補助として位置づけるのが安全な使い方になります。

参考ソース

関連記事