Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6266
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

Глава 16. Стек потока.docx 525

Рис. 16-2. Почти заполненный регион стека потока

Рис. 16-3. Целиком заполненный регион стека потока

526 Часть III. Управление памятью

Как и можно было предполагать, флаг PAGE_GUARD со страницы по адресу 0x08002000 удаляется, а странице по адресу 0x08001000 передается физическая память. Но этой странице не присваивается флаг PAGE_GUARD. Это значит, что региону адресного пространства, зарезервированному под стек потока, теперь передана вся физическая память, которая могла быть ему передана. Самая нижняя страница остается зарезервированной, физическая память ей никогда не передается. Чуть позже я поясню, зачем это сделано.

Передавая физическую память странице по адресу 0x08001000, система выполняет еще одну операцию: генерирует исключение EXCEPTION_STACK_ OVERFLOW (в файле WinNT.h оно определено как 0xС00000FD). При использовании структурной обработки исключений (SEH) ваша программа получит уведомление об этой ситуации и сможет корректно обработать ее. Подробнее о SEH см. главы 23, 24 и 25, а также листинг программы Summation, приведенный в конце этой главы.

Если поток продолжит использовать стек даже после исключения, связанного с переполнением стека, будет задействована вся память на странице по адресу 0x08001000, и поток попытается получить доступ к странице по адресу 0x08000000. Поскольку эта страница лишь зарезервирована (но не передана), возникнет исключение — нарушение доступа. Если это произойдет в момент обращения потока к стеку, вас ждут крупные неприятности. Система передаст управление службе Windows Error Reporting service, которая покажет следующее сообщение и завершит весь процесс (а не только поток, в котором возникла ошибка).

Приложение может заставить систему сгенерировать исключение EXCEPTION_STACK_OVERFLOW и раньше, вызвав функцию SetThreadStackGuarantee. Это позволяет гарантировать наличие свободной области заданного размера перед сторожевой страницей стека. Таким образом, в распоряжении приложения будет несколько страниц, прежде чем следующая попытка записи в стек заставит службу Windows Error Reporting прервать работу процесса.

Внимание! Исключение EXCEPTION_STACK_OVERFLOW генерируется при обращении потока к сторожевой странице. Если поток перехватит это исключение и продолжит работу, повторных исключений не будет, поскольку других сторожевых страниц не предусмотрено. Чтобы в дальнейшем получить исключение EXCEPTION_STACK_OVERFLOW, поток должен сбросить сторожевую страницу вызовом функции _resetstkoflw из биб-

лиотеки (см. malloc.h).

Глава 16. Стек потока.docx 527

Теперь объясню, почему нижняя страница стека всегда остается зарезервированной. Это позволяет защищать другие данные процесса от случайной перезаписи. Видите ли, по адресу 0x07FFF000 (на 1 страницу ниже, чем 0х08000000) может быть передана физическая память для другого региона адресного пространства. Если бы странице по адресу 0x08000000 была передана физическая память, система не сумела бы перехватить попытку потока расширить стек за пределы зарезервированного региона. А если бы стек расползся за пределы этого региона, поток мог бы перезаписать другие данные в адресном пространстве своего процесса — такого «жучка» выловить очень сложно.

Блок перед стеком предназначен для перехвата его переполнения, а блок после стека — для перехвата обращений к несуществующим областям стека. Чтобы понять, какая польза от последнего блока, рассмотрим такой фрагмент кода:

int WINAPI WinMain (HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int nCmdShow) {

BYTE aBytes[100];

 

aBytes[10000] = 0;

// Stack underflow

return(0);

 

}

Когда выполняется оператор присвоения, происходит попытка обращения за конец стека потока. Разумеется, ни компилятор, ни компоновщик не уловят эту ошибку в приведенном фрагменте кода, а нарушения доступа может и не случиться, если сразу за стеком потока окажется другой регион. И если вы случайно обратитесь за пределы стека, вы можете испортить содержимое области памяти, принадлежащей другой части вашего процесса, — система ничего не заметит. Вот пример кода, в котором обращение к области за пределами сетка неизбежно вызывает повреждение памяти, т.к. приложение выделяет блок памяти, прилегающий к нижней границе стека:

DWORD WINAPI ThreadFunc(PV0ID pvParam) {

BYTE aBytes[0x10];

//Определяем, где именно в виртуальном адресном пространстве находится стек;

//подробнее о VirtualQuery см. в главе 14.

MEM0RY_BASIC_INF0RMATI0N mbi;

SIZE_T size = VirtualQuery(aBytes, &mbi, sizeof(mbi));

// выделяем блок сразу после 1-Мб региона стека

SIZE_T s = (SIZE_T)mbi.AllocationBase + 1024*1024; PBYTE pAddress = (PBYTE)s;

BYTE* pBytes = (BYTE*)VirtualAlloc(pAddress, 0x10000, MEM_C0MMIT | MEM_RESERVE, PAGE_READWRITE);

528Часть III. Управление памятью

//Имитируем ошибку из-за обращения к памяти за пределами стека,

//которая останется незамеченной.

aBytes[0x10000] = 1; // записываем в блок, расположенный после стека

return(0);

}

Функция из библиотеки С/С++ для контроля стека

Библиотека С/С++ содержит функцию, позволяющую контролировать стек. Транслируя исходный код программы, компилятор при необходимости генерирует вызовы этой функции. Она обеспечивает корректную передачу страниц физической памяти стеку потока.

Возьмем, к примеру, небольшую функцию, требующую массу памяти под свои локальные переменные:

void SomeFunction () { int nValues[4000];

// здесь что-то

делаем с массивом

nValues[0] = 0;

// а тут что-то присваиваем

}

Для размещения целочисленного массива функция потребует минимум 16 000 байтов стекового пространства, так как каждое целое значение занимает 4 байта. Код, генерируемый компилятором, обычно выделяет такое пространство в стеке простым уменьшением указателя стека процессора на 16 000 байтов. Однако система не передаст физическую память этой нижней области стека, пока не произойдет обращения по данному адресу.

В системе с размером страниц по 4 или 8 Кб это могло бы создать проблему. Если первое обращение к стеку проходит по адресу, расположенному ниже сторожевой страницы (как в показанном выше фрагменте кода), поток обратится к зарезервированной памяти, и возникнет нарушение доступа. Поэтому, чтобы можно было спокойно писать функции вроде приведенной выше, компилятор и вставляет в код вызовы библиотечной функции для контроля стека.

При трансляции программы компилятору известен размер страниц памяти, используемых целевым процессором (4 Кб для x86 и 8 Кб для IA-64). Встречая в программе ту или иную функцию, компилятор определяет требуемый для нее объем стека и, если он превышает размер одной страницы, вставляет вызов функции, контролирующей стек.

Ниже показан псевдокод, который иллюстрирует, что именно делает функция, контролирующая стек. (Я говорю «псевдокод» потому, что обычно эта функция реализуется поставщиками компиляторов на языке ассемблера.)

Глава 16. Стек потока.docx 529

// стандартной библиотеке С "известен" размер страницы в целевой системе

#ifdef _M_IA64

#define PAGESIZE

(8 *

1024)

// страницы no

8

Кб

#else

 

 

 

 

 

#define PAGESIZE

(4 *

1024)

// страницы по

4

Кб

#endif

 

 

 

 

 

void StackCheck(int nBytesNeededFromStack) {

//Получим значение указателя стека. В этом месте указатель стека

//еще НЕ был уменьшен для учета локальных переменных функции. PBYTE pbStackPtr = (указатель стека процессора);

while (nBytesNeededFromStack >= PAGESIZE) {

//смещаем страницу вниз по стеку - должна быть сторожевой pbStackPtr -= PAGESIZE;

//обращаемся к какому-нибудь байту на сторожевой странице, вызывая

//тем самым передачу новой страницы и сдвиг сторожевой страницы вниз pbStackPtr[0] = 0;

//уменьшаем требуемое количество байтов в стеке nBytesNeededFromStack -= PAGESIZE;

}

//перед возвратом управления функция StackCheck устанавливает регистр

//указателя стека на адрес, следующий за локальными переменными функции

}

В компиляторе Microsoft Visual С++ предусмотрен параметр /GS, позволяющий контролировать пороговый предел числа страниц, начиная с которого компилятор автоматически вставляет в программу вызов функции StackCheck (см. http://msdn2.microsoft.com/en-us/library/9598wk25(VS.80).aspx). Используйте этот параметр, только если вы точно знасте, что делаете, и если это действительно нужно. В 99,99999 процентах из ста приложения и DLL нс требуют применения упомянутого параметра.

Примечание. Компилятор Microsoft С/С++ также поддерживает параметры для обнаружения повреждения стека во время выполнения. При отладочной (DEBUG) сборке С++-проекта no умолчанию активен параметр /RTCsu

компилятора (см. http://msdn2.micwsoft.com/enus/library/8wtf2dfz(VS.80).aspx). Если во время выполнения случится переполнение массива локальных переменных, вставленный компилятором код обнаружит это и уведомит вас при возврате управления сбойной функцией. Параметр /RTC поддерживается только для отладочной сборки. Для окончательной (RELEASE) сборки необходимо включить параметр /GS компилятора. Этот параметр заставляет компилятор добавить код, который записывает состояние стека в cookie-файл перед вызовом функций, а после вызова проверяет целостность функций. Такие предосто-

530 Часть III. Управление памятью

рожности блокируют попытки вредоносных программ инициировать переполнение стека с целью перехвата управления путем перезаписи адреса возврата в стеке. Сверка содержимого стека с cookie-файлом выявляет повреждение и работа приложения прерывается. Весьма детальный разбор параметра /GS compiler см. в статье http://www.symantec.com/avcenter/reference/GS_Protections_in_Vista.pdf.

Программа-пример Summation

Эта программа, «16 Summation.exe» (см. листинг на рис. 16-6), демонстрирует использование фильтров и обработчиков исключений для корректного восстановления после переполнения стека. Файлы исходного кода и ресурсов этой программы находятся в каталоге 16-Summation внутри архива, доступного на сайте поддержки этой книги. Возможно, вам придется сначала прочесть главы по SEH, чтобы понять, как работает эта программа.

Она суммирует числа от 0 до x, где x — число, введенное пользователем. Конечно, проще было бы написать функцию с именем Sum, которая вычисляла бы по формуле:

Sum = (x * (x + 1)) / 2;

Но для этого примера я сделал функцию Sum рекурсивной, чтобы она использовала большое стековое пространство.

При запуске программы появляется диалоговое окно, показанное ниже.

В этом окне вы вводите число и щелкаете кнопку Calculate. Программа создает поток, единственная обязанность которого — сложить все числа от 0 до x. Пока он выполняется, первичный поток программы, вызвав WaitForSingleObject, просит систему не выделять ему процессорное время. Когда новый поток завершается, система вновь выделяет процессорное время первичному потоку. Тот выясняет сумму, получая код завершения нового потока вызовом GetExitCodeThread, и — это очень важно — закрывает свой описатель нового потока, так что система может уничтожить объект ядра «поток», и утечки ресурсов не произойдет.

Далее первичный поток проверяет код завершения суммирующего потока. Если он равен UINT_MAX, значит, произошла ошибка: суммирующий поток переполнил стек при подсчете суммы; тогда первичный поток выведет окно с соответствующим сообщением. Если же код завершения отличен от UINT_MAX, суммирующий поток отработал успешно; код завершения и есть искомая сумма. В этом случае первичный поток просто отображает результат суммирования в диалоговом окне.

Глава 16. Стек потока.docx 531

Теперь обратимся к суммирующему потоку. Его функция — SumThreadFunc. При создании этого потока первичный поток передает ему в единственном параметре pvParam количество целых чисел, которые следует просуммировать. Затем его функция инициализирует переменную uSum значением UINT_MAX, т. е. изначально предполагается, что работа функции не завершится успехом. Далее SumThreadFunc активизирует SEH так, чтобы перехватывать любое исключение, возникающее при выполнении потока. После чего для вычисления суммы вызывается рекурсивная функция Sum.

Если сумма успешно вычислена, SumThreadFunc просто возвращает значение переменной uSum; оно и будет кодом завершения потока. Но, если при выполнении Sum возникает исключение, система сразу оценивает выражение в фильтре исключений. Иначе говоря, система вызывает FilterFunc, передавая ей код исключения. В случае переполнения стека этим кодом будет EXCEPTION_STACK_OVERFLOW. Чтобы увидеть, как программа обрабатывает исключение, вызванное переполнением стека, дайте ей просуммировать числа от

0 до 44000.

Моя функция FilterFunc очень проста. Сначала она проверяет, произошло ли исключение, связанное с переполнением стека. Если нет, возвращает EXCEPTION_CONTINUE_SEARCH, а если да — EXCEPTION_EXECUTE_HANDLER.

Это подсказывает системе, что фильтр готов к обработке этого исключения и что надо выполнить код в блоке except. В данном случае обработчик исключения ничего особенного не делает, просто закрывая поток с кодом завершения UINT_MAX. Родительский поток, получив это специальное значение, выводит пользователю сообщение с предупреждением.

И последнее, что хотелось бы обсудить: почему я выделил функцию Sum в отдельный поток вместо того, чтобы просто создать SEH-фрейм в первичном потоке и вызывать Sum из его блока try. На то есть несколько причин.

Во-первых, всякий раз, когда создается поток, он получает стек размером 1 Мб. Если бы я вызывал Sum из первичного потока, часть стекового пространства уже была бы занята, и функция не смогла бы использовать весь объем стека. Согласен, моя программа очень проста и, может быть, не займет слишком большое стековое пространство. А если программа посложнее? Легко представить ситуацию, когда Sum подсчитывает сумму целых чисел от 0 до 1000 и стек вдруг оказывается чем-то занят, — тогда его переполнение произойдет, скажем, еще при вычислении суммы от 0 до 750. Таким образом, работа функции Sum будет надежнее, если предоставить ей полный стек, не используемый другим кодом.

Вторая причина в том, что поток уведомляется об исключении «переполнение стека» лишь однажды. Если бы я вызывал Sum из первичного потока и произошло бы переполнение стека, то это исключение было бы перехвачено и корректно обработано. Но к тому моменту физическая память была бы передана под все зарезервированное адресное пространство стека, и в нем уже не осталось бы страниц с флагом защиты. Начни пользователь новое

532 Часть III. Управление памятью

суммирование, и функция Sum переполнила бы стек, а соответствующее исключение не было бы возбуждено. Вместо этого возникло бы исключение «нарушение доступа», и корректно обработать эту ситуацию уже не удалось бы. Естественно, проблему можно было 6ы устранить вызовом библиотечной функции

_resetstkoflw.

В-третьих, физическую память, отведенную под его стек, можно освободить. Рассмотрим такой сценарий: пользователь просит функцию Sum вычислить сумму целых чисел от 0 до 30 000. Это требует передачи региону стека весьма ощутимого объема памяти. Затем пользователь проводит несколько операций суммирования — максимум до 5000. И окажется, что стеку передан порядочный объем памяти, который больше не используется. А ведь эта физическая память выделяется из страничного файла. Так что лучше бы освободить ее и вернуть системе. И поскольку программа завершает поток SumThreadFunc, система автоматически освобождает физическую память, переданную региону стека.

И последнее, почему я использую отдельный поток: если поручить всю работу одному потоку, придется использовать синхронизацию, чтобы скоординировать исполнение потока и возврат результатов вычислений. Для такой простой программы, как в этом примере, проще всего породить для расчетов новый поток, передать ему входные данные для суммирования и дождаться результатов.

Summation.cpp

/****************************************************************************** Module: Summation.cpp

Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre

******************************************************************************/

#include "..\CommonFiles\CmnHdr.h" /* See Appendix A. */ #include <windowsx.h>

#include <limits.h> #include <tchar.h> #include "Resource.h"

///////////////////////////////////////////////////////////////////////////////

// программа вызывает Sum для uNum = 0 до 9

// uNum: 0 1 2 3 4 5 6 7 8 9 ...

// Sum: 0 1 3 6 10 15 21 28 36 45 ...

UINT Sum(UINT uNum) {

// рекурсивный вызов Sum

return((uNum == 0) ? 0 : (uNum + Sum(uNum - 1)));

}

Глава 16. Стек потока.docx 533

///////////////////////////////////////////////////////////////////////////////

LONG WINAPI FilterFunc(DWORD dwExceptionCode) {

return((dwExceptionCode == STATUS_STACK_OVERFLOW)

? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);

}

///////////////////////////////////////////////////////////////////////////////

//отдельный поток, отвечающий за вычисление суммы.

//Я использую его по следующим причинам:

//1. Отдельный поток получает собственный мегабайт стекового пространства.

//2. Поток уведомляется о переполнении стека лишь однажды.

//3. Память, выделенная для стека, освобождается по завершении потока.

DWORD WINAPI SumThreadFunc(PVOID pvParam) {

//параметр pvParam определяет количество суммируемых чисел.

UINT uSumNum = PtrToUlong(pvParam);

//uSum содержит сумму чисел от 0 до uSumNum.

//если сумму вычислить не удалось, возвращается значение UINT_MAX. UINT uSum = UINT_MAX;

__try {

//для перехвата исключения «переполнение стека»

//функцию Sum надо выполнять в SEH-фрейме. uSum = Sum(uSumNum);

}

__except (FilterFunc(GetExceptionCode())) {

//Если мы попали сюда, то это потому, что перехватили переполнение

//стека. Здесь можно сделать все, что надо для корректного

//возобновления работы. Но, так как от этого примера больше ничего

//не требуется, кода в блоке обработчика нет.

}

//Кодом завершения потока является либо сумма первых uSumNum

//чисел, либо UINT_MAX в случае переполнения стека. return(uSum);

}

///////////////////////////////////////////////////////////////////////////////

BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam) {

chSETDLGICONS(hWnd, IDI_SUMMATION);

534 Часть III. Управление памятью

// мы принимаем не более чем девятизначные числа

Edit_LimitText(GetDlgItem(hWnd, IDC_SUMNUM), 9);

return(TRUE);

}

///////////////////////////////////////////////////////////////////////////////

void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {

switch (id) { case IDCANCEL:

EndDialog(hWnd, id); break;

case IDC_CALC:

// получаем количество целых числе, которые

//пользователь хочет просуммировать

BOOL bSuccess = TRUE;

UINT uSum = GetDlgItemInt(hWnd, IDC_SUMNUM, &bSuccess, FALSE); if (!bSuccess) {

MessageBox(hWnd, TEXT("Please enter a valid numeric value!"), TEXT("Invalid input..."), MB_ICONINFORMATION | MB_OK);

SetFocus(GetDlgItem(hWnd, IDC_CALC)); break;

}

//создаем поток (с собственным стеком), отвечающий за суммирование

DWORD dwThreadId;

HANDLE hThread = chBEGINTHREADEX(NULL, 0,

SumThreadFunc, (PVOID) (UINT_PTR) uSum, 0, &dwThreadId);

//ждем завершения потока.

WaitForSingleObject(hThread, INFINITE);

//код завершения — результат суммирования. GetExitCodeThread(hThread, (PDWORD) &uSum);

//закончив, закрываем описатель потока,

//чтобы система могла разрушить объект ядра «поток»

CloseHandle(hThread);

//обновляем содержимое диалогового окна.

if (uSum == UINT_MAX) {

// если код завершения равен UINT_MAX,

Соседние файлы в предмете Программирование на C++