Skip to content

Instantly share code, notes, and snippets.

@iwate
Last active July 26, 2019 06:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iwate/dbb6c4b02099adcdb4c83b23d879a5fe to your computer and use it in GitHub Desktop.
Save iwate/dbb6c4b02099adcdb4c83b23d879a5fe to your computer and use it in GitHub Desktop.
Let's read MSIL

MSILを読んでみよう

計算機をつくってみようの続きです。内容は独立しているのでこの演習から始めても問題ありませんが、計算機を自分で作ってからのほうが理解が捗ると思われます。

MSIL (Microsoft Intermediate Language)

MSILはCIL(Common Intermediate Language)のMS実装です。CILはdotnetのruntimeが実行できる中間言語です。

C#やF#、Visual Basicをコンパイルするとdllやexeができます。これらの成果物は中間言語であるMSILやCILのバイナリになっています。

runtimeがこのdllやexeを実行するにアーキテクチャに沿ったNativeコードに変換しアプリケーションが実行されます。

     +----+            +----+          +--------+
     | C# |            | F# |          | VB.NET |
     +----+            +----+          +--------+
        |                 |                 |
        |                 |                 |
 +------v------+   +------v------+   +------v------+
 | C# Compiler |   | F# Compiler |   | VB Compiler |
 +-------------+   +-------------+   +-------------+
        |                 |                 |
        |                 |                 |
        |                 |                 |
        |                 |                 |
        |                 |                 |
        |         +--------------+          |
        +--------->  dll or exe  +<---------+
                  | (MSIL / CIL) |
                  +--------------+
                          |
                          |
    +-------------------------------------------+
    |                     |                     |
    |       +-------------v-------------+       |
    |       |JIT (Just In Time) Compiler|       |
    |       +---------------------------+       |
    |                     |                     |
    |                     |                     |
    |               +-----v-----+               |
    |               |Native Code|               |
    |               +-----------+               |
    |                                   Runtime |
    +-------------------------------------------+

MSILをみてみよう

実際にMSILを見てみましょう。まずは対象のexeを用意します。

using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Print(int n)
        {
            Console.WriteLine("Result {0}", n);
        }
        static void Main(string[] args)
        {
            var l = 100;
            var r = 1;
            var n = l + r;
            Print(n);
        }
    }
}

これをコンパイルし、ConsoleApp1.exeを用意しましょう。

ildasmでexeを開く

exeはそのままではバイナリ形式なので簡単には理解することができません。ildasmというアプリケーションを使用して逆アセンブルしてみましょう。

ildasmはVisualStudioをインストールした際に入っているので、開発者コンソールから起動してみましょう。

**********************************************************************
** Visual Studio 2019 Developer Command Prompt v16.1.5
** Copyright (c) 2019 Microsoft Corporation
**********************************************************************

C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise>ildasm

ファイルメニュー、開くから先ほどコンパイルしたexeを開きましょう。

ツリーを眺める

exeを開くと次のようなツリーがでたはずです。※下記のツリーはテキストダンプしたものです。ウィンドウでは[xxx]の部分はアイコンになっています。

[MOD] <dll path>
|      M A N I F E S T
|___[NSP] ConsoleApp1
    |___[CLS] ConsoleApp1.Program
        |     .class private auto ansi beforefieldinit 
        |___[MET] .ctor : void()
        |___[STM] Main : void(string[])
        |___[STM] Print : void(int32)
Label Description
MOD Module
NSP Namespace
CLS Class
MET Method
STM Static method

作成したProgramクラスにスタティックメソッドのMainPrintがあることが確認できます。

MainのILをよんでみよう

ツリーでMainをクリックするとILのウィンドウが開きます。

開いたウィンドウにはMainメソッドのILを逆アセンブルして人間に読みやすいようにして表示されているのでこれを読んでいきましょう。

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // コード サイズ       18 (0x12)
  .maxstack  2
  .locals init (int32 V_0,
           int32 V_1,
           int32 V_2)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   100
  IL_0003:  stloc.0
  IL_0004:  ldc.i4.1
  IL_0005:  stloc.1
  IL_0006:  ldloc.0
  IL_0007:  ldloc.1
  IL_0008:  add
  IL_0009:  stloc.2
  IL_000a:  ldloc.2
  IL_000b:  call       void ConsoleApp1.Program::Print(int32)
  IL_0010:  nop
  IL_0011:  ret
} // end of method Program::Main

1行目のメソッド宣言はとばして{}の中だけを読んでいきましょう。

IL 説明
.entrypoint 実行時の開始時点の表す
.maxstack 2 スタックサイズを2に設定する
.locals init (int32 V_0, int32 V_1, int32 V_2) int型の変数を3つ用意する
nop 何もしない
ldc.i4.s 100 スタックに4byteの固定値=100を積む
stloc.0 0番目の変数=V_0にスタックに積まれた値=100を格納する
ldc.i4.1 スタックに4byteの固定値=1を積む(ldc.i4.s 1とも書ける)
stloc.1 1番目の変数=V_1にスタックに積まれた値=1を格納する
ldloc.0 V_0の値をスタックに積む
ldloc.1 V_1の値をスタックに積む
add スタックに積まれた値、100と1を加算する
call void ConsoleApp1.Program::Print(int32) Printメソッドをコールする
nop 何もしない
ret 処理を終了する

「おっ!どこかで見たことあるぞ!?」と感じましたでしょうか。そう、計算機をつくってみようで作成した計算機と同様にMSILもスタックをベースに演算を実行していくのです!

先ほど書いたPrintメソッドが呼び出されている箇所がありました。今度はPrintメソッドを読んでみましょう。

PrintのILをよんでみよう

ツリーでPrintをクリックするとILのウィンドウが開き、Mainと同様に人間が見やすい逆アセンブルされたコードを見ることができます。

.method private hidebysig static void  Print(int32 n) cil managed
{
  // コード サイズ       19 (0x13)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      "Result {0}"
  IL_0006:  ldarg.0
  IL_0007:  box        [System.Runtime]System.Int32
  IL_000c:  call       void [System.Console]System.Console::WriteLine(string,
                                                                      object)
  IL_0011:  nop
  IL_0012:  ret
} // end of method Program::Print
IL 説明
.maxstack 8 スタックサイズを8に設定する
nop 何もしない
ldstr "Result {0}" スタックに文字列Result {0}の参照(アドレス)が積まれる
ldarg.0 0番目の引数の値をスタックに積む
box スタックに積まれた0番目の引数の値をBox化して参照を積む
call void [System.Console]System.Console::WriteLine(string,object) WriteLineメソッドををコールする
nop 何もしない
ret 呼び出し元(Main)に処理をもどす

大体処理は読み解けたでしょうか。ここではBox化という新たな言葉が出てきました。次はBox化について詳しく見ていきましょう。

Box化

先ほどのPrintメソッドを読み進めているうちにbox命令があらわれました。この命令は値を参照に変換する命令です。この変換をBox化と呼びます。このBox化について詳しく見ていきましょう。

値と参照

計算機をつくってみようで作成した計算機は、すべてのデータはint型でした。しかし、私たちは.NETにそれ以外の型があり、必ずしも4bytesに収まるものではないことを知っています。

例えば、Printメソッドで出てきた文字列Result {0}を考えてみましょう。

簡単にするために、ASCIIコードで考えると文字列Result {0}は10bytesです。ldstr命令では、この10bytesをそのままスタックに積んでいるのでしょうか? 答えはNOです。仮にそのまま積んでしまったら、popする際に文字列データはバラバラになってしまいます。

それでは、ldstr命令の実行時にスタックには何を積んでいるのかというと、 文字列Result {0}のデータはスタックとは関係ない全く別の領域に配置されており、そこの場所を表す値=参照をスタックに積んでいます。

この仕様は、文字列だけではありません。classから生成したインスタンスや配列などの固定長でないものは参照で取り扱われます。

値と参照については以下のページに詳しく記載されているので時間があれば読んでみましょう。

値型と参照型 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

Box化

さて、本題のbox化ですがbox化は値を参照に変換するということです。なぜ、そのようなことをする必要があるのでしょうか? PrintメソッドのSystem.Console.WriteLineを呼び出している箇所のILを確認してみましょう。

IL_0001:  ldstr      "Result {0}"
IL_0006:  ldarg.0
IL_0007:  box        [System.Runtime]System.Int32
IL_000c:  call       void [System.Console]System.Console::WriteLine(string,
                                                                      object)

System.Console::WriteLineの引数は2つで、その引数をIL_0001,IL_0006,IL_0007で準備しています。box化しているのは2つ目引数です。2つ目の引数の型をIL_000cで確認すると、object型であることがわかります。C#ではobjectはすべてのクラスが継承しているクラスですので、2つ目の引数で要求しているのは参照であることがここからわかります。しかし、我々がC#のコードで引数に渡しているのは、int型であり、値です。この値そのままでは、メソッドの要求を満たすことができません。これが、IL_0007でbox化している理由です。

演習

クラス、構造体、メソッド、スタティックメソッドを使用するC#コードを書いてそのILを読んでみよう。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment