maxfie1d のブログ

マイクロソフト系技術ネタを中心に書きます。

心を決めて IL をやる #1

IL(アイエル)をご存知でしょうか。

そう、C#などの言語とマシン語命令の間にあるやや機械よりの中間言語のことです。

C#などの.NETの言語はコンパイルされるとILに変換され、 実行時に実際のマシン語命令に変換されます。

なので、通常開発者がILを意識する必要はないのですが .NETを深く理解したい勇気ある開発者はILも(ある程度)理解しているものです。

こうしてブログを書いている私も勇気を出して、 難攻不落なILに一歩踏み出します。ご一緒にいかがですか(´;ω;`)

さあ、行きますよ!

さっそくILに変換してみる

難しいことは簡単なところから攻めるのがセオリーです。

以下のコードは Main 関数内で変数a1+1の結果を代入し、 Console.WriteLineメソッドで変数aの内容を表示するというこれ以上ないくらいシンプルなものです。

using System;

namespace ConsoleApp3
{
    class Program
    {
        static void Main(string[] args)
        {
            int a = 1 + 1;
            Console.WriteLine(a);
        }
    }
}

これをILに変換してみます。

C#→ILに変換する専用のツールが必要かと思いきや、使うのはC#コンパイラ csc です。 cscを使うために Developer Command Prompt for VS 2017 を起動します。 Visual Studio をインストールしていれば名前は多少違うと思いますがどこかにあります。

起動した Developer Command Promp で上記のソースをコンパイルします。

> csc Program.cs

コンパイルが成功すると同じディレクトリにexeファイルが出来るはずです。 exeは実行形式ファイルとしておなじみですが、実はこの中にILが含まれています。 ただしバイトコード形式なので人間がそのまま読むのは(ほぼ)不可能です。

そこでildasmで逆アセンブルして人間が読める形に変換します。

>ildasm /output=Program.il Program.exe

成功すればProgram.ilというファイルが同じディレクトリに生成されます。

中身を見てみましょう(長いです)。

//  Microsoft (R) .NET Framework IL Disassembler.  Version 4.6.1055.0
//  Copyright (c) Microsoft Corporation.  All rights reserved.



// Metadata version: v4.0.30319
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
}
.assembly Program
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) 
  .custom instance void [mscorlib]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78   // ....T..WrapNonEx
                                                                                                             63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 )       // ceptionThrows.

  // --- The following custom attribute is added automatically, do not uncomment -------
  //  .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(valuetype [mscorlib]System.Diagnostics.DebuggableAttribute/DebuggingModes) = ( 01 00 07 01 00 00 00 00 ) 

  .hash algorithm 0x00008004
  .ver 0:0:0:0
}
.module Program.exe
// MVID: {63EE3CCC-2698-41E5-B6C1-F172BFAECB1D}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY
// Image base: 0x04DF0000


// =============== CLASS MEMBERS DECLARATION ===================

.class private auto ansi beforefieldinit ConsoleApp3.Program
       extends [mscorlib]System.Object
{
  .method private hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // Code size       11 (0xb)
    .maxstack  1
    .locals init (int32 V_0)
    IL_0000:  nop
    IL_0001:  ldc.i4.2
    IL_0002:  stloc.0
    IL_0003:  ldloc.0
    IL_0004:  call       void [mscorlib]System.Console::WriteLine(int32)
    IL_0009:  nop
    IL_000a:  ret
  } // end of method Program::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       8 (0x8)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  ret
  } // end of method Program::.ctor

} // end of class ConsoleApp3.Program


// =============================================================

// *********** DISASSEMBLY COMPLETE ***********************
// WARNING: Created Win32 resource file Program.res

メイン関数内で言うとたった2行だったコードがこんなに長くなるの?と 言いたくなりますが、他の様々な情報も含まれているのでメインの部分だけを抜き出します。

  .method private hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // Code size       11 (0xb)
    .maxstack  1
    .locals init (int32 V_0)
    IL_0000:  nop
    IL_0001:  ldc.i4.2
    IL_0002:  stloc.0
    IL_0003:  ldloc.0
    IL_0004:  call       void [mscorlib]System.Console::WriteLine(int32)
    IL_0009:  nop
    IL_000a:  ret
  } // end of method Program::Main

なんとなくMainメソッドの面影が感じられるでしょうか。 nopldcなどアセンブリ命令っぽいものを見受けられます。 この中で登場する命令の意味を調べてまとめてみます。

命令 説明
nop 何もしない
ldc.i4.2 2をint32としてスタックにプッシュする
stloc.0 スタックから値を一つ「ローカル変数0」にポップする
ldloc.0 「ローカル変数0」の値をスタックに読み込む
call メソッドを呼び出す
ret メソッドからリターンする

List of CIL instructions - Wikipedia

命令の意味と合わせてILでのMain関数の処理内容を推測すると 「2をスタックにプッシュする」→「さっきプッシュした2をローカル変数0にポップする」→「さっき2を入れたローカル変数0の値をスタックに読み込む」→「Console.WriteLineメソッドを呼び出す」→「リターンする」となります。

2をプッシュしたりポップしたりするあたりが冗長な感じがしますが、 大したことはしてないことは分かると思います。ちなみに元のソースコードでは 1+1となっているところがいきなり2になっているのは恐らくコンパイラの 最適化が効いたためと思われます。

とりあえずここまで...

こんな感じで少しずつ IL に親しむシリーズをやりたいと思います。 めざせ IL マスター