8

Ldarg.0IL には、などの引数を操作するためのオペコードがいくつかありLdarg.1ます。

これらの引数は、オペコードが実行される前にスタックにプッシュされることを知っています。call場合によっては、 (メンバーなど)Ldarg.0への参照を取得するために使用されます。this

私の質問は次のとおりです。呼び出しが開始されたときに、これらの引数はどこに保存されますか? 実行された呼び出しから、呼び出し元のスタックのコピーにアクセスできますか?

その件に関する詳細情報はどこで入手できますか?

アップデート

仮想マシンが抽象的であり、JIT コンパイラがこれらの問題を処理することは知っていますが、IL が .NET Micro Framework のように解釈されたかどうかを想像してみましょう。

4

3 に答える 3

10

IL (現在は MSILではなく CIL、Common Intermediate Language として知られている) は、架空のスタック マシンでの操作を記述します。JIT コンパイラは IL 命令を受け取り、それをマシン コードにコンパイルします。

メソッドを呼び出すとき、JIT コンパイラは呼び出し規約に従う必要があります。この規則は、引数が呼び出されたメソッドに渡される方法、戻り値が呼び出し元に返される方法、およびスタックから引数を削除する責任があるユーザー (呼び出し元または呼び出し先) を指定します。この例では、cdecl呼び出し規則を使用していますが、実際の JIT コンパイラは他の規則を使用しています。

一般的方法

正確な詳細は実装によって異なりますが、CIL をマシン コードにコンパイルするために .NET および Mono JIT コンパイラで使用される一般的なアプローチは次のとおりです。

  1. スタックを「シミュレート」し、それを使用してすべてのスタックベースの操作を仮想レジスタ(変数) の操作に変換します。理論上、仮想レジスタは無数にあります。
  2. すべての IL 命令を同等の機械語命令に変換します。
  3. 各仮想レジスタを実マシン レジスタに割り当てます。使用可能なマシン レジスタの数は限られています。たとえば、32 ビット x86 アーキテクチャには 8 つのマシン レジスタしかありません。

もちろん、これらのステップの間には多くの最適化が行われています。

これらの手順を説明するために、例を挙げてみましょう。

ldarg.1                     // Load argument 1 on the stack
ldarg.3                     // Load argument 3 on the stack
add                         // Pop value2 and value1, and push (value1 + value2)
call int32 MyMethod(int32)  // Pop value and call MyMethod, push result
ret                         // Pop value and return

ステップ 1 では、IL はレジスタベースの操作に変換されます ( operation dest <- src1, src2)。

ldarg.1 %reg0 <-            // Load argument 1 in %reg0
ldarg.3 %reg1 <-            // Load argument 3 in %reg1
add %reg0 <- %reg0, %reg1   // %reg0 = (%reg0 + %reg1)
// Call MyMethod(%reg0), store result in %reg0
call int32 MyMethod(int32) %reg0 <- %reg0
ret <- %reg0                // Return %reg0

次に、x86 などの機械語命令に変換されます。

mov %reg0, [addr_of_arg1]   // Move argument 1 in %reg0
mov %reg1, [addr_of_arg3]   // Move argument 3 in %reg1
add %reg0, %reg1            // Add %reg1 to %reg0

push %reg0                  // Push %reg0 on the real stack
call [addr_of_MyMethod]     // Call the method
add esp, 4

mov %reg0, eax              // Move the return value into %reg0
mov eax, %reg0              // Move %reg0 into the return value register EAX
ret                         // Return

次に、各仮想レジスタ %reg0、%reg1 にマシン レジスタが割り当てられます。例えば:

mov eax, [addr_of_arg1]     // Move argument 1 in EAX
mov ecx, [addr_of_arg3]     // Move argument 3 in ECX
add eax, ecx                // Add ECX to EAX

push eax                    // Push EAX on the real stack
call [addr_of_MyMethod]     // Call the method
add esp, 4

mov ecx, eax                // Move the return value into ECX
mov eax, ecx                // Move ECX into the return value register EAX
ret                         // Return

こぼれる

レジスタを慎重に選択することで、一部のmov命令を削除できます。コード内の任意の時点で、使用可能なマシン レジスタよりも多くの仮想レジスタが使用されている場合、1 つのマシン レジスタをスピルして使用する必要があります。マシン レジスタがスピルされると、レジスタの値を実際のスタックにプッシュする命令が挿入されます。後で、こぼれた値を再度使用する必要がある場合、実際のスタックからレジスタの値をポップする命令が挿入されます。

結論

ご覧のとおり、機械語コードは、IL コードが評価スタックを使用する頻度ほど実際のスタックを使用していません。その理由は、マシン レジスタはプロセッサの最速のメモリ要素であるため、コンパイラは可能な限りそれらを使用しようとするためです。値は、マシン レジスタが不足している場合、または値をスタックに格納する必要がある場合 (呼び出し規則などにより) にのみ、実際のスタックに格納されます。

于 2013-05-06T17:04:03.173 に答える
8

ECMA-335はおそらくこれの良い出発点です。

たとえば、セクション I.12.4.1 には次のように記載されています。

CIL コード ジェネレーターによって出力される命令には、CLI のさまざまな実装がさまざまなネイティブ呼び出し規則を使用するための十分な情報が含まれています。すべてのメソッド呼び出しは、次のようにメソッド状態領域を初期化します (§I.12.3.2 を参照)。

  1. 着信引数配列は、呼び出し元によって目的の値に設定されます。
  2. ローカル変数配列は、オブジェクト型と、オブジェクトを保持する値型内のフィールドに対して常に null を持ちます。さらに、localsinit フラグがメソッド ヘッダーに設定されている場合、ローカル変数配列は、すべての整数型に対して 0 に初期化され、すべての浮動小数点型に対して 0.0 に初期化されます。値の型は CLI によって初期化されませんが、検証されたコードは、メソッドのエントリ ポイント コードの一部として初期化子への呼び出しを提供します。
  3. 評価スタックが空です。

I.12.3.2 は次のとおりです。

各メソッド状態の一部は、ローカル変数を保持する配列と引数を保持する配列です。評価スタックと同様に、これらの配列の各要素は、単一のデータ型または値型のインスタンスを保持できます。どちらの配列も 0 から始まります (つまり、最初の引数またはローカル変数の番号は 0 です)。ローカル変数のアドレスは ldloca 命令を使用して計算でき、引数のアドレスは ldarga 命令を使用して計算できます。

各メソッドに関連付けられているのは、次を指定するメタデータです。

  • メソッドに入ったときに、ローカル変数とメモリ プール メモリを初期化するかどうか。
  • 各引数の型と引数配列の長さ (ただし、可変引数リストについては以下を参照してください)。
  • 各ローカル変数の型とローカル変数配列の長さ。

CLI は、ターゲット アーキテクチャに応じてパディングを挿入します。つまり、一部の 64 ビット アーキテクチャでは、すべてのローカル変数を 64 ビットでアラインすることができますが、他のアーキテクチャでは、8、16、または 32 ビットでアラインすることができます。CIL ジェネレーターは、配列内のローカル変数のオフセットについて想定しません。実際、CLI はローカル変数配列内の要素を自由に並べ替えることができ、異なる実装では異なる方法でそれらを並べ替えることができます。

そして、パーティション III では、callvirt(例として) の説明は次のとおりです。

callvirtメソッドを呼び出す前に、オブジェクトと引数を評価スタックからポップします。メソッドに戻り値がある場合は、メソッドの完了時にスタックにプッシュされます。呼び出し先側では、obj パラメーターは引数 0 としてアクセスされ、arg1 は引数 1 としてアクセスされます。

現在、これはすべて仕様レベルです。実際の実装では、関数呼び出しが現在のメソッドのスタックの上位 n 要素を継承するようにすることを決定する可能性があります。これは、引数が既に適切な場所にあることを意味します。

于 2013-05-06T14:16:57.187 に答える