JIT and Run
.NET Framework の内部: CLR がランタイム オブジェクトを作成するしくみ
Hanu Kommalapati and Tom Christian
翻訳元: Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects (英語)
この記事で使用する技術: .NET Framework および C#
この記事で取り上げる話題:
- SystemDomain、SharedDomain、および DefaultDomain
- オブジェクトのレイアウトおよびその他のメモリの詳細
- メソッド テーブルのレイアウト
- メソッドのディスパッチ
目次
- CLR のブートストラップによって作成されるドメイン
- System Domain
- SharedDomain
- DefaultDomain
- LoaderHeaps
- 型の基礎
- ObjectInstance
- MethodTable
- Base Instance Size
- メソッド スロット テーブル
- MethodDesc
- Interface Vtable Map と Interface Map
- 仮想ディスパッチ
- 静的変数
- EEClass
- まとめ
- 補足記事: Son of Strike
この先しばらくの間は、共通言語ランタイム (CLR) が Windows アプリケーション開発の主要なインフラストラクチャとなります。したがって、CLR を深く理解することは、効率的かつ強力なアプリケーションを開発するうえで役に立ちます。この記事では、オブジェクト インスタンスのレイアウト、メソッド テーブルのレイアウト、メソッドのディスパッチ、インターフェイス ベースのディスパッチ、さまざまなデータ構造など、CLR の内部について説明します。
ここでは、C# で書かれたごく単純なコードをサンプルとして使用します。したがって、特に説明がない限り、使用されている言語の構文は C# のものになります。ここで取り上げるデータ構造やアルゴリズムの一部は、Microsoft .NET Framework 2.0 では変更されます。しかし、概念については、ほとんど変わらないはずです。記事の中で取り上げるデータ構造の調査には、Visual Studio .NET 2003 のデバッガと、デバッガ拡張機能 Son of Strike (SOS) を使用します。SOS は CLR 内部のデータ構造を認識し、有用な情報をダンプします。Visual Studio .NET 2003 のデバッガ プロセスに SOS.dll を読み込む方法については、補足記事「Son of Strike」を参照してください。この記事の中で取り上げられているクラスには、対応する実装が Shared Source CLI (SSCLI) にあります。SSCLI は、msdn.microsoft.com/net/sscli からダウンロードできます。SSCLI のコードは数メガバイトに及ぶため、参照先の構造体を探すときには、図 1 を使用すると便利です。
アイテム | SSCLI のパス |
AppDomain | \sscli\clr\src\vm\appdomain.hpp |
AppDomainStringLiteralMap | \sscli\clr\src\vm\stringliteralmap.h |
BaseDomain | \sscli\clr\src\vm\appdomain.hpp |
ClassLoader | \sscli\clr\src\vm\clsload.hpp |
EEClass | \sscli\clr\src\vm\class.h |
FieldDescs | \sscli\clr\src\vm\field.h |
GCHeap | \sscli\clr\src\vm\gc.h |
GlobalStringLiteralMap | \sscli\clr\src\vm\stringliteralmap.h |
HandleTable | \sscli\clr\src\vm\handletable.h |
InterfaceVTableMapMgr | \sscli\clr\src\vm\appdomain.hpp |
Large Object Heap | \sscli\clr\src\vm\gc.h |
LayoutKind | \sscli\clr\src\bcl\system\runtime\interopservices\layoutkind.cs |
LoaderHeaps | \sscli\clr\src\inc\utilcode.h |
MethodDescs | \sscli\clr\src\vm\method.hpp |
MethodTables | \sscli\clr\src\vm\class.h |
OBJECTREF | \sscli\clr\src\vm\typehandle.h |
SecurityContext | \sscli\clr\src\vm\security.h |
SecurityDescriptor | \sscli\clr\src\vm\security.h |
SharedDomain | \sscli\clr\src\vm\appdomain.hpp |
StructLayoutAttribute | \sscli\clr\src\bcl\system\runtime\interopservices\attributes.cs |
SyncTableEntry | \sscli\clr\src\vm\syncblk.h |
System namespace | \sscli\clr\src\bcl\system |
SystemDomain | \sscli\clr\src\vm\appdomain.hpp |
TypeHandle | \sscli\clr\src\vm\typehandle.h |
本題に入る前に、1 つ注意点があります。この記事の情報が有効なのは、x86 プラットフォームで実行されている .NET Framework 1.1 に対してのみです (Shared Source CLI についても、一部の相互運用のシナリオを始めとする例外を除いて、ほとんどの内容が Shared Source CLI 1.0 のみを対象としています)。この記事の情報は .NET Framework 2.0 では変更されます。したがって、ここで紹介する内部構造が変わらないことを前提にしてソフトウェアを開発しないでください。
ページのトップへ
1. CLR のブートストラップによって作成されるドメイン
CLR は、マネージ コードの最初の行を実行する前に、3 つのアプリケーション ドメインを作成します。そのうちの 2 つは、マネージ コード内からも CLR ホストからも見えません。これらを作成できるのは、シム (mscoree.dll) と mscorwks.dll (マルチプロセッサ システムの場合は mscorsvr.dll) を利用する CLR のブートストラップ プロセスだけです。図 2 に示されているように、この 2 つのドメインは System Domain と Shared Domain です。これらはシングルトンです。3 つ目のドメインは、既定の AppDomain です。これは、AppDomain クラスのインスタンスで、名前を持つ唯一のドメインです。コンソール プログラムなどの単純な CLR ホストでは、既定のドメインの名前は実行イメージの名前になります。追加のドメインは、AppDomain.CreateDomain メソッド (マネージ コードの場合) または ICORRuntimeHost インターフェイス (アンマネージ ホスト コードの場合) を使って作成できます。ASP.NET のような複雑なホストは、特定の Web サイトのアプリケーションの数に基づいて複数のドメインを作成します。
図 2 CLR のブートストラップによって作成されるドメイン
ページのトップへ
2. System Domain
SystemDomain は、SharedDomain と既定の AppDomain の作成と初期化を司ります。そのほか、システム ライブラリ mscorlib.dll を SharedDomain に読み込んだり、プロセス全体で使用されるリテラル文字列を暗黙的または明示的にインターン化したりするのも、SystemDomain の役割です。
文字列のインターン化は最適化機能の 1 つですが、.NET Framework 1.1 ではアセンブリでこの機能をバイパスすることができないため、あまり使い勝手がよくありません。とはいえ、この機能によって、特定のリテラルに対して保持される文字列のインスタンスをアプリケーション ドメイン全体で 1 つだけにすることができるため、メモリが節約されます。
SystemDomain には、プロセス全体で使用されるインターフェイス ID を生成する役割もあります。この ID は、各 AppDomain で InterfaceVtableMap を作成する際に使用されます。SystemDomain はプロセス内のすべてのドメインを追跡し、AppDomain のロードとアンロードの機能を実装します。
ページのトップへ
3. SharedDomain
ドメインに依存しないコードはすべて SharedDomain に読み込まれます。システム ライブラリの mscorlib は、すべての AppDomain のユーザー コードで必要とされるため、 自動的に SharedDomain に読み込まれます。また、System 名前空間の基本型 (Object、ValueType、Array、Enum、String、Delegate など) は、CLR のブートストラップ プロセスの間にこのドメインにプリロードされます。ユーザー コードをこのドメインに読み込むこともできます。ユーザー コードを読み込むには、LoaderOptimization 属性を使用します。この属性は、CLR ホスト アプリケーションで、CorBindToRuntimeEx の呼び出し時に指定します。コンソール プログラムでコードを SharedDomain に読み込むには、アプリケーションの Main メソッドで System.LoaderOptimizationAttribute を設定します。SharedDomain では、このほか、ベース アドレスをインデックスとするアセンブリ マップの管理も行われます。このアセンブリ マップは、DefaultDomain に読み込まれているアセンブリや、マネージ コードで作成されるその他の AppDomain の、共有されている依存関係を管理するためのルックアップ テーブルとして機能します。DefaultDomain は、共有されないユーザー コードが読み込まれる場所です。
ページのトップへ
4. DefaultDomain
DefaultDomain は AppDomain のインスタンスで、アプリケーションのコードは通常ここで実行されます。実行時に追加の AppDomain の作成を必要とするアプリケーションもありますが (プラグイン アーキテクチャを持つアプリケーションや、実行時に大量のコードを生成するアプリケーションなど)、ほとんどのアプリケーションでは存続期間全体で 1 つのドメインが作成されます。このドメインで実行されるすべてのコードは、ドメイン レベルでコンテキストにバインドされています。アプリケーションに複数の AppDomain がある場合、ドメイン間のアクセスは、.NET リモート処理のプロキシを通じて行われます。ドメイン内のコンテキストの境界を追加するには、System.ContextBoundObject から継承される型を使用します。各 AppDomain には、それぞれ固有の SecurityDescriptor、SecurityContext、および DefaultContext と、固有のローダー ヒープ (High-Frequency Heap、Low-Frequency Heap、および Stub Heap)、Handle Table (Handle Table、Large Object Heap Handle Table)、Interface Vtable Map Manager、および Assembly Cache があります。
ページのトップへ
5. LoaderHeaps
LoaderHeaps は、ドメインの存続期間全体を通じて存続する実行時のさまざまな CLR アイテムや最適化アイテムを読み込むための場所です。これらのヒープは、断片化を最小限にするために、予測可能なチャンクで拡大します。LoaderHeaps と GC (ガベージ コレクタ) Heap (対称型マルチプロセッサ (SMP) の場合は複数の GC Heap があります) の違いは、GC Heap がオブジェクト インスタンスをホストするのに対し、LoaderHeaps は型システムをつなぎ合わせるという点にあります。MethodTables、MethodDescs、FieldDescs、Interface Map などの頻繁にアクセスされるアイテムは HighFrequencyHeap に割り当てられ、EEClass や ClassLoader およびそのルックアップ テーブルなどのあまり頻繁にアクセスされないデータ構造は LowFrequencyHeap に割り当てられます。StubHeap は、コード アクセス セキュリティ (CAS)、COM ラッパー呼び出し、および P/Invoke のためのスタブをホストします。
ドメインと LoaderHeaps の高レベルの概要を把握できたので、今度は、図 3 の単純なアプリケーションのコンテキストで、これらの物理的な詳細について見てみましょう。"mc.Method1();" でプログラムの実行を中断して、SOS デバッガ拡張機能のコマンド DumpDomain を使用してドメインの情報をダンプします (SOS の読み込みについては、補足記事「Son of Strike」を参照してください)。
図 3 Sample1.exe
using System;
public interface MyInterface1
{
void Method1();
void Method2();
}
public interface MyInterface2
{
void Method2();
void Method3();
}
class MyClass : MyInterface1, MyInterface2
{
public static string str = "MyString";
public static uint ui = 0xAAAAAAAA;
public void Method1() { Console.WriteLine("Method1"); }
public void Method2() { Console.WriteLine("Method2"); }
public virtual void Method3() { Console.WriteLine("Method3"); }
}
class Program
{
static void Main()
{
MyClass mc = new MyClass();
MyInterface1 mi1 = mc;
MyInterface2 mi2 = mc;
int i = MyClass.str.Length;
uint j = MyClass.ui;
mc.Method1();
mi1.Method1();
mi1.Method2();
mi2.Method2();
mi2.Method3();
mc.Method3();
}
}
以下は、その出力を編集したものです。
!DumpDomain
System Domain: 793e9d58, LowFrequencyHeap: 793e9dbc,
HighFrequencyHeap: 793e9e14, StubHeap: 793e9e6c,
Assembly: 0015aa68 [mscorlib], ClassLoader: 0015ab40
Shared Domain: 793eb278, LowFrequencyHeap: 793eb2dc,
HighFrequencyHeap: 793eb334, StubHeap: 793eb38c,
Assembly: 0015aa68 [mscorlib], ClassLoader: 0015ab40
Domain 1: 149100, LowFrequencyHeap: 00149164,
HighFrequencyHeap: 001491bc, StubHeap: 00149214,
Name: Sample1.exe, Assembly: 00164938 [Sample1],
ClassLoader: 00164a78
コンソール プログラム Sample1.exe は、"Sample1.exe" という名前の AppDomain に読み込まれます。mscorlib.dll は SharedDomain に読み込まれますが、コア システム ライブラリであるため、SystemDomain のところにも表示されています。各ドメインには、HighFrequencyHeap、LowFrequencyHeap、および StubHeap が割り当てられています。SystemDomain と SharedDomain は同じ ClassLoader を使用しますが、Default AppDomain は固有の ClassLoader を使用します。
この出力には、ローダー ヒープの予約サイズとコミット サイズは示されていません。HighFrequencyHeap の最初の予約サイズは 32KB、コミット サイズは 4KB です。LowFrequencyHeap と StubHeap の最初の予約サイズは 8KB、コミット サイズは 4KB です。SOS の出力には、InterfaceVtableMap ヒープも示されていません。InterfaceVtableMap (ここでは IVMap と呼びます) は各ドメインに 1 つずつあり、ドメインの初期化フェーズの間にその LoaderHeap に作成されます。IVMap ヒープの最初の予約サイズは 4KB、コミット サイズは 4KB です。IVMap の重要性については、後ほど、型のレイアウトの解説の中で説明します。
図 2 には、ローダー ヒープとの意味的な違いを示すために、既定の Process Heap、JIT Code Heap、GC Heap (小さいオブジェクト用)、および Large Object Heap (サイズが 85000 バイト以上のオブジェクト用) も含まれています。Just-In-Time (JIT) コンパイラは x86 命令を生成すると、それらを JIT Code Heap に格納します。GC Heap と Large Object Heap はガベージ コレクションの対象となるヒープで、マネージ オブジェクトはここでインスタンス化されます。
ページのトップへ
6. 型の基礎
型は .NET プログラミングの基本単位です。C# では、class、struct、および interface の各キーワードを使って型を宣言できます。ほとんどの型はプログラマによって明示的に作成されますが、特殊な相互運用のケースやリモート オブジェクト呼び出し (.NET リモート処理) のシナリオでは、.NET CLR が暗黙的に型を生成します。このようにして生成される型には、COM 呼び出し可能ラッパーやランタイム呼び出し可能ラッパー、透過プロキシなどがあります。
ここでは、.NET の型の基礎について、オブジェクト参照を含むスタック フレームから見ていくことにします (一般にスタックは、オブジェクト インスタンスが作成される場所の 1 つです)。図 4 のコードには、静的メソッドを呼び出すコンソール エントリ ポイントを持つ単純なプログラムが含まれています。Method1 は、バイト配列を含む SmallClass 型のインスタンスを作成します。これにより、Large Object Heap に作成されるオブジェクト インスタンスの例を見ることができます。些細なコードですが、現在の議論には十分です。
図 4 大きなオブジェクトと小さなオブジェクト
using System;
class SmallClass
{
private byte[] _largeObj;
public SmallClass(int size)
{
_largeObj = new byte[size];
_largeObj[0] = 0xAA;
_largeObj[1] = 0xBB;
_largeObj[2] = 0xCC;
}
public byte[] LargeObj
{
get { return this._largeObj; }
}
}
class SimpleProgram
{
static void Main(string[] args)
{
SmallClass smallObj = SimpleProgram.Create(84930,10,15,20,25);
return;
}
static SmallClass Create(int size1, int size2, int size3,
int size4, int size5)
{
int objSize = size1 + size2 + size3 + size4 + size5;
SmallClass smallObj = new SmallClass(objSize);
return smallObj;
}
}
図 5 は、Create メソッド内の "return smallObj;" という行に設定されたブレークポイントにおける、スタック フレームのスナップショットを表しています。これは、典型的な fastcall のスタック フレームです (fastcall は .NET の呼び出し規約で、可能な場合は関数の引数をレジスタで渡すように指定します。その他の引数はすべて右から左の順にスタックに渡されて、呼び出された関数によってポップされます)。値型のローカル変数 objSize は、スタック フレーム内にインライン化されます。smallObj のような参照型の変数は、固定サイズ (4 バイト DWORD) としてスタックに格納されます。ここには、標準の GC Heap に割り当てられているオブジェクト インスタンスのアドレスが含まれます。これは、従来の C++ ではオブジェクト ポインタですが、マネージ コードではオブジェクト参照になります。いずれにしろ、そこにはオブジェクト インスタンスのアドレスが含まれています。ここでは、オブジェクト参照が指すアドレスにあるデータ構造を ObjectInstance と呼びます。
図 5 SimpleProgram のスタック フレームとヒープ
標準の GC Heap にある smallObj のオブジェクト インスタンスには、_largeObj という Byte[] が含まれています。サイズは 85000 バイトです (図では 85016 バイトになっていますが、これは格納領域の実際のサイズです)。CLR は、サイズが 85000 バイト以上のオブジェクトをそれより小さいオブジェクトとは違う方法で処理します。サイズの大きなオブジェクトは Large Object Heap (LOH) に割り当てられ、サイズの小さなオブジェクトは標準の GC Heap に作成されます。これにより、オブジェクトの割り当てとガベージ コレクションが最適化されます。LOH はコンパクト化されないのに対し、GC Heap はガベージ コレクションが発生するたびにコンパクト化されます。さらに、LOH がガベージ コレクションの対象になるのは、完全なガベージ コレクションのときだけです。
smallObj の ObjectInstance には、対応する型の MethodTable を指す TypeHandle が含まれています。MethodTable は、宣言された型ごとに 1 つずつあります。同じ型のオブジェクト インスタンスはすべて同じ MethodTable を指します。MethodTable には、型の種類 (インターフェイス、抽象クラス、具象クラス、COM ラッパー、およびプロキシ) に関する情報、実装されているインターフェイスの数、メソッド ディスパッチのためのインターフェイス マップ、メソッド テーブルのスロットの数、および実装を指すスロットのテーブルが含まれます。
MethodTable が指す重要なデータ構造の 1 つが EEClass です。CLR クラス ローダーは、MethodTable がレイアウトされる前に、メタデータから EEClass を作成します。図 4 では、SmallClass の MethodTable が SmallClass の EEClass を指しています。これらの構造体は、モジュールとアセンブリを指しています。MethodTable と EEClass は、通常、ドメイン固有のローダー ヒープに割り当てられます。Byte[] は特殊なケースで、Byte[] の MethodTable と EEClass は SharedDomain のローダー ヒープに割り当てられます。ローダー ヒープは AppDomain 固有のものであり、これまでに挙げたすべてのデータ構造は、いったん読み込まれると、AppDomain がアンロードされるまで保持されます。また、既定の AppDomain はアンロードできないため、そのコードは CLR がシャットダウンするまで存続します。
ページのトップへ
7. ObjectInstance
既に述べたように、すべての値型のインスタンスは、スレッド スタックにインライン化されるか、GC Heap にインライン化されます。すべての参照型は、GC Heap または LOH に作成されます。図 6 は、典型的なオブジェクト インスタンスのレイアウトを表しています。オブジェクトは、スタックベースのローカル変数、相互運用または P/Invoke のシナリオのハンドル テーブル、レジスタ (メソッドの実行中の this ポインタおよびメソッドの引数)、またはファイナライザ メソッドを持つオブジェクトのファイナライザ キューから参照できます。OBJECTREF は、オブジェクト インスタンスの先頭ではなく、DWORD オフセット (4 バイト) を指します。この DWORD は Object Header と呼ばれ、SyncTableEntry テーブルへのインデックス (1 を基数とする syncblk 番号) を保持します。チェーニングはインデックスを通じて行われるため、CLR は、必要に応じてサイズを増やしながらこのテーブルをメモリ内で移動できます。SyncTableEntry はオブジェクトへの弱い参照を保持しているため、CLR は SyncBlock の所有元を追跡できます。弱い参照では、ほかに強い参照が存在しない場合に、そのオブジェクトをガベージ コレクトすることができます。SyncTableEntry には SyncBlock へのポインタも格納されています。SyncBlock には有用な情報が含まれていますが、オブジェクトのすべてのインスタンスによって必要とされることはほとんどありません。たとえば、オブジェクトのロック、ハッシュ コード、サンキング データ、AppDomain のインデックスなどの情報が含まれています。ほとんどのオブジェクト インスタンスでは、実際の SyncBlock に格納領域が割り当てられていないため、syncblk 番号はゼロになります。しかし、次のように、実行スレッドが lock(obj) や obj.GetHashCode などのステートメントに遭遇すると、事情が変わります。
図 6 オブジェクト インスタンスのレイアウト
SmallClass obj = new SmallClass()
// 何らかの処理
lock(obj) { /* 同期化された処理 */ }
obj.GetHashCode();
このコードでは、smallObj が使用する開始 syncblk 番号はゼロ (syncblk なし) です。CLR は、lock ステートメントに遭遇すると、syncblk エントリを作成し、対応する番号でオブジェクト ヘッダーを更新します。C# の lock キーワードが、Monitor クラスを使用する try-finally に展開すると、同期のために Monitor オブジェクトが syncblk に作成されます。GetHashCode メソッドへの呼び出しでは、オブジェクトのハッシュ コードが syncblk に設定されます。
SyncBlock には、COM 相互運用機能で使用されるフィールドや、デリゲートのアンマネージ コードへのマーシャリングのためのフィールドなど、その他のフィールドもありますが、オブジェクトの典型的な使用方法という議論からは外れています。
ObjectInstance の syncblk 番号の後には、TypeHandle が続きます。議論の流れのために、TypeHandle の前にインスタンス変数について説明します。インスタンス フィールドの変数リストは TypeHandle の後に続きます。既定では、インスタンス フィールドはメモリが効率的に使用されるようにパックされ、アラインメントのためのパディングは最小限に抑えられます。図 7 のコードは、さまざまなサイズの一連のインスタンス変数を持つ SimpleClass を示しています。
図 7 インスタンス変数を持つ SimpleClass
class SimpleClass
{
private byte b1 = 1; // 1 バイト
private byte b2 = 2; // 1 バイト
private byte b3 = 3; // 1 バイト
private byte b4 = 4; // 1 バイト
private char c1 = 'A'; // 2 バイト
private char c2 = 'B'; // 2 バイト
private short s1 = 11; // 2 バイト
private short s2 = 12; // 2 バイト
private int i1 = 21; // 4 バイト
private long l1 = 31; // 8 バイト
private string str = "MyString"; // 4 バイト (OBJECTREF のみ)
//インスタンス変数の合計サイズ = 28 バイト
static void Main()
{
SimpleClass simpleObj = new SimpleClass();
return;
}
}
図 8 は、Visual Studio デバッガのメモリ ウィンドウに表示された SimpleClass のオブジェクト インスタンスの例です。ここでは、図 7 の return ステートメントにブレークポイントを設定し、ECX レジスタに格納されている simpleObj のアドレスを使用して、オブジェクト インスタンスをメモリ ウィンドウに表示しています。最初の 4 バイト ブロックは syncblk 番号です。このインスタンスは同期コードでは使用されていないため (また、HashCode にもアクセスしていないため)、syncblk 番号は 0 に設定されています。スタック変数に格納されているオブジェクト参照は、オフセット 4 から始まる 4 バイトを指しています。Byte 変数の b1、b2、b3、および b4 は、すべて一緒にパックされています。short 変数の s1 と s2 も一緒にパックされています。String 変数 str は 4 バイトの OBJECTREF で、GC Heap にある文字列の実際のインスタンスを指しています。String は特殊な型で、同じリテラルを含むすべてのインスタンスは、アセンブリ読み込みプロセスの間に、グローバル文字列テーブル内の同じインスタンスを指すように処理されます。このプロセスは文字列のインターン化と呼ばれ、メモリの使用を最適化するために行われます。前にも述べたように、.NET Framework 1.1 では、アセンブリでこのインターン化のプロセスをバイパスすることはできません。ただし、将来のバージョンの CLR では可能になるかもしれません。
図 8 オブジェクト インスタンスのデバッガ メモリ ウィンドウ
このように、既定では、ソース コードのメンバ変数の語順はメモリ内では維持されません。語順がメモリ内でも維持される必要がある相互運用のシナリオでは、LayoutKind 列挙体を引数として受け取る StructLayoutAttribute を使用することができます。LayoutKind.Sequential は、マーシャリングされたデータの語順を維持します。ただし、.NET Framework 1.1 では、マネージ コードのレイアウトには作用しません (.NET Framework 2.0 では作用するようになります)。追加のパディングやフィールド順の明示的な制御が実際に必要となる相互運用のシナリオでは、LayoutKind.Explicit をフィールド レベルの FieldOffset 属性と組み合わせて使うことができます。
メモリの生の内容を確認できたので、今度は、SOS を使用してオブジェクト インスタンスを見てみましょう。SOS の便利なコマンドの 1 つが DumpHeap です。このコマンドを使用すると、ヒープのすべての内容や、特定の型のすべてのインスタンスを表示できます。DumpHeap では、レジスタに頼らずに、作成したインスタンスのアドレスのみを表示できます。
!DumpHeap -type SimpleClass
Loaded Son of Strike data table version 5 from
"C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\mscorwks.dll"
Address MT Size
00a8197c 00955124 36
Last good object: 00a819a0
total 1 objects
Statistics:
MT Count TotalSize Class Name
955124 1 36 SimpleClass
オブジェクトの合計サイズは 36 バイトです。文字列のサイズがどれほど大きくても、SimpleClass のインスタンスに含まれるのは DWORD の OBJECTREF のみです。SimpleClass のインスタンス変数が占有しているのは 28 バイトだけで、残りの 8 バイトには、TypeHandle (4 バイト) と syncblk 番号 (4 バイト) が含まれています。インスタンス simpleObj のアドレスがわかったので、DumpObj コマンドを使用してこのインスタンスの内容をダンプしてみましょう。結果は次のようになります。
!DumpObj 0x00a8197c
Name: SimpleClass
MethodTable 0x00955124
EEClass 0x02ca33b0
Size 36(0x24) bytes
FieldDesc*: 00955064
MT Field Offset Type Attr Value Name
00955124 400000a 4 System.Int64 instance 31 l1
00955124 400000b c CLASS instance 00a819a0 str
<< 簡潔にするために一部のフィールドの表示を省略しています >>
00955124 4000003 1e System.Byte instance 3 b3
00955124 4000004 1f System.Byte instance 4 b4
先に述べたように、C# コンパイラによってクラスに対して生成される既定のレイアウトは LayoutType.Auto (構造体の場合は LayoutType.Sequential) であるため、クラス ローダーはパディングが最小限になるようにインスタンス フィールドを再配置します。ObjSize を使用すると、インスタンス str によって占有されている領域を含むグラフをダンプできます。出力は次のようになります。
!ObjSize 0x00a8197c
sizeof(00a8197c) = 72 ( 0x48) bytes (SimpleClass)
SimpleClass のインスタンスのサイズ (36 バイト) をオブジェクト グラフ全体のサイズ (72 バイト) から引くと、str のサイズが 36 バイトであることがわかります。インスタンス str をダンプしてこのことを確認してみましょう。出力は次のようになります。
!DumpObj 0x00a819a0
Name: System.String
MethodTable 0x009742d8
EEClass 0x02c4c6c4
Size 36(0x24) bytes
string のインスタンス str のサイズ (36 バイト) を SimpleClass のインスタンスのサイズ (36 バイト) に加えると、合計サイズは、ObjSize コマンドで報告されたとおり、72 バイトになります。
ObjSize では、syncblk インフラストラクチャによって占有されるメモリは含まれないので注意してください。また、.NET Framework 1.1 では、GDI オブジェクト、COM オブジェクト、ファイル ハンドルなどのアンマネージ リソースによって占有されているメモリは CLR によって認識されないため、このコマンドでは報告されません。
MethodTable へのポインタである TypeHandle は、syncblk 番号の直後に配置されます。オブジェクト インスタンスが作成される前に CLR は、読み込まれている型を検索して、見つからなかった場合には型を読み込みます。その後、MethodTable のアドレスを取得し、オブジェクト インスタンスを作成して、オブジェクト インスタンスに TypeHandle の値を設定します。JIT コンパイラによって生成されたコードは、TypeHandle を使用して MethodTable を見つけます。これにより、メソッド ディスパッチが行われます。CLR は、読み込まれた型を MethodTable を通じて戻って参照する必要が生じるたびに、TypeHandle を使用します。
ページのトップへ
8. MethodTable
各クラスやインターフェイスは、いったん AppDomain に読み込まれると、メモリ内では MethodTable のデータ構造によって表されます。これは、オブジェクトの最初のインスタンスが作成される前に行われるクラス読み込みプロセスの結果です。ObjectInstance が状態を表すのに対し、MethodTable は動作を表します。MethodTable は、オブジェクト インスタンスを、言語コンパイラによって生成されたメモリマップ メタデータ構造体へと EEClass を通じてバインドします。MethodTable の情報とその背後にあるデータ構造には、System.Type を通じてマネージ コードからアクセスできます。MethodTable へのポインタは、マネージ コードからでも、Type.RuntimeTypeHandle プロパティを通じて取得できます。ObjectInstance に格納されている TypeHandle は、MethodTable の先頭からのオフセットを指します。このオフセットは既定では 12 バイトで、GC の情報が含まれています。これについては、ここでは説明しません。
図 9 は、MethodTable の典型的なレイアウトを表しています。ここでは TypeHandle の重要なフィールドをいくつか取り上げます。完全なリストについてはこの図を参照してください。では、実行時のメモリ プロファイルと直接の関連がある Base Instance Size から見ていきましょう。
図 9 MethodTable のレイアウト
ページのトップへ
9. Base Instance Size
Base Instance Size は、コード内のフィールド宣言に基づいてクラス ローダーが算出したオブジェクトのサイズです。既に説明したように、現在の GC の実装では、少なくとも 12 バイトのサイズがオブジェクト インスタンスに必要になります。クラスにインスタンス フィールドがなにも定義されていない場合のオーバーヘッドは 4 バイトです。残りの 8 バイトは、Object Header (syncblk 番号を含む) と TypeHandle によって使用されます。オブジェクトのサイズも、StructLayoutAttribute によって制御できます。
図 3 (2 つのインターフェイスを持つ MyClass) の MyClass の MethodTable のメモリ スナップショット (Visual Studio .NET 2003 のメモリ ウィンドウ) を、SOS によって生成された出力と比較してみましょう。図 9 では、オブジェクト サイズは 4 バイト オフセットにあり、値は 12 (0x0000000C) バイトです。SOS の DumpHeap の出力は、次のようになります。
!DumpHeap -type MyClass
Address MT Size
00a819ac 009552a0 12
total 1 objects
Statistics:
MT Count TotalSize Class Name
9552a0 1 12 MyClass
ページのトップへ
10. メソッド スロット テーブル
MethodTable 内には、個々のメソッド記述子 (MethodDesc) を指すスロットのテーブルが埋め込まれています。これにより、型の動作が実現されます。メソッド スロット テーブルは、実装メソッドの線形化されたリストに基づいて作成されます。このリストは、 継承された仮想メソッド、導入された仮想メソッド、インスタンス メソッド、静的メソッドという順序でレイアウトされます。
ClassLoader は、現在のクラス、親クラス、およびインターフェイスのメタデータを参照して、メソッド テーブルを作成します。レイアウトのプロセスでは、オーバーライドされている仮想メソッドの置換、隠蔽される親クラスのメソッドの置換、新しいスロットの作成、およびスロットの複製が、必要に応じて行われます。スロットの複製は、各インターフェイスにそれぞれ固有の小さな vtable があるように見せかけるために必要になります。ただし、複製されたスロットは同じ物理実装を指します。MyClass には 3 つのインスタンス メソッド、1 つのクラス コンストラクタ (.cctor)、および 1 つのオブジェクト コンストラクタ (.ctor) があります。オブジェクト コンストラクタは、コンストラクタが明示的に定義されていないすべてのオブジェクトに対して、C# コンパイラによって自動的に生成されます。クラス コンストラクタが生成されるのは、定義および初期化されている静的変数があるからです。図 10 は、MyClass のメソッド テーブルのレイアウトを示しています。このレイアウトには、10 のメソッドが含まれています。これは、IVMap の Method2 のスロットが複製されているからです。IVMap については、この後で説明します。図 11 は、MyClass のメソッド テーブルの SOS ダンプを編集したものです。
図 10 MyClass の MethodTable のレイアウト
どの型についても、最初の 4 つのメソッドは常に、ToString、Equals、GetHashCode、および Finalize になります。これらは、System.Object から継承される仮想メソッドです。Method2 のスロットは複製されていますが、どちらも同じメソッド記述子を指しています。明示的にコーディングされた .cctor と .ctor は、それぞれ静的メソッドおよびインスタンス メソッドとグループ化されます。
図 11 MyClass の MethodTable の SOS ダンプ
!DumpMT -MD 0x9552a0
Entry MethodDesc Return Type Name
0097203b 00972040 String System.Object.ToString()
009720fb 00972100 Boolean System.Object.Equals(Object)
00972113 00972118 I4 System.Object.GetHashCode()
0097207b 00972080 Void System.Object.Finalize()
00955253 00955258 Void MyClass.Method1()
00955263 00955268 Void MyClass.Method2()
00955263 00955268 Void MyClass.Method2()
00955273 00955278 Void MyClass.Method3()
00955283 00955288 Void MyClass..cctor()
00955293 00955298 Void MyClass..ctor()
ページのトップへ
11. MethodDesc
メソッド記述子 (MethodDesc) は、CLR によって認識されるメソッド実装のカプセル化です。メソッド記述子にはいくつかの種類があります。これは、マネージ実装に加えてさまざまな相互運用機能の実装を呼び出すのに役立ちます。ここでは、マネージ MethodDesc のみについて、図 3 のコードのコンテキストで見ていきます。MethodDesc は、クラス読み込みプロセスの一部として生成され、最初は Intermediate Language (IL) を指します。各 MethodDesc は PreJitStub でパディングされています。PreJitStub には、JIT コンパイルをトリガする役割があります。図 12 は、典型的なレイアウトを表しています。メソッド テーブル スロット エントリが実際に指しているのは、実際の MethodDesc データ構造ではなくスタブです。これは、実際の MethodDesc から 5 バイトの負のオフセットにあり、すべてのメソッドが継承する 8 バイトのパディングの一部です。この 5 バイトには、PreJitStub ルーチンへの呼び出しの命令が含まれています。この 5 バイトのオフセットの存在は、SOS の DumpMT の出力 (MyClass の出力は 図 11) からもわかります。というのも、MethodDesc は常に、メソッド スロット テーブル エントリが指す場所の 5 バイト後ろになっているからです。最初の呼び出しの際に、JIT コンパイル ルーチンへの呼び出しが行われます。コンパイルが完了すると、呼び出し命令を含む 5 バイトは、JIT コンパイルされた x86 コードへの無条件ジャンプで上書きされます。
図 12 メソッド記述子
図 12 のメソッド テーブル スロット エントリが指すコードを逆アセンブルすると、PreJitStub への呼び出しが含まれていることがわかります。以下は、JIT コンパイル前の Method 2 の逆アセンブリの表示を要約したものです。
!u 0x00955263
Unmanaged code
00955263 call 003C3538 ;JIT コンパイルされた Method2() への呼び出し
00955268 add eax,68040000h ;これ以降は無視してください
;!u ではコードと見なされています
次に、このメソッドを実行して、同じアドレスを逆アセンブルしてみましょう。
!u 0x00955263
Unmanaged code
00955263 jmp 02C633E8 ;JIT コンパイルされた Method2() への呼び出し
00955268 add eax,0E8040000h ;これ以降は無視してください
;!u ではコードと見なされています
このアドレスの最初の 5 バイトのみがコードで、残りの部分には Method2 の MethodDesc のデータが含まれています。"!u" コマンドではこのことが認識されず、無意味な内容が生成されます。したがって、最初の 5 バイトより後の内容は無視してかまいません。
JIT コンパイル前の CodeOrIL には、IL のメソッド実装の Relative Virtual Address (RVA) が含まれています。このフィールドには、IL であることを示すフラグが設定されています。CLR は、オンデマンド コンパイルの後、このフィールドを JIT コンパイルされたコードのアドレスで更新します。リストのメソッドの中から 1 つを選んで、DumpMT コマンドで JIT コンパイルの前後の MethodDesc をダンプしてみましょう。
!DumpMD 0x00955268
Method Name : [DEFAULT] [hasThis] Void MyClass.Method2()
MethodTable 9552a0
Module: 164008
mdToken: 06000006
Flags : 400
IL RVA : 00002068
コンパイル後の MethodDesc は次のようになります。
!DumpMD 0x00955268
Method Name : [DEFAULT] [hasThis] Void MyClass.Method2()
MethodTable 9552a0
Module: 164008
mdToken: 06000006
Flags : 400
Method VA : 02c633e8
メソッド記述子の Flags フィールドは、メソッドの種類 (静的メソッド、インスタンス メソッド、インターフェイス メソッド、COM 実装など) に関する情報を含むようにエンコードされています。
MethodTable にはもう 1 つ複雑な側面があります。それは、インターフェイスの実装です。マネージ環境からは簡単に見えますが、それは、複雑な部分がすべてレイアウトのプロセスに吸収されているからです。次のセクションでは、インターフェイスがどのようにレイアウトされているのか、およびインターフェイス ベースのメソッド ディスパッチが実際にはどのように行われているのかを説明します。
ページのトップへ
12. Interface Vtable Map と Interface Map
MethodTable のオフセット 12 には、IVMap という重要なポインタがあります。IVMap は、プロセス レベルのインターフェイス ID をインデックスとする AppDomain レベルのマッピング テーブルを指します (図 9 を参照)。このインターフェイス ID は、インターフェイス型が初めて読み込まれたときに生成されます。各インターフェイス実装は、それぞれに IVMap のエントリを持ちます。たとえば、MyInterface1 が 2 つのクラスによって実装された場合、IVMap テーブルのエントリは 2 つになります。このエントリは、MyClass のメソッド テーブル内に埋め込まれたサブテーブルの先頭を指します (図 9 を参照)。この参照によって、インターフェイスベースのメソッド ディスパッチが行われます。IVMap は、メソッド テーブル内に埋め込まれている Interface Map の情報に基づいて作成されます。Interface Map は、MethodTable のレイアウト プロセスの間に、クラスのメタデータに基づいて作成されます。いったん型の読み込みが完了すると、メソッド ディスパッチでは IVMap のみが使用されます。
オフセット 28 の Interface Map は、MethodTable 内に埋め込まれている InterfaceInfo エントリを指しています。この例の場合、MyClass によって実装されている 2 つのインターフェイスに対応する 2 つのエントリがあります。1 つ目の InterfaceInfo エントリの最初の 4 バイトは、MyInterface1 の TypeHandle を指しています (図 9 と図 10 を参照)。次の WORD (2 バイト) は Flags によって占有されています (0 の場合は親から継承され、1 の場合は現在のクラスによって実装されます)。Flags の直後の WORD は Start Slot です。Start Slot は、クラス ローダーによって、インターフェイス実装サブテーブルのレイアウトのために使用されます。MyInterface1 の場合、この値は 4 です。これは、スロット 5 と 6 が実装を指すという意味です。MyInterface2 の値は 6 なので、スロット 7 と 8 が実装を指すことになります。ClassLoader は、必要に応じてスロットを複製して、各インターフェイスが、物理的には同じメソッド記述子にマップされているにもかかわらず、それぞれ独自の実装を取得するように見せかけます。MyClass では、MyInterface1.Method2 と MyInterface2.Method2 は同じ実装を指します。
インターフェイスベースのメソッド ディスパッチが IVMap を通じて行われるのに対し、直接のメソッド ディスパッチは、それぞれのスロットに格納されている MethodDesc のアドレスを通じて行われます。既に述べたように、.NET Framework は fastcall 呼び出し規約を使用します。通常、最初の 2 つの引数は、可能であれば、ECX レジスタと EDX レジスタを通じて渡されます。インスタンス メソッドの最初の引数は常に this ポインタで、次のように、"mov ecx, esi" ステートメントによって ECX レジスタを通じて渡されます。
mi1.Method1();
mov ecx,edi ;"this" ポインタを ecx に移動します
mov eax,dword ptr [ecx] ;"TypeHandle" を eax に移動します
mov eax,dword ptr [eax+0Ch] ;IVMap のアドレスを eax のオフセット 12 に移動します
mov eax,dword ptr [eax+30h] ;インターフェイス実装開始スロットを eax に移動します
call dword ptr [eax] ;Method1 を呼び出します
mc.Method1();
mov ecx,esi ;"this" ポインタを ecx に移動します
cmp dword ptr [ecx],ecx ;比較してフラグを設定します
call dword ptr ds:[009552D8h];直接 Method1 を呼び出します
これらの逆アセンブリから、MyClass のインスタンス メソッドを直接呼び出す場合にはオフセットは使用されないことがわかります。JIT コンパイラは、MethodDesc のアドレスを直接コードに書き込みます。インターフェイスベースのディスパッチは IVMap を通じて行われ、直接のディスパッチにはない追加の命令をいくつか必要とします。1 つは IVMap のアドレスを取得するためのもので、もう 1 つは、メソッド スロット テーブル内のインターフェイス実装の開始スロットを取得するためのものです。また、オブジェクト インスタンスのインターフェイスへのキャストでは、this ポインタがターゲット変数にコピーされるだけです。図 2 のステートメント "mi1 = mc;" では、1 つの命令を使用して mc の OBJECTREF を mi1 にコピーしています。
ページのトップへ
13. 仮想ディスパッチ
次に、仮想ディスパッチについて見てみましょう。仮想ディスパッチは、直接ディスパッチやインターフェイスベースのディスパッチとどのように違うのでしょうか。以下は、図 3 の MyClass.Method3 への仮想メソッド呼び出しの逆アセンブリです。
mc.Method3();
Mov ecx,esi ;"this" ポインタを ecx に移動します
Mov eax,dword ptr [ecx] ;MethodTable のアドレスを取得します
Call dword ptr [eax+44h] ;オフセット 0x44 のメソッドにディスパッチします
仮想ディスパッチは、特定の実装クラス (型) の階層における MethodTable のポインタに関係なく、常に固定スロット番号を通じて行われます。MethodTable のレイアウトの間に、ClassLoader が親の実装を、オーバーライドする子の実装に置き換えます。その結果、親オブジェクトに対してコーディングされているメソッド呼び出しは、子オブジェクトの実装にディスパッチされます。デバッガのメモリ ウィンドウ (図 10 を参照) と DumpMT の出力から、このディスパッチがスロット番号 8 を通じて行われていることがわかります。
ページのトップへ
14. 静的変数
静的変数は、MethodTable のデータ構造の重要な構成要素です。静的変数は MethodTable の一部として、メソッド テーブル スロットの配列の直後に割り当てられます。静的なプリミティブ型はすべてインライン化され、構造体や参照型などの静的な値オブジェクトは、ハンドル テーブルに作成される OBJECTREF を通じて参照されます。MethodTable の OBJECTREF は、AppDomain のハンドル テーブルの OBJECTREF を参照します。ハンドル テーブルの OBJECTREF は、ヒープに作成されたオブジェクト インスタンスを参照します。ハンドル テーブルの OBJECTREF は、いったん作成されると、AppDomain がアンロードされるまでオブジェクト インスタンスをヒープに保持します。図 9 では、静的文字列変数 str がハンドル テーブルの OBJECTREF を指し、ハンドル テーブルの OBJECTREF は GC Heap の MyString を指しています。
ページのトップへ
15. EEClass
EEClass は、MethodTable が作成される前に作成され、MethodTable と一緒に CLR バージョンの型宣言を構成します。実際、EEClass と MethodTable は論理的には 1 つのデータ構造であり (両方で 1 つの型を表す)、使用頻度に基づいて分割されたものです。頻繁に使用されるフィールドは MethodTable に配置され、あまり頻繁に使用されないフィールドは EEClass に配置されます。このため、関数の JIT コンパイルに必要な情報 (名前、フィールド、オフセットなど) は EEClass に配置されているのに対し、実行時に必要とされる情報 (vtable のスロットや GC の情報など) は MethodTable に配置されています。
AppDomain に読み込まれた各型ごとに 1 つの EEClass があります。型には、インターフェイス、クラス、抽象クラス、配列、および構造体が含まれます。各 EEClass は、実行エンジンによって追跡されるツリーのノードです。CLR は、クラスの読み込み、MethodTable のレイアウト、型の検証、型キャストなどの目的で、EEClass 構造体の間をこのネットワークを使用して移動します。EEClass 間の子から親への関係は継承階層に基づいて確立され、親から子への関係は、継承階層とクラスの読み込み順の組み合わせに基づいて確立されます。マネージ コードの実行が進むにつれて、新しい EEClass ノードが追加され、ノードの関係が調整されて、新しい関係が確立されます。このネットワークには、兄弟にあたる EEClass の間の水平の関係もあります。EEClass には、読み込まれた型の間のノード関係を管理する 3 つのフィールドがあります。ParentClass、SiblingChain、およびChildrenChain の 3 つです。図 13 は、図 4 の MyClass のコンテキストの EEClass を図に表したものです。
図 13 に示されているのは、現在の議論に関係する少数のフィールドだけです。レイアウトのフィールドがいくつか省略されているため、この図にはオフセットは示されていません。EEClass には MethodTable への循環参照があります。また、EEClass は、既定の AppDomain の HighFrequencyHeap に割り当てられている MethodDesc チャンクも指しています。プロセス ヒープに割り当てられている FieldDesc オブジェクトのリストへの参照は、MethodTable の作成時にフィールドのレイアウトの情報を提供します。EEClass は、オペレーティング システムがメモリのページを効率的に管理できるように、AppDomain の LowFrequencyHeap に割り当てられます。これにより、ワーキング セットのサイズが抑えられます。
図 13 EEClass のレイアウト
図 13 のその他のフィールドについては、MyClass (図 3) のコンテキストから明らかです。では、SOS を使用して EEClass をダンプして、実際の物理メモリを見てみましょう。図 3 のプログラムを、mc.Method1 という行にブレークポイントを設定して実行します。まず、コマンド Name2EE を使用して、MyClass の EEClass のアドレスを取得します。
!Name2EE C:\Working\test\ClrInternals\Sample1.exe MyClass
MethodTable: 009552a0
EEClass: 02ca3508
Name: MyClass
Name2EE の最初の引数はモジュール名です。これは、DumpDomain コマンドで取得できます。EEClass のアドレスがわかったので、次に EEClass 自体をダンプします。
!DumpClass 02ca3508
Class Name : MyClass, mdToken : 02000004, Parent Class : 02c4c3e4
ClassLoader : 00163ad8, Method Table : 009552a0, Vtable Slots : 8
Total Method Slots : a, NumInstanceFields: 0,
NumStaticFields: 2,FieldDesc*: 00955224
MT Field Offset Type Attr Value Name
009552a0 4000001 2c CLASS static 00a8198c str
009552a0 4000002 30 System.UInt32 static aaaaaaaa ui
図 13 と DumpClass の出力は、本質的に同じに見えます。メタデータ トークン (mdToken) は、モジュール PE ファイルのメモリ マップ メタデータ テーブルにおける MyClass のインデックスを表します。Parent Class は、System.Object を指します。Sibling Chain (図 13) は、Program クラスが読み込まれた結果として読み込まれたことを示しています。
MyClass には、vtable のスロット (仮想的にディスパッチできるメソッド) が 8 つあります。Method1 と Method2 は仮想メソッドではありませんが、インターフェイスを通じてディスパッチされ、このリストに追加されると、仮想メソッドと見なされます。このリストに .cctor と .ctor を追加すると、メソッドの合計は 10 (0?a) になります。このクラスには静的フィールドが 2 つあります。これらは、リストの末尾にあります。インスタンス フィールドはありません。残りのフィールドについては、説明の必要はないでしょう。
ページのトップへ
16. まとめ
ここでは、CLR の内部の最も重要な要素をいくつか紹介しました。確かに、ここで取り上げた内容は、その幅についても深さについても十分とは言えません。しかし、CLR の内部のしくみをかいま見ることはできたのではないかと思います。ここで紹介した情報の多くは、おそらく、CLR および .NET Framework の今後のリリースで変更されます。しかし、この記事で取り上げた CLR のデータ構造が変わったとしても、その概念は変わりません。
ページのトップへ
17. 補足記事: Son of Strike
この記事では、CLR のデータ構造の内容を表示するために SOS デバッガ拡張機能を使用しています。SOS は、.NET Framework インストール環境の一部で、%windir%\Microsoft.NET\Framework\v1.1.4322 にあります。SOS をプロセスに読み込むには、事前に、Visual Studio .NET のプロジェクトのプロパティでマネージ デバッグを有効にする必要があります。その後、SOS.dll が置かれているディレクトリを PATH 環境変数に追加します。SOS.dll を読み込むには、ブレークポイントで中断している状態で、[デバッグ] メニューの [ウィンドウ] をポイントし、[イミディエイト] をクリックします。続いてイミディエイト ウィンドウで、.load sos.dll を実行します。デバッガ コマンドのリストを表示するには、!help を使用します。SOS の詳細については、2004 年 6 月の Bugslayer コラム (英語)を参照してください。
Hanu Kommalapati は、Microsoft Gulf Coast District (米国テキサス州ヒューストン) のアーキテクトです。現在は、.NET Framework ベースのスケーラブルなコンポーネント フレームワークを構築する企業のお客様のサポートを担当しています。連絡先は hanuk@microsoft.com (英語)です。
Tom Christian は、ASP.NET および WinDBG (sos/psscor) 用 .NET デバッガ拡張機能を担当するマイクロソフトの開発者サポート担当エスカレーション エンジニアです。勤務地は米国ノースカロライナ州シャーロットで、連絡先は tomchris@microsoft.com (英語)です。
この記事は、MSDN マガジン - 2005 年 5 月号からの翻訳です。
ページのトップへ