C#で作られたプログラムをデコンパイルしてみよう
この記事はC#アドベントカレンダー2014の15日目の記事です。
C# Advent Calendar 2014 - Qiita
デコンパイルとは
大雑把に言うと、コンパイルされたプログラム(exe, dllなど)から、元のコードを復元する行為のことです。
特にC#やJavaの様に中間言語に翻訳された状態でコンパイルされるような言語の場合は
元のコードにかなり忠実に復元されます。
本記事ではC#に限らず、.NETに関わる言語全般のデコンパイルをする方法を書いていきます。*1*2
中間言語(IL)
C#(や、VB.NETなど)では、コンパイル時に直接ネイティブコードにするのではなく、
一旦中間言語(以下、IL)という形にし、実行時にネイティブコードを生成するJITコンパイル方式を採用しています。
事前コンパイルという手段も用意されていますが基本的にはILを出力します。
ところで、ネイティブコンパイルをしないということは、JITコンパイラが存在する環境でさえあれば、
実行する環境を選ばないというメリットがあります。
今まではWindows環境で実行されることが殆どだったのであまり気にするする機会はありませんでしたが、
最近のDocker隆盛も相まって、Linux環境で実行する機会も増えており、
遂にJITコンパイル形式の強みが本当の意味で生かされることになりそうです。
閑話休題。ILはバイトコードのため、そのままでは人間が読む事ができませんが、
Visual Studioに付属しているildasm.exeを使うことでアセンブリ言語を高級にしたような形式で確認することができます。
逆アセンブル:IL(バイトコード)から人が読める形のILへ
本題のデコンパイルに入ります。 まずはIL(バイトコード)から人が読める形に出力する、逆アセンブルについての紹介です。
ildasm.exe
先ほど挙げたVisual Studioに付属している逆アセンブラー。
dllやexeに対して使用する事でILをテキストとして出力できます。
ildasm.exeはSDK内に存在しています。
C:\Program Files\Microsoft SDKs\Windows\v7.0A\bin C:\Program Files\Microsoft SDKs\Windows\v7.0A\x64\bin C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\bin C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\x64\bin
試しに起動してみるとこのようなウィンドウが表示されます。
今回はGUIで操作をしますが、コマンドラインからも実行できるので、一括で処理したい場合などはそちらをご利用ください。
Ildasm.exe (IL 逆アセンブラー) | Microsoft Docs
それでは試しに適当なコードを書いてコンパイルしたものをildasm.exeに通してみましょう。
namespace Hogehogee { public class SampleClass { public string HelloHogehoge(string[] args) { var word = args[0]; return string.Format("Hello {0}", word); } } }
これを作成してDebugビルド。生成されたdllを先ほどのウィンドウで開いてみましょう。
すると以下の様に展開されます。
上から順に名前空間、クラス名、クラス情報(.class)、コンストラクタ情報(.ctor)、HelloHogehogeメソッドの情報です。
それぞれをダブルクリックすることで中身をさらっと見ることができます。*3
クラス名の配下にある情報について見ていきます。
まずは.class。
.class public auto ansi beforefieldinit Hogehogee.SampleClass extends [mscorlib]System.Object { } // end of class Hogehogee.SampleClass
普段あまり気にしませんが、ちゃんと仕様通りSystem.Objectが暗黙的に継承されている事が見て取れます。
次に.ctor。
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed { // コード サイズ 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: ret } // end of method SampleClass::.ctor
こちらも暗黙的に基底クラス(System.Object)のデフォルトコンストラクタが呼ばれている事がなんとなくわかります。
C#の仕様で、スーパークラスのデフォルトコンストラクタが暗黙的に呼ばれるという仕様通りです。
最後にHelloHogehogeメソッド。
.method public hidebysig instance string HelloHogehoge(string[] args) cil managed { // コード サイズ 21 (0x15) .maxstack 2 .locals init ([0] string word, [1] string CS$1$0000) IL_0000: nop IL_0001: ldarg.1 IL_0002: ldc.i4.0 IL_0003: ldelem.ref IL_0004: stloc.0 IL_0005: ldstr "Hello {0}" IL_000a: ldloc.0 IL_000b: call string [mscorlib]System.String::Format(string, object) IL_0010: stloc.1 IL_0011: br.s IL_0013 IL_0013: ldloc.1 IL_0014: ret } // end of method SampleClass::HelloHogehoge
ちゃんとしたコードが書かれるともうILについてちゃんと知らないと何が何やら。
しかしなんとなく"Hello {0}"文字列を使ってstring.Format()を呼び出している様に見えます。
そしてなんとなくですがwordって変数っぽいものが見えます。*4
なんとなーくわかりますね。ただ、C#のコードからはかなり程遠いものが出てきます。
ILの仕様を理解しないと、たったこれだけでも割と四苦八苦してしまいそうです。
実際のプロダクトのコードを全部ILで読もうとしたら辛さしかありません。
しかし、後述するように、C#のコードそのものを復元するデコンパイラも開発されていますので、
コードからは読み取れないような謎の動きを解析する場合以外はこちらを使うことはあまり無いかもしれません。
LINQPad
少し趣向は違いますが、LINQPadでも人が読める形式のILを確認することができます。
こちらはdllやexeをデコンパイルするのではなく、コードを書いて、それによって生成されるILを確認する事ができます。
当ブログでも過去に少しだけ使っているのでそちらを見て頂ければと思います。
ItemクラスにIndexerが存在出来ない理由がILを眺めるとわかります。*5
C#における Indexer ちょっと特殊なケースについて - ほげほげー
ILを確認する以外の超絶便利な使い方はアドベントカレンダー 6日目の
takeshik せんせーの記事をご参照ください。
今日からはじめる LINQPad | TAKESHIK.ORG
デコンパイル:IL(マシン語)から元のコードを復元
さて、ここからが本記事の本題です。
前述のように、人に読める形でILを出力してくれるのもそれはそれで便利ですが、
やはり通常のC#のコードとして読めたほうが早いし嬉しいですよね。
そこで、ILを解釈してC#のコードを出力してくれる素敵なデコンパイラの出番です。
.NET Reflector に始まり dotPeek, ILSpy, etc...
大体の場合はこれらを使うことでかなり読みやすいコードが出力されます。
変数名の情報(誤ってDebugビルドで提供されていない限り)は残っていないので、
str1, str2などとなりますが、それは仕方ないので我慢しましょう。
以下にいくつかのデコンパイラを記載しますので、色々試してみて頂ければと思います。
.NET Reflector
以前はこちらが無償提供されていたのでこれ一択でした。
豊富なプラグインがあり、今でも愛好家が多いようです。
2011年の3月に有償化され、気軽には使えなくなりましたが、
その際いくつかの競合プロダクトが無償化、公開されました。
以下にそれら競合プロダクトを挙げていきます。
dotPeek
dotPeek: Free .NET Decompiler & Assembly Browser by JetBrains JetBrains社製のデコンパイラ。 ReSharperに付属していて、定義へ移動する際にdotPeekを通してデコンパイルしたものを表示したりしてくれます。
UIがVisual Studioライクでかなり馴染みやすいのではないでしょうか。
ILSpy
MITライセンスで公開されているオープンソースです。
スタンドアロンで利用できるため、普段はもっぱらこちらを利用しています。
試しに最初に作ったdllをこれを使ってデコンパイルした結果を載せておきます。
// Debug ビルド版 using System; namespace Hogehogee { public class SampleClass { public string HelloHogehoge(string[] args) { string word = args[0]; return string.Format("Hello {0}", word); } } }
// Release ビルド版(pdbファイル有) using System; namespace Hogehogee { public class SampleClass { public string HelloHogehoge(string[] args) { string word = args[0]; return string.Format("Hello {0}", word); } } }
// Release ビルド版(pdbファイル無) using System; namespace Hogehogee { public class SampleClass { public string HelloHogehoge(string[] args) { string arg = args[0]; return string.Format("Hello {0}", arg); } } }
Release ビルドでpdbファイルが削除されたもの以外では暗黙的に付与されるusing System;が明示されただけで
最初に書いたものとほぼ同等のものが出力されました。
非常に便利ですね。
ここまで見事に戻されると、センシティブな情報をコード内に埋め込むことは非常に怖い、
という所も容易に理解できるかと思います。
プログラムを提供する際にはお気をつけ下さい。
その他
JustDecompile .NET Assembly Decompiler & Browser - Telerik
CodeReflect - Free .NET Decompiler
まとめ
オープンソースなプロダクトも増えてきているので活用の機会は多少減っていますが、
まだまだ.NETでクローズドなプロダクトを扱うことは多々あります。むしろそのほうが多いですよね。
その際には規約を良く読み、問題が無ければじゃんじゃん使っていきましょう。
勉強にもなりますしね。
今回は取り上げていませんが、VB.NETの生成物からC#、C#の生成物からVB.NET、F#の生成物からC#なんてことも出来ますので、
興味があればお試しください。
F#をC#やVB.NETに変換すると、F#がどのようにしてパターンマッチを実現しているのかが分かったりして面白いですね。
明日は「oika」さんです。よろしく!