スタック オーバーフローのデバッグ

スタック オーバーフローは、ユーザー モード スレッドで発生することがあるエラーです。 このエラーの原因は 3 つ考えられます。

  • スレッドが、そのスレッド用に予約されたスタック全体を使用する。 多くの場合、これは無限再帰によって発生します。

  • ページ ファイルが上限に達していおり、スタックを拡張するために追加のページをコミットできないため、スレッドがスタックを拡張できない。

  • ページ ファイルの拡張に使用される短い期間内にシステムがあるため、スレッドがスタックを拡張できない。

スレッドで実行されている関数がローカル変数を割り当てると、変数はスレッドの呼び出しスタックに置かれます。 関数に必要なスタック領域の量は、すべてのローカル変数のサイズの合計と同じ大きさになる可能性があります。 ただし、コンパイラは通常、関数に必要なスタック領域を減らす最適化を行います。 たとえば、2 つの変数が別のスコープにある場合、コンパイラは両方の変数に同じスタック メモリを使用できます。 コンパイラは、計算を最適化することで、一部のローカル変数を完全に除去することもできます。

最適化の量は、ビルド時に適用されるコンパイラ設定の影響を受けます。 たとえば、[/F (スタック サイズの設定)] - [C++ コンパイラ オプション] で指定します。

このトピックでは、スレッド、スレッド ブロック、スタック、ヒープなどの概念に関する一般的な知識を前提としています。 これらの基本的な概念の詳細については、Mark Russinovich と David ソロモンによる「Microsoft Windows Internals」を参照してください。

シンボルなしのスタック オーバーフローのデバッグ

ここで、スタック オーバーフローをデバッグする方法の例を示します。 この例では、NTSD はターゲット アプリケーションと同じコンピューター上で実行され、その出力がホスト コンピューター上の KD にリダイレクトされています。 詳細については、「カーネル デバッガー からのユーザーモード デバッガーの制御」を参照してください。

最初の手順では、デバッガーが割り込む原因となったイベントを確認します。

0:002> .lastevent 
Last event: Exception C00000FD, second chance 

ntstatus.h で例外コード 0xC00000FD を検索できます。この例外コードは STATUS_STACK_OVERFLOW で、スタックの新しいガード ページを作成できないことを示します。すべてのステータス コードは、「2.3.1 NTSTATUS 値」に記載されています。

!error コマンドを使用して、Windows デバッガーでエラーを検索することもできます。

0:002> !error 0xC00000FD
Error code: (NTSTATUS) 0xc00000fd (3221225725) - A new guard page for the stack cannot be created.

スタックがオーバーフローしたことを再確認するために、k (スタック バックトレースの表示) コマンドを使用できます。

0:002> k 
ChildEBP RetAddr
009fdd0c 71a32520 COMCTL32!_chkstk+0x25
009fde78 77cf8290 COMCTL32!ListView_WndProc+0x4c4
009fde98 77cfd634 USER32!_InternalCallWinProc+0x18
009fdf00 77cd55e9 USER32!UserCallWinProcCheckWow+0x17f
009fdf3c 77cd63b2 USER32!SendMessageWorker+0x4a3
009fdf5c 71a45b30 USER32!SendMessageW+0x44
009fdfec 71a45bb0 COMCTL32!CCSendNotify+0xc0e
009fdffc 71a1d688 COMCTL32!CICustomDrawNotify+0x2a
009fe074 71a1db30 COMCTL32!Header_Draw+0x63
009fe0d0 71a1f196 COMCTL32!Header_OnPaint+0x3f
009fe128 77cf8290 COMCTL32!Header_WndProc+0x4e2
009fe148 77cfd634 USER32!_InternalCallWinProc+0x18
009fe1b0 77cd4490 USER32!UserCallWinProcCheckWow+0x17f
009fe1d8 77cd46c8 USER32!DispatchClientMessage+0x31
009fe200 77f7bb3f USER32!__fnDWORD+0x22
009fe220 77cd445e ntdll!_KiUserCallbackDispatcher+0x13
009fe27c 77cfd634 USER32!DispatchMessageWorker+0x3bc
009fe2e4 009fe4a8 USER32!UserCallWinProcCheckWow+0x17f
00000000 00000000 0x9fe4a8 

ターゲット スレッドは、スタックの問題を示す COMCTL32!_chkstk に割り込みました。 次に、ターゲット プロセスのスタックの使用状況を調べる必要があります。 プロセスには複数のスレッドがありますが、重要なのはオーバーフローを発生させたスレッドであるため、最初に ~ (スレッドの状態) コマンドを使用してこのスレッドを特定します。

0:002> ~*k

   0  id: 570.574   Suspend: 1 Teb 7ffde000 Unfrozen
   .....

   1  id: 570.590   Suspend: 1 Teb 7ffdd000 Unfrozen
   .....

. 2  id: 570.598   Suspend: 1 Teb 7ffdc000 Unfrozen
ChildEBP RetAddr
 009fdd0c 71a32520 COMCTL32!_chkstk+0x25 
.....

   3  id: 570.760   Suspend: 1 Teb 7ffdb000 Unfrozen 

次に、スレッド 2 を調査する必要があります。 この行の左側にあるピリオドは、これが現在のスレッドであることを示します。

スタック情報は、0X7FFDC000 の TEB (スレッド環境ブロック) に含まれています。 これを一覧表示する最も簡単な方法は、!teb を使用することです。

0:000> !teb
TEB at 000000c64b95d000
    ExceptionList:        0000000000000000
    StackBase:            000000c64ba80000
    StackLimit:           000000c64ba6f000
    SubSystemTib:         0000000000000000
    FiberData:            0000000000001e00
    ArbitraryUserPointer: 0000000000000000
    Self:                 000000c64b95d000
    EnvironmentPointer:   0000000000000000
    ClientId:             0000000000003bbc . 0000000000004ba0
    RpcHandle:            0000000000000000
    Tls Storage:          0000027957243530
    PEB Address:          000000c64b95c000
    LastErrorValue:       0
    LastStatusValue:      0
    Count Owned Locks:    0
    HardErrorMode:        0```

ただし、これには適切なシンボルが必要です。 さらに難しい状況では、シンボルがないときに、dd (メモリの表示) コマンドを使用してその場所に未加工の値を表示する必要があります。

0:002> dd 7ffdc000 L4 
7ffdc000   009fdef0 00a00000 009fc000 00000000 

これを解釈するには、TEB データ構造の定義を検索する必要があります。 dt (表示の種類) コマンドを使用して、シンボルが使用可能なシステムでこれを行います。

0:000> dt _TEB
ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x01c EnvironmentPointer : Ptr32 Void
   +0x020 ClientId         : _CLIENT_ID
   +0x028 ActiveRpcHandle  : Ptr32 Void
   +0x02c ThreadLocalStoragePointer : Ptr32 Void
   +0x030 ProcessEnvironmentBlock : Ptr32 _PEB
   +0x034 LastErrorValue   : Uint4B
   +0x038 CountOfOwnedCriticalSections : Uint4B
   +0x03c CsrClientThread  : Ptr32 Void
   +0x040 Win32ThreadInfo  : Ptr32 Void
   +0x044 User32Reserved   : [26] Uint4B
   +0x0ac UserReserved     : [5] Uint4B
   +0x0c0 WOW32Reserved    : Ptr32 Void
...

スレッドのデータ構造

スレッドの詳細を知るために、スレッド コントロール ブロックに関連する構造体、ethread と kthread に関する情報を表示することもできます。 (64 ビットの例を次に示します)。

0:001> dt nt!_ethread
ntdll!_ETHREAD
   +0x000 Tcb              : _KTHREAD
   +0x430 CreateTime       : _LARGE_INTEGER
   +0x438 ExitTime         : _LARGE_INTEGER
   +0x438 KeyedWaitChain   : _LIST_ENTRY
   +0x448 PostBlockList    : _LIST_ENTRY
   +0x448 ForwardLinkShadow : Ptr64 Void
   +0x450 StartAddress     : Ptr64 Void
...
0:001> dt nt!_kthread
ntdll!_KTHREAD
   +0x000 Header           : _DISPATCHER_HEADER
   +0x018 SListFaultAddress : Ptr64 Void
   +0x020 QuantumTarget    : Uint8B
   +0x028 InitialStack     : Ptr64 Void
   +0x030 StackLimit       : Ptr64 Void
   +0x038 StackBase        : Ptr64 Void

スレッド データ構造の詳細については、「Microsoft Windows Internals」を参照してください。

_TEB 構造体の 32 ビット バージョンを調べると、TEB 構造体の 2 番目と 3 番目の DWORD がそれぞれスタックの下端と上端を指していることを示しています。 この例では、これらのアドレスは 0x00A00000 と 0x009FC000 です。 (スタックはメモリ内で下に拡張されます)。? (式の評価) コマンドを使用して、スタック サイズを計算できます。

0:002> ? a00000-9fc000
Evaluate expression: 16384 = 00004000 

この例は、スタック サイズが 16 K であることを示しています。最大スタック サイズは、DeallocationStack フィールドに格納されます。 少し計算すると、このフィールドのオフセットが 0xE0C であることを確認できます。

0:002> dd 7ffdc000+e0c L1 
7ffdce0c   009c0000 

0:002> ? a00000-9c0000 
Evaluate expression: 262144 = 00040000 

この例は、最大スタック サイズが 256 K であることを示しています。これは、十分過ぎるスタック領域が残っていることを意味します。

さらに、このプロセスはクリーンに見えます。これは無限再帰にはなく、スタック ベースのデータ構造が過度に大きすぎてスタック領域を超えることはありません。

次に、KD に割り込み、!vm 拡張機能コマンドを使用してシステム メモリの全体的な使用量を調べます。

0:002> .breakin 
Break instruction exception - code 80000003 (first chance)
ntoskrnl!_DbgBreakPointWithStatus+4:
80148f9c cc               int     3

kd> !vm 

*** Virtual Memory Usage ***
        Physical Memory:     16268   (   65072 Kb)
        Page File: \??\C:\pagefile.sys
           Current:    147456Kb Free Space:     65988Kb
           Minimum:     98304Kb Maximum:       196608Kb
        Available Pages:      2299   (    9196 Kb)
        ResAvail Pages:       4579   (   18316 Kb)
        Locked IO Pages:        93   (     372 Kb)
        Free System PTEs:    42754   (  171016 Kb)
        Free NP PTEs:         5402   (   21608 Kb)
        Free Special NP:       348   (    1392 Kb)
        Modified Pages:        757   (    3028 Kb)
        NonPagedPool Usage:    811   (    3244 Kb)
        NonPagedPool Max:     6252   (   25008 Kb)
        PagedPool 0 Usage:    1337   (    5348 Kb)
        PagedPool 1 Usage:     893   (    3572 Kb)
        PagedPool 2 Usage:     362   (    1448 Kb)
        PagedPool Usage:      2592   (   10368 Kb)
        PagedPool Maximum:   13312   (   53248 Kb)
        Shared Commit:        3928   (   15712 Kb)
        Special Pool:         1040   (    4160 Kb)
        Shared Process:       3641   (   14564 Kb)
        PagedPool Commit:     2592   (   10368 Kb)
        Driver Commit:         887   (    3548 Kb)
        Committed pages:     45882   (  183528 Kb)
        Commit limit:        50570   (  202280 Kb)

        Total Private:       33309   (  133236 Kb)
         ..... 

まず、非ページ プールとページ プールの使用状況を調べます。 どちらも制限内であるため、これらは問題の原因ではありません。

次に、コミットされたページの数を調べます (183528 / 202280)。 これは上限に非常に近いです。 この表示では、この数が完全に上限に達することは示されていませんが、ユーザー モード デバッグを行っている間は、システム上で他のプロセスが実行されていることに注意してください。 NTSD コマンドが実行されるたびに、これらの他のプロセスもメモリの割り当てと解放を行います。 つまり、スタック オーバーフローが発生した時点のメモリの状態が正確にわかりません。 コミットされたページの数が上限にどれだけ近いかを考えると、ある時点でページ ファイルが使い尽くされ、それが原因でスタック オーバーフローが発生したと結論付けるのが妥当です。

これは珍しいことではなく、このためにターゲット アプリケーションに実際に障害が発生することはありません。 頻繁に発生する場合は、障害が発生したアプリケーションの初期スタック コミットメントを上げることを検討しましょう。

単一つの関数呼び出しの分析

また、特定の関数呼び出しが割り当てているスタック領域の正確な量を調べるのも役に立ちます。

これを行うには、最初のいくつかの命令を逆アセンブルし、命令の sub espを探します。 これにより、スタック ポインターが移動され、ローカル データ向けにその分のバイトが効果的に予約されます。

次に例を示します。 まず、k コマンドを使用してスタックを調べます。

0:002> k 
ChildEBP RetAddr
009fdd0c 71a32520 COMCTL32!_chkstk+0x25
009fde78 77cf8290 COMCTL32!ListView_WndProc+0x4c4
009fde98 77cfd634 USER32!_InternalCallWinProc+0x18
009fdf00 77cd55e9 USER32!UserCallWinProcCheckWow+0x17f
009fdf3c 77cd63b2 USER32!SendMessageWorker+0x4a3
009fdf5c 71a45b30 USER32!SendMessageW+0x44
009fdfec 71a45bb0 COMCTL32!CCSendNotify+0xc0e
009fdffc 71a1d688 COMCTL32!CICustomDrawNotify+0x2a
009fe074 71a1db30 COMCTL32!Header_Draw+0x63
009fe0d0 71a1f196 COMCTL32!Header_OnPaint+0x3f
009fe128 77cf8290 COMCTL32!Header_WndProc+0x4e2

次に、u、ub、uu (逆アセンブル) コマンドを使用して、そのアドレスのアセンブラー コードを確認します。

0:002> u COMCTL32!Header_Draw
 COMCTL32!Header_Draw :
71a1d625 55               push    ebp
71a1d626 8bec             mov     ebp,esp
71a1d628 83ec58           sub     esp,0x58
71a1d62b 53               push    ebx
71a1d62c 8b5d08           mov     ebx,[ebp+0x8]
71a1d62f 56               push    esi
71a1d630 57               push    edi
71a1d631 33f6             xor     esi,esi 

これは、Header_Draw がスタック領域の 0x58 バイトを割り当てたことを示しています。

r (レジスタ) コマンドは、esp などのレジスタの現在の内容に関する情報を提供します。

シンボルが使用可能なときのスタック オーバーフローのデバッグ

シンボルはメモリに格納されている項目にラベルを提供し、使用可能なときはコードの確認を容易にすることができます。 シンボルの概要については、「シンボルの使用」を参照してください。 シンボル パスの設定については、「.sympath (シンボル パスの設定)」を参照してください。

スタック オーバーフローを作成するには、スタックが使い果たされるまでサブルーチンを呼び出し続ける以下のコードを使用できます。

// StackOverFlow1.cpp 
// This program calls a sub routine using recursion too many times
// This causes a stack overflow
//

#include <iostream>

void Loop2Big()
{
    const char* pszTest = "My Test String";
    for (int LoopCount = 0; LoopCount < 10000000; LoopCount++)
    {
        std::cout << "In big loop \n";
        std::cout << (pszTest), "\n";
        std::cout << "\n";
        Loop2Big();
    }
}


int main()
{
    std::cout << "Calling Loop to use memory \n";
    Loop2Big();
}

コードがコンパイルされ、WinDbg で実行されると、コードは何回かループし、スタック オーバーフロー例外をスローします。

(336c.264c): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=0fa90000 edx=00000000 esi=773f1ff4 edi=773f25bc
eip=77491a02 esp=010ffa0c ebp=010ffa38 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!LdrpDoDebuggerBreak+0x2b:
77491a02 cc              int     3
0:000> g
(336c.264c): Stack overflow - code c00000fd (first chance)

!analyze コマンドを使用して、実際にループに問題があることを確認します。

...

FAULTING_SOURCE_LINE_NUMBER:  25

FAULTING_SOURCE_CODE:  
    21: int main()
    22: {
    23:     std::cout << "Calling Loop to use memory \n";
    24:     Loop2Big();
>   25: }
    26: 

kb コマンドを使用することで、ループ プログラムの多くのインスタンスがそれぞれメモリを使用していることがわかります。

0:000> kb
 # ChildEBP RetAddr      Args to Child      
...
0e 010049b0 00d855b5     01004b88 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x57 [C:\StackOverFlow1\StackOverFlow1.cpp @ 13] 
0f 01004a9c 00d855b5     01004c74 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17] 
10 01004b88 00d855b5     01004d60 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17] 
11 01004c74 00d855b5     01004e4c 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17] 
12 01004d60 00d855b5     01004f38 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17] 
13 01004e4c 00d855b5     01005024 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17] 
14 01004f38 00d855b5     01005110 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17] 
15 01005024 00d855b5     010051fc 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17] 
16 01005110 00d855b5     010052e8 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17] 
17 010051fc 00d855b5     010053d4 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17] 
18 010052e8 00d855b5     010054c0 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17] 
19 010053d4 00d855b5     010055ac 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17] 
1a 010054c0 00d855b5     01005698 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17] 
1b 010055ac 00d855b5     01005784 00d81023 00ff5000 StackOverFlow1!Loop2Big+0x85 [C:\StackOverFlow1\StackOverFlow1.cpp @ 17] 

...

シンボルが使用可能な場合は、dt _TEB を使用してスレッド ブロックに関する情報を表示できます。 スレッド メモリの詳細については、「スレッド スタック サイズ」を参照してください。

0:000> dt _TEB
ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x01c EnvironmentPointer : Ptr32 Void
   +0x020 ClientId         : _CLIENT_ID
   +0x028 ActiveRpcHandle  : Ptr32 Void
   +0x02c ThreadLocalStoragePointer : Ptr32 Void
   +0x030 ProcessEnvironmentBlock : Ptr32 _PEB
   +0x034 LastErrorValue   : Uint4B
   +0x038 CountOfOwnedCriticalSections : Uint4B
   +0x03c CsrClientThread  : Ptr32 Void
   +0x040 Win32ThreadInfo  : Ptr32 Void
   +0x044 User32Reserved   : [26] Uint4B
   +0x0ac UserReserved     : [5] Uint4B
   +0x0c0 WOW32Reserved    : Ptr32 Void

StackBase abd StackLimit を表示する !teb コマンドも使用できます。

0:000> !teb
TEB at 00ff8000
    ExceptionList:        01004570
    StackBase:            01100000
    StackLimit:           01001000
    SubSystemTib:         00000000
    FiberData:            00001e00
    ArbitraryUserPointer: 00000000
    Self:                 00ff8000
    EnvironmentPointer:   00000000
    ClientId:             0000336c . 0000264c
    RpcHandle:            00000000
    Tls Storage:          00ff802c
    PEB Address:          00ff5000
    LastErrorValue:       0
    LastStatusValue:      c00700bb
    Count Owned Locks:    0
    HardErrorMode:        0

以下のコマンドを使用して、スタック サイズを計算できます。

0:000> ?? int(@$teb->NtTib.StackBase) - int(@$teb->NtTib.StackLimit)
int 0n1044480

コマンドのまとめ

関連項目

WinDbg ドライバーの概要 (ユーザー モード)

[/F (スタック サイズの設定)] - [C++ コンパイラ オプション]