(小技) .NET プログラムの手軽?な解析方法

# 1. 概要

c# でプログラムを記述する場合、そのソースコードが実際にはどのような振る舞いをするのか気になることがよくあります。
特にパフォーマンス向上を目的として最適化を考慮しなければならない場合には、書かれたソースコードがどのように実行されるかは重要な情報です。

しかし、本格的な解析のためのソフトウェアは大抵高価なものになります。

本格的な解析ソフトウェアに比べて得られる情報は少ないが Visual Studio 以外のツールが不要で済む、そういった需要のために、本稿では、 .NET のアセンブリの解析手順について述べていきたいと思います。

**これはおそらく基本的な方法であり、改めて説明するほどでもないとは思います。**
しかし、これからしようとしている別のいくつかの投稿でアセンブリの解析手順の説明が必要なため、それらの参照元としてこの記事を投稿することにしました。ご了承ください。

# 2. IL レベルの解析

ILレベルの解析とは言っても、正確には **「アセンブリ内の IL のコードを c# で表現するとどのようなソースコードになるか」** ということになります。いわゆる「デコンパイル」という方法です。

# 2.1 手順
Visual Studio の c# エディタには簡単なデコンパイル機能があるので、それを利用します。
以下の手順でデコンパイルが可能です。

1. デコンパイル対象のアセンブリ (.dll) を用意する。
1. コンソールアプリケーション c# プロジェクトを新規に作成する。使用する .NET ランタイムは、デコンパイル対象のアセンブリが使用するランタイムと同じバージョンとする。
1. コンソールアプリケーションプロジェクトの依存関係に「参照」でデコンパイル対象のアセンブリファイルを追加する。
4. コンソールアプリケーションプロジェクトのソースコード上にデコンパイルしたいクラスまたはメソッドの呼び出しを記述し、ビルドする。実行はしなくてもいいので、構文的に合っていれば大抵OK。例えばデコンパイル対象のクラスの名前が `SomethingClass` の場合は、`Console.WriteLine(nameof(SomethingClass));` を記述するなど。
1. ビルドができたら、4 で記述したソースコードを c# エディタで開いて、デコンパイルしたいクラスまたはメソッドの名前にカーソルを合わせて `F12` キーを押すと別のウィンドウにデコンパイル結果が表示される。

3 までの手順を追えると、コンソールアプリケーションのプロジェクトファイル (.csproj) は以下のようになっているはずです。
```xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>

... 

  </PropertyGroup>

...

  <ItemGroup>
    <Reference Include="ClassLibrary">
      <HintPath>..\ClassLibrary\bin\Release\net8.0\ClassLibrary.dll</HintPath>
    </Reference>
  </ItemGroup>
</Project>
```

`HintPath` の部分がクラスライブラリの Release 版の DLL のパス名になっていることに注意してください。
また、`HintPath` が指しているクラスライブラリのフレームワーク (この例の場合 `net8.0`) が `TargetFramework` の値と一致していることを確認してください。


デコンパイルが行われると、デコンパイル結果は"[逆コンパイル済み]" という文字列が付加された別のタブに表示されます。
また、デコンパイル結果の末尾には以下のようなログが生成されているはずです。
※実際のログの内容はデコンパイル対象のアセンブリによって異なります。
```
#if false // 逆コンパイルのログ
キャッシュ内の '164' 個の項目
------------------
解決: "System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
1 つのアセンブリが見つかりました: 'System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
読み込み元: 'C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.1\ref\net8.0\System.Runtime.dll'
------------------
解決: "System.Runtime.InteropServices, Version=8.0.0.0, Culture=neutral, PublicKeyToken=null"
1 つのアセンブリが見つかりました: 'System.Runtime.InteropServices, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
読み込み元: 'C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.1\ref\net8.0\System.Runtime.InteropServices.dll'
------------------
解決: "System.Runtime.CompilerServices.Unsafe, Version=8.0.0.0, Culture=neutral, PublicKeyToken=null"
1 つのアセンブリが見つかりました: 'System.Runtime.CompilerServices.Unsafe, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
読み込み元: 'C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.1\ref\net8.0\System.Runtime.CompilerServices.Unsafe.dll'
#endif
```
## 2.2 注意事項
+ .NET ランタイムのクラスやメソッドに対して同様の手順を実行すると、デコンパイル結果ではなく、タブ名に "[SourceLink]" という文字列が付加されて実際のソースコードが表示されることがあります。これは .NET のアセンブリが git 上のソースコードに紐づけられているからです。これはこれで便利なのですが、本稿の目的には合いません。
+ デコンパイル対象のアセンブリを参照する場合、そのアセンブリにデバッグシンボルファイル (".pdb") がないことを確認してください。デバッグシンボルファイルがあると、デコンパイル結果ではなく元のソースコードが表示されることがあります。可能であれば、__デコンパイル対象のアセンブリのプロジェクト設定で__ 「デバッグシンボル」の項目を「生成済みのシンボルはありません」に設定してビルドするといいでしょう。

# 3. 機械語レベルの解析

Visual Studio には簡単な逆アセンブルの機能もあるので、それを利用します。
以下の手順で解析が可能です。なお、改めて書くまでもないとは思いますが、**アセンブラ言語を読めることが必須です。**

## 3.1 手順

1. [2. IL レベルの解析](#2-il-レベルの解析) が可能な手順まで完了させる。
1. コンソールアプリケーションに解析したいメソッドの呼び出しを記述する。
1. 2 で記述したソースコードの行にブレークポイントを設定する。
1. コンソールアプリケーションをデバッグモードで実行して、3 で設定したブレークポイントの場所で停止させる。
1. Visual Studo のメニューから"デバッグ">"ウィンドウ">"逆アセンブリ"を選択する。
1. 逆アセンブリタブができるので、逆アセンブリタブに切り替える。
1. ステップ実行していって、メソッド呼び出し命令 (x86/x64 なら `call` 命令) でステップインする。ステップインしたアドレスが解析したいメソッドの先頭アドレスになるので、機械語を読む。

## 3.2 注意事項

+ メソッド呼び出しの行はなるべく単純にしてください。例えば、`SampleClass.Execute(string s)` というメソッドの解析をしたい場合に`SampleClass.Execute($"count:{count}");` などと記述すると、ブレークポイントで停止した箇所から `SampleClass.Execute()` への呼び出し箇所を探すのが面倒になります。引数を設定しなければならない場合は、`var value = $"count:{count}";` と `SampleClass.Execute(value);` などのように別のステートメントに分けて、ブレークポイントは後者の行に設定するようにしてください。
+ 解析対象がメソッドではなくプロパティの場合、この方法ではうまくいきません。プロパティの取得/設定の呼び出しの機械語命令 (x86/x64 なら `call` 命令) でステップインしようとしてもステップオーバーしてしまうからです。理由は不明です… orz