連載
.NETの動作原理を基礎から理解する!

第3回 .NETアプリケーションが実行される仕組み

デジタルアドバンテージ 一色 政彦
2005/10/26
Page1 Page2

.NETプログラム実行の処理の流れ(Mainメソッドまで)

 次の図は、冒頭で示したサンプル・アプリケーション(.EXEファイル)をダブルクリックして起動したときの、CLR上での処理の流れ、具体的には_CorExeMain関数を呼び出してから後の処理の流れを図示したものである。ちなみに_CorDllMain関数の場合も以下の流れと大差ない。

CLR上で.NETアプリケーションが起動する際の処理の流れ図(Mainメソッドまで)
ディレクトリ(本稿の例では「C:\fdotnet\WindowsApplication1\bin\Release」)の中にあるWindowsアプリケーション「WindowsApplication1.exe」をダブルクリックして実行したときのCLR上での処理プロセスを図示したもの。CLRに処理が引き渡される前の、つまり_CorExeMain関数が呼び出される前までの処理の流れについては前回の「.NETアプリケーションが起動する仕組み」を併せて参照するとよい。
  「CLRの初期化処理」と「CLRローダーの始動」。
  <アセンブリ・ローダー>による「アセンブリのロード」。
  <ポリシー・マネージャ>による「厳密名の検証」。
  CLRローダーによる「MainメソッドのILコード上の位置の特定」。
  <クラス・ローダー>による「Mainメソッド呼び出しに必要な型情報の収集」。
  CLRローダーによる「Mainメソッドの呼び出し」。
  <JITコンパイラ(型ベリファイア)>による「型の妥当性検証」。
  <JITコンパイラ>による「ILコードからネイティブ・コードへの変換」。
  <コード・マネージャ>による「ネイティブ・コードの実行と管理」。

 それでは、ここで図示されている一連の処理の流れを、1つずつ、もう少し詳しく説明していこう。

「CLRの初期化処理」と「CLRローダーの始動」

 前回の最後で説明したように、mscoree.dllの_CorExeMain関数(プログラムが.EXEファイルの場合。.DLLファイルの場合は_CorDllMain関数)が「.NET世界の玄関口となるAPI」である(ちなみに「mscoree」は、「MicroSoft Component Object Runtime Execution Engine:マイクロソフト・コンポーネント・オブジェクト・ランタイム実行エンジン」の略である)。

(1)CLRの初期化処理

 _CorExeMain関数の内部では、まずCLRの初期化処理が行われる。具体的には、アセンブリのCLRヘッダ(マニフェスト)を解析して、そのアセンブリが対応しているCLRバージョン(=ランタイム・バージョン)を特定する。このときにもし、肝心のマニフェストがアセンブリに格納されていなければ、そこで例外が発生してプログラムの実行は終了する。

 なおCLRにはすでにいくつかのバージョンがリリースされているが、ここで特定したバージョンのCLRがローカル環境にインストールされていない場合には、別バージョンのCLRが代理として自動的に使用されることがある。例えば、特定したアセンブリのCLRバージョンが「1.0.3705」(=.NET Framework 1.0でビルドした場合)で、ローカル環境のCLRバージョンが「1.1.4322」の場合、自動的にバージョン「1.1.4322」のCLRが用いられる。

 ちなみに、アセンブリのCLRバージョンが「1.1.4322」(=新しいバージョン)で、ローカル環境のCLRバージョンが「1.0.3705」(=古いバージョン)の場合は、古いバージョンのCLRは使用されずにプログラムは終了する。つまり新しいCLRバージョンのアセンブリを古い環境で実行することは基本的にできない。

 また、アプリケーション構成ファイル(.configファイル)を使ってサイド・バイ・サイド(side-by-side)実行が指定されている場合には、その構成ファイル内で指定されたCLRバージョンが使用される。CLRのサイド・バイ・サイド実行については「TIPS:サイド・バイ・サイドによりCLRバージョンを指定するには?」を参照してほしい。

 以上の手順によりCLRバージョンが決定したら、いよいよCLRの実行エンジン(以降、「CLRローダー」)が動作する。

 なお、実はmscoree.dllの仕事範囲は、CLRローダーを動作させる前までのつなぎでしかない。それ以降の本格的なCLRの処理は、ほかの.DLLファイル(例えばmscorwks.dllやmscorsvr.dllなど)に処理が引き継がれる。このためmscoree.dllは、「CLRの詰め木(シム)」を意味する“CLR Shim”と呼ばれることがある。

(2)CLRローダーの始動

 CLRローダーを動かすために、(CorBindToRuntimeEx関数というWin32 APIを呼び出して)決定したバージョンのCLRローダーを、ホスト・プロセス(=現在プログラムを実行中のプロセス)に読み込む。これによってCLRローダーが動き始める。

 さらにこの際、デフォルト(=既定)のアプリケーション・ドメイン(=.NET専用のプロセス)が自動的に作成される。

CLRローダーの<アセンブリ・ローダー>による「アセンブリのロード」

 動作し始めたCLRローダーはアセンブリ・ローダー(Assembly Loader)によって、(System.AppDomainオブジェクトのLoad メソッドという.NET APIを活用して)既定のアプリケーション・ドメインの中にアセンブリをロード(=展開)する。

 これにより、以降のアセンブリ(=プログラム本体)のすべての実行処理は、.NETの世界で進められることになる。要するにここで初めて実行環境の舞台が、Win32世界(Windowsローダー)から.NET世界(CLRローダー)に完全に遷移したといえるだろう。

CLRローダーの<ポリシー・マネージャ>による「厳密名の検証」

 アセンブリがロードされたら、ポリシー・マネージャ(Policy Manager)が厳密名の検証処理を開始する。

 なお厳密名とは簡単にいうと、アセンブリに対して付けられる.NET特有の電子署名のことだ。厳密名はアセンブリに任意で付けることができるが、デフォルトでは付けられない(厳密名の付け方は本稿では割愛する)。厳密名を付けると、ファイルの改ざんをチェックできるので、ファイルを偽装・改ざんするタイプのウイルスを無効化するのに効き目がある。また.NETにはこの厳密名を使用したバージョン管理機能が用意されているので、厳密名を付ければその機能を活用できるというメリットもある。

 ポリシー・マネージャは、アセンブリのCLRヘッダ(マニフェスト)を解析して、厳密名の有無を確認する。そこで厳密名が設定されていなければ、厳密名の検証処理はスキップする。設定されていれば、厳密名の情報を取得して、それが正当なものかどうかを検証し、偽装・改ざんされたファイルもしくは意図と異なるファイルの実行を防止する。

 .NETではWindowsの世界に比べてアプリケーション実行時のセキュリティ・チェックが非常に厳重になっているが、ここがCLRの最初のセキュリティ・ポイントだ。

CLRローダーによる「MainメソッドのILコード上の位置の特定」

 次にCLRローダーは、エントリ・ポイントとなるメソッドのILコード上での位置(=アドレス)を特定する。具体的には、再度CLRヘッダ(マニフェスト)を解析してエントリ・ポイントのメタデータ・トークン(つまりILコード上のMainメソッドの位置)を取得する。

 本稿のサンプル・アプリケーションでは、次のようなコードがエントリ・ポイントである。

  static void Main()
  {
    ……中略……
  }

 このようにC#やVisual Basic .NETでは、基本的にMainメソッドがエントリ・ポイントのメソッドとなる。

CLRローダーの<クラス・ローダー>による「Mainメソッド呼び出しに必要な型情報の収集」

 エントリ・ポイントであるMainメソッドの位置が判明したら、次にクラス・ローダー(Class Loader)がそのMainメソッドを公開している型(例えば本稿では「WindowsApplication1名前空間のForm1クラス」)を取得する。そして、動的に割り当てたメモリ上に、その型の「内部データ構造」(Internal Data Structure)をロードする。

 クラス・ローダーは、ロードしたクラス情報のメソッド1つ1つに対して、「スタブ」を設定する。スタブとは「ほかのプログラムを呼び出すための仲介となるプログラム」のことだが、ここではそれぞれのメソッドを呼び出すと、(メソッドそのものではなく)メソッドに設定されたスタブを仲介して(次で説明する)「JITコンパイラ」(JIT Compiler。“JITter”ともいう)が呼び出される仕組みになっている。

CLRローダーによる「Mainメソッドの呼び出し」

 ここでCLRローダーは実際にMainメソッドを呼び出そうとする。そこで先ほどのスタブを仲介して、JITコンパイラが働きだす。具体的には、(次で説明する)「型の妥当性検証」と「変換処理」が実行され、その後のでやっと実際にMainメソッドが実行されることになる。

CLRローダーの<JITコンパイラ(型ベリファイア)>による「型の妥当性検証」

 JITコンパイラが呼び出されると、型ベリファイア(Type Verifier)によって、次の検証処理を実行する。

  • 呼び出そうとしている型やメソッド(のメタデータ情報)が正確・正当なものなのかどうかを調べる

  • 呼び出そうとしている型やメソッドのシグネチャが正しいかどうかを調べる

 これらの検証で失敗した場合、例外が発生してプログラムの実行は終了する。これが「タイプ・セーフ」(Type Safe)と呼ばれるセキュリティ・ポイントである。

CLRローダーの<JITコンパイラ>による「ILコードからネイティブ・コードへの変換」

 型の検証処理をパスすると、JIT(Just-In-Time)コンパイラは本来の仕事を開始する。ここがCLRのキモの部分だ。

 つまりJITコンパイラは、その場で(=ジャスト・イン・タイムで)、Mainメソッドの「ILコード」を「マシン環境に最適化したCPUネイティブの命令」(基本的には「x86機械語コード」。以降、ネイティブ・コード)にコンパイルして変換する。そして、変換したネイティブ・コードを、動的に割り当てたメモリ・ブロックに格納する(なおこのメモリ上のネイティブ・コードはアプリケーションを終了すると同時に自動的に破棄される)。

 続いてJITコンパイラは、で用意した型の内部データ構造のMainメソッドのスタブを、ここでコンパイルされたネイティブ・コードのMainメソッドのアドレスに置き換える。これにより、次にMainメソッドを呼び出すときには、(スタブからJITコンパイラが呼ばれるわけではなく)直接にネイティブ・コードを呼び出せる(ただしMainメソッドに限っては基本的に2度呼ぶことはないだろうが……)。

 このように、JITコンパイラによって検証とコンパイルが済んだメソッドは、それが2回以上呼ばれる際にはJITコンパイラを呼ばないようにして、処理を最適化している。

CLRローダーの<コード・マネージャ>による「ネイティブ・コードの実行と管理」

 Mainメソッドのコードは、CPUが読み取って処理できるネイティブ・コードへと変換されているので、後はそのコードへジャンプすればプログラムが動き始める。これ以降のコード実行は、コード・マネージャ(Code Manager)により管理・制御される。これにより、以下のようなサービスが提供される。

  • ガーベジ・コレクション(Garbage Collection):未使用のオブジェクトを自動的に破棄してメモリを解放するサービス

  • 例外管理(Exception Manager):例外処理メカニズムによりアプリケーション・エラーをすべて管理できるサービス

  • セキュリティ(Security Permission Enforcement):コード・アクセス・セキュリティ(code-access security)によりメソッドなどのコードの実行権限を細かく制御できるサービス

●Mainメソッド以降のCLRローダーの処理の流れについて

 Mainメソッド以降の処理の流れでは、メソッドが呼び出されるたびに、その事前処理として、クラス・ローダーによって、メソッド内のすべての型の内部データ構造がメモリ上にロードされ、メソッドにスタブが設定される。当然ながら、すでにメモリ上にロードされている型はロードされない。

 本稿のサンプル・アプリケーションの例でいえば、MainメソッドでApplicationクラスとForm1クラスの型を使用しているので、Mainメソッドを呼び出すときにはクラス・ローダーによってこれらの型情報が検出されて、メモリ上にロードされることになる。ただしForm1の型情報は、Mainメソッドをロードするときにすでにロードされているので実際にはここではロードされない。

  static void Main()
  {
    Application.Run(new Form1());
  }

 なお型情報の検出は、アセンブリの型メタデータを参照して行われる。その型が外部アセンブリのものである場合には、アプリケーション構成ファイル(.configファイル)のコードベース(<codeBase>要素)や、GAC(Global Assembly Cache:グローバル・アセンブリ・キャッシュ)、アプリケーション・パスなどに基づいて外部アセンブリの場所を特定し、上記の「アセンブリのロード」「厳密名の検証」と同じような処理が実行される。なお、GACのアセンブリを利用する場合は、厳密名の検証処理はスキップされる。

 型がメモリにロードされたら、実際にメソッドが呼ばれる。この後はと同じような処理の流れになる。違うのは、Mainメソッドがほかのメソッドになることである。

 以上3回にわたり、.NETアプリケーションが動作する仕組みを、コンピュータの世界、Windows(Win32)の世界、.NETの世界という観点から解説した。これで本連載は完結だ。最後までお付き合いいただいたことに感謝の意を表したい。End of Article

 

 INDEX
  .NETの動作原理を基礎から理解する!
  第3回 .NETアプリケーションが実行される仕組み
    1..NETプログラムの構造(箱と本体)
  2..NETプログラム実行の処理の流れ
 
インデックス・ページヘ  「.NETの動作原理を基礎から理解する!」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間