다음을 통해 공유


스택 오버플로 디버깅

스택 오버플로는 사용자 모드 스레드에서 발생할 수 있는 오류입니다. 이 오류가 일어날 수 있는 세 가지 원인은 다음과 같습니다.

  • 스레드는 예약된 전체 스택을 사용합니다. 이는 종종 무한 재귀로 인해 발생합니다.

  • 페이지 파일이 최대화되므로 스레드가 스택을 확장할 수 없으므로 스택을 확장하기 위해 추가 페이지를 커밋할 수 없습니다.

  • 시스템이 페이지 파일을 확장하는 데 사용되는 짧은 기간 내에 있으므로 스레드가 스택을 확장할 수 없습니다.

스레드에서 실행되는 함수가 지역 변수를 할당하면 변수가 스레드의 호출 스택에 배치됩니다. 함수에 필요한 스택 공간의 양은 모든 지역 변수의 크기 합계만큼 클 수 있습니다. 그러나 컴파일러는 일반적으로 함수에 필요한 스택 공간을 줄이는 최적화를 수행합니다. 예를 들어 두 변수가 서로 다른 범위에 있는 경우 컴파일러는 두 변수에 대해 동일한 스택 메모리를 사용할 수 있습니다. 컴파일러는 계산을 최적화하여 일부 지역 변수를 완전히 제거할 수도 있습니다.

최적화의 양은 빌드 시 적용되는 컴파일러 설정의 영향을 받습니다. 예를 들어 /F(스택 크기 설정) - C++ 컴파일러 옵션입니다.

이 항목에서는 스레드, 스레드 블록, 스택 및 힙과 같은 개념에 대한 일반적인 지식을 가정합니다. 이러한 기본 개념에 대한 자세한 내용은 Mark Russinovich 및 David Solomon의 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(Stack Backtrace 표시) 명령을 사용할 수 있습니다.

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
...

스레드 데이터 구조

스레드에 대해 자세히 알아보려면 스레드 제어 블록 관련 구조에 대한 정보를 표시할 수도 있습니다. (여기에 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 내부 를 참조하세요.

_TEB 구조체의 32비트 버전을 살펴보면 TEB 구조체의 두 번째 및 세 번째 DWORD가 각각 스택의 아래쪽과 위쪽을 가리킨다는 것을 나타냅니다. 이 예제에서 이러한 주소는 0x00A00000 0x009FC000. (메모리에서 스택이 아래쪽으로 증가합니다.) ?을 사용하여 스택 크기를 계산할 수 있습니다. (식 계산) 명령:

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

스택 크기가 16K임을 보여줍니다. 최대 스택 크기는 이 TEB 구조의 일부인 DeallocationStack 필드에 저장됩니다. DeallocationStack 필드는 스택의 기반을 나타냅니다. 몇 가지 계산 후에 이 필드의 오프셋이 0xE0C 확인할 수 있습니다.

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

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

이는 최대 스택 크기가 256K임을 보여 하며, 이는 적절한 스택 공간이 남아 있음을 의미합니다.

또한 이 프로세스는 너무 큰 스택 기반 데이터 구조를 사용하여 무한 재귀 또는 스택 공간을 초과하지 않는 것처럼 보입니다.

이제 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)
         ..... 

먼저 페이지가 없는 풀 및 페이징된 풀 사용량을 살펴봅니다. 둘 다 한도 내에 있으므로 문제의 원인이 아닙니다.

다음으로, 커밋된 페이지 수를 확인합니다. 202280년 중 183528. 이것은 한계에 매우 가깝습니다. 이 디스플레이에 이 숫자가 완전히 제한되는 것은 아니지만 사용자 모드 디버깅을 수행하는 동안 시스템에서 다른 프로세스가 실행되고 있음을 명심해야 합니다. 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(Unassemble) 명령을 사용하여 해당 주소에서 어셈블러 코드를 확인합니다.

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(Registers) 명령은 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에서 실행하면 몇 번 반복된 다음 스택 오버플로 예외를 throw합니다.

(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++ 컴파일러 옵션