Отладка переполнения стека
Переполнение стека — это ошибка, с которой могут столкнуться потоки пользовательского режима. Существует три возможных причины этой ошибки:
Поток использует весь стек, зарезервированный для него. Это часто вызвано бесконечным рекурсией.
Поток не может расширить стек, так как файл страницы недоступен, и поэтому дополнительные страницы не могут быть зафиксированы для расширения стека.
Поток не может расширить стек, так как система находится в течение короткого периода, используемого для расширения файла страницы.
Если функция, запущенная в потоке, выделяет локальные переменные, переменные помещаются в стек вызовов потока. Объем пространства стека, требуемого функцией, может быть таким, как сумма размеров всех локальных переменных. Однако компилятор обычно выполняет оптимизацию, которая сокращает пространство стека, требуемое функцией. Например, если две переменные находятся в разных областях, компилятор может использовать одну и ту же память стека для обеих этих переменных. Компилятор также может полностью исключить некоторые локальные переменные путем оптимизации вычислений.
Объем оптимизации зависит от параметров компилятора, применяемых во время сборки. Например, по параметру /F (set Stack Size) — параметр компилятора C++.
В этом разделе предполагается общие знания о понятиях, таких как потоки, блоки потоков, стек и куча. Дополнительные сведения об этих базовых понятиях см. в статье microsoft Windows Internals by Mark Russinovich и Дэвид Соломон.
Отладка переполнения стека без символов
Ниже приведен пример отладки переполнения стека. В этом примере NTSD работает на том же компьютере, что и целевое приложение, и перенаправляет выходные данные в KD на хост-компьютере. Дополнительные сведения см. в разделе "Управление отладчиком пользовательского режима" из отладчика ядра.
Первый шаг заключается в том, какое событие привело к разрыву отладчика:
0:002> .lastevent
Last event: Exception C00000FD, second chance
Вы можете найти код исключения 0xC00000FD в ntstatus.h, этот код исключения STATUS_STACK_OVERFLOW, который указывает, что новая страница защиты для стека не может быть создана. Все коды состояния перечислены в значениях NTSTATUS 2.3.1.
Вы также можете использовать команду !error для поиска ошибок в отладчике Windows.
0:002> !error 0xC00000FD
Error code: (NTSTATUS) 0xc00000fd (3221225725) - A new guard page for the stack cannot be created.
Чтобы дважды проверить переполнение стека, можно использовать команду k (Display 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. Период слева от этой строки указывает, что это текущий поток.
Сведения о стеке содержатся в TEB (блок среды потоков) в 0x7FFDC000. Самый простой способ перечислить его — использовать !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 (display Memory) для отображения необработанных значений в этом расположении:
0:002> dd 7ffdc000 L4
7ffdc000 009fdef0 00a00000 009fc000 00000000
Чтобы интерпретировать это, необходимо найти определение структуры данных TEB. Используйте команду dt Display Type, чтобы сделать это в системе, где доступны символы.
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".
Глядя на 32-разрядную версию структуры _TEB, она указывает, что второй и третий DWORD в структуре TEB указывают на нижнее и верхнюю часть стека соответственно. В этом примере эти адреса 0x00A00000 и 0x009FC000. (Стек растет вниз в памяти.) Вы можете вычислить размер стека с помощью ? (Вычислять выражение) команда:
0:002> ? a00000-9fc000
Evaluate expression: 16384 = 00004000
В этом примере показано, что размер стека составляет 16 K. Максимальный размер стека хранится в поле DeallocationStack, который является частью этой структуры TEB. Поле 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 из 20280 года. Это очень близко к пределу. Несмотря на то, что это число не отображается полностью, следует помнить, что при выполнении отладки в пользовательском режиме другие процессы выполняются в системе. При каждом выполнении команды 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 (Set Symbol Path).
Чтобы создать переполнение стека, мы можем использовать этот код, который продолжает вызывать подпрограмму, пока стек не будет исчерпан.
// 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]
...
Если символы доступны _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
Мы также можем использовать команду !teb , отображающую stackBase abd StackLimit.
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
Сводка команд
- k (Отображение стека backtrace)
- ~ (состояние потока)
- d, da, db, dc, dd, dD, df, dp, dq, du, dw (display Memory)
- u, ub, uu (Unassemble)
- r (Registers)
- .sympath (задать путь к символам)
- x (проверка символов)
- dt (тип отображения)
- !анализировать
- !teb