最適化されたコードとインライン関数のデバッグ
Windows 8 では、デバッガーと Windows コンパイラが強化され、最適化されたコードをデバッグし、インライン関数をデバッグできるようになりました。 デバッガーは、パラメーターとローカル変数がレジスタに格納されているか、スタックに格納されているかに関係なくそれらを表示します。 デバッガーは、呼び出しスタックにインライン関数も表示します。 インライン関数の場合、デバッガーはローカル変数を表示しますが、パラメーターは表示しません。
コードが最適化されると、より高速に実行され、使用するメモリが少なくなるように変換されます。 実行されないコードが削除されたり、コードがマージされたり、関数がインラインで配置されたりした結果、関数が削除されることがあります。 ローカル変数とパラメーターも削除されることがあります。 多くのコード最適化では、不要または使用されていないローカル変数が削除されます。その他の最適化では、ループ内の誘導変数が削除されます。 一般的な部分式の除去では、ローカル変数がマージされます。
Windows の製品ビルドは最適化されています。 そのため、Windows の製品ビルドを実行している場合は、最適化されたコードで適切に動作するように設計されたデバッガーがあると特に便利です。 最適化されたコードのデバッグを効果的★にするには、2 つの主要機能が必要です。1) ローカル変数の正確な表示と、2) 呼び出しスタックでのインライン関数の表示です。
ローカル変数とパラメーターの正確な表示
ローカル変数とパラメーターの正確な表示を容易にするために、コンパイラはローカル変数とパラメーターの場所に関する情報をシンボル (PDB) ファイルに記録します。 これらの場所の記録は、変数の保存場所と、これらの場所が有効な特定のコード範囲を追跡します。 これらの記録は、変数の場所 (レジスタ内またはスタック スロット内) の追跡だけでなく、変数の移動の追跡にも役立ちます。 たとえば、パラメーターは最初にレジスタ RCX に入りますが、RCX を解放するためにスタック スロットに移動され、ループで頻繁に使用されているときはレジスタ R8 に移動され、コードがループから外れると別のスタック スロットに移動されます。 Windows デバッガーは、PDB ファイル内の豊富な場所の記録を使用し、現在の命令ポインターを使用して、ローカル変数とパラメーターの適切な場所の記録を選択します。
Visual Studio の [ローカル] ウィンドウのこのスクリーン ショットは、最適化された 64 ビット アプリケーションの関数のパラメーターとローカル変数を示しています。 この関数はインラインではないため、パラメーターとローカル変数の両方が表示されます。
dv -v コマンドを使用して、パラメーターとローカル変数の場所を確認できます。
パラメーターがレジスタに格納されていても、[ローカル] ウィンドウにパラメーターが正しく表示されていることに注意してください。
位置の記録は、プリミティブ型の変数を追跡するだけでなく、ローカルな構造体とクラスのデータ メンバーを追跡します。 以下のデバッガー出力に、ローカルな構造が表示されています。
0:000> dt My1
Local var Type _LocalStruct
+0x000 i1 : 0n0 (edi)
+0x004 i2 : 0n1 (rsp+0x94)
+0x008 i3 : 0n2 (rsp+0x90)
+0x00c i4 : 0n3 (rsp+0x208)
+0x010 i5 : 0n4 (r10d)
+0x014 i6 : 0n7 (rsp+0x200)
0:000> dt My2
Local var @ 0xefa60 Type _IntSum
+0x000 sum1 : 0n4760 (edx)
+0x004 sum2 : 0n30772 (ecx)
+0x008 sum3 : 0n2 (r12d)
+0x00c sum4 : 0n0
上のデバッガー出力から、次のようなことがわかります。
- ローカルな構造体 My1 は、コンパイラがローカルな構造体のデータ メンバーを、レジスタおよび不連続なスタック スロットに分散できることを示しています。
- dt My2 コマンドの出力は、dt _IntSum 0xefa60 コマンドの出力とは異なります。 ローカルな構造体がスタック メモリの連続したブロックを占有するとは限りません。 My2 の場合、
sum4
だけが元のスタック ブロックに留まり、他の 3 つのデータ メンバーはレジスタに移動されます。 - 複数の場所があるデータ メンバーもあります。 たとえば、My2.sum2 には 2 つの場所があります。1 つは登録 ECX (Windows デバッガーが選択) で、もう 1 つは 0xefa60+0x4 (元のスタック スロット) です。 これはプリミティブ型のローカル変数でも発生する可能性があり、Windows デバッガーでは、使用する場所を決定するために、優先順位の高いヒューリスティックが適用されます。 たとえば、レジスタの場所は常にスタックの場所に勝ります。
呼び出しスタックでインライン関数を表示する
コードの最適化中に、一部の関数はインラインで配置されます。 つまり、マクロ展開のように関数の本体がコードに直接配置されます。 関数呼び出しはなく、呼び出し元への戻り値もありません。 インライン関数の表示を容易にするために、コンパイラはインライン関数のコード チャンク (つまり、インラインに配置されている呼び出し先関数に属する呼び出し元関数のコード ブロックのシーケンス) とローカル変数 (それらのコード ブロック内のスコープ付きローカル変数) をデコードするのに役立つ PDB ファイルにデータを格納します。 このデータは、デバッガーがスタック アンワインドの一部としてインライン関数を含めるのに役立ちます。
アプリケーションをコンパイルし、func1
という名前の関数を強制的にインラインにするとします。
__forceinline int func1(int p1, int p2, int p3)
{
int num1 = 0;
int num2 = 0;
int num3 = 0;
...
}
bm コマンドを使用して、func1
にブレークポイントを設定できます。
0:000> bm MyApp!func1
1: 000007f6`8d621088 @!"MyApp!func1" (MyApp!func1 inlined in MyApp!main+0x88)
0:000> g
Breakpoint 1 hit
MyApp!main+0x88:
000007f6`8d621088 488d0d21110000 lea rcx,[MyApp!`string' (000007f6`8d6221b0)]
func1
の処理に入ると、k コマンドを使用して呼び出しスタックに func1
を表示できます。 dv コマンドを使用して、func1
のローカル変数を確認できます。 ローカル変数 num3
が "unavailable" として表示されていることに注意してください。 多くの理由から、最適化されたコードではローカル変数が使用不可になることがあります。 最適化されたコードには、その変数が存在しない可能性があります。 変数がまだ初期化されていないか、変数が使用されなくなった可能性があります。
0:000> p
MyApp!func1+0x7:
000007f6`8d62108f 8d3c33 lea edi,[rbx+rsi]
0:000> knL
# Child-SP RetAddr Call Site
00 (Inline Function) --------`-------- MyApp!func1+0x7
01 00000000`0050fc90 000007f6`8d6213f3 MyApp!main+0x8f
02 00000000`0050fcf0 000007ff`c6af0f7d MyApp!__tmainCRTStartup+0x10f
03 00000000`0050fd20 000007ff`c7063d6d KERNEL32!BaseThreadInitThunk+0xd
04 00000000`0050fd50 00000000`00000000 ntdll!RtlUserThreadStart+0x1d
0:000> dv -v
00000000`0050fcb0 num1 = 0n0
00000000`0050fcb4 num2 = 0n0
<unavailable> num3 = <value unavailable>
スタック トレースの frame 1 を見ると、main
関数のローカル変数を確認できます。 2 つの変数がレジスタに格納されていることに注意してください。
0:000> .frame 1
01 00000000`0050fc90 000007f6`8d6213f3 MyApp!main+0x8f
0:000> dv -v
00000000`0050fd08 c = 0n7
@ebx b = 0n13
@esi a = 0n6
Windows デバッガーは PDB ファイルのデータを集計して、特定の関数がインラインで配置されているすべての場所を見つけます。 x を使用して、インライン関数のすべての呼び出し元サイトを一覧表示できます。
0:000> x simple!MoreCalculate
00000000`ff6e1455 simple!MoreCalculate = (inline caller) simple!wmain+8d
00000000`ff6e1528 simple!MoreCalculate = (inline caller) simple!wmain+160
0:000> x simple!Calculate
00000000`ff6e141b simple!Calculate = (inline caller) simple!wmain+53
Windows デバッガーはインライン関数のすべての呼び出し元サイトを列挙できるため、呼び出し元サイトからのオフセットを計算することで、インライン関数内にブレークポイントを設定できます。 bm コマンド (正規表現パターンに一致するブレークポイントを設定するために使用される) を使用して、インライン関数のブレークポイントを設定できます。
Windows デバッガーは、特定のインライン関数に設定されているすべてのブレークポイントを、ブレークポイント コンテナーにグループ化します。 be、bd、bc などのコマンドを使用して、ブレークポイント コンテナーを全体として操作できます。 以下の bd 3 および bc 3 コマンドの例をご覧ください。 個々のブレークポイントを操作することもできます。 以下の be 2 コマンドの例をご覧ください。
0:000> bm simple!MoreCalculate
2: 00000000`ff6e1455 @!"simple!MoreCalculate" (simple!MoreCalculate inlined in simple!wmain+0x8d)
4: 00000000`ff6e1528 @!"simple!MoreCalculate" (simple!MoreCalculate inlined in simple!wmain+0x160)
0:000> bl
0 e 00000000`ff6e13c8 [n:\win7\simple\simple.cpp @ 52] 0001 (0001) 0:**** simple!wmain
3 e <inline function> 0001 (0001) 0:**** {simple!MoreCalculate}
2 e 00000000`ff6e1455 [n:\win7\simple\simple.cpp @ 58] 0001 (0001) 0:**** simple!wmain+0x8d (inline function simple!MoreCalculate)
4 e 00000000`ff6e1528 [n:\win7\simple\simple.cpp @ 72] 0001 (0001) 0:**** simple!wmain+0x160 (inline function simple!MoreCalculate)
0:000> bd 3
0:000> be 2
0:000> bl
0 e 00000000`ff6e13c8 [n:\win7\simple\simple.cpp @ 52] 0001 (0001) 0:**** simple!wmain
3 d <inline function> 0001 (0001) 0:**** {simple!MoreCalculate}
2 e 00000000`ff6e1455 [n:\win7\simple\simple.cpp @ 58] 0001 (0001) 0:**** simple!wmain+0x8d (inline function simple!MoreCalculate)
4 d 00000000`ff6e1528 [n:\win7\simple\simple.cpp @ 72] 0001 (0001) 0:**** simple!wmain+0x160 (inline function simple!MoreCalculate)
0:000> bc 3
0:000> bl
0 e 00000000`ff6e13c8 [n:\win7\simple\simple.cpp @ 52] 0001 (0001) 0:**** simple!wmain
インライン関数の明示的な呼び出しまたは戻り命令がないため、ソース レベルのステップ実行はデバッガーにとって特に困難です。 たとえば、意図せずにインライン関数にステップ インすることや (次の命令がインライン関数の一部である場合)、同じインライン関数に何度もステップ インしてステップ アウトすることがあります (インライン関数のコード ブロックがコンパイラによって分割されたり移動されたりしたため)。 よく知っているステップ実行エクスペリエンスを保持するために、Windows デバッガーはすべてのコード命令アドレスに対して小さな概念的な呼び出しスタックを保持し、ステップ イン、ステップ オーバー、およびステップ アウト操作を実行するために内部状態マシンを構築します。 これにより、インライン関数以外の関数のステップ実行エクスペリエンスに対する、かなり正確な概算値が得られます。
追加情報
注.inline 0 コマンドを使用して、インライン関数のデバッグを無効にすることができます。 .inline 1 コマンドは、インライン関数のデバッグを有効にします。 標準的なデバッグの手法