Издание четвертое windows ® для профессионалов создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows



Pdf просмотр
страница41/68
Дата28.11.2016
Размер3.57 Mb.
Просмотров12466
Скачиваний0
1   ...   37   38   39   40   41   42   43   44   ...   68
Адрес
Страницы, зарезервированные для перехвата переполнения стека
Переданная страница с атрибутом PAGE_READWRITE для совместимости с разрядными компонентами)
Нижняя часть стека (зарезервирована для перехвата переполнения стека)
Размер
16 страниц
(65 536 байтов страница
(4096 байтов страниц
(32 768 байтов страниц
(1 Мб)
7 страниц
(28 672 байта)
Рис. 16-5.
Целиком заполненный регион стека потока в Windows 98

403
Г ЛАВА 16
Стек потока
Функция из библиотеки C/C++ для контроля стека
Библиотека C/C++ содержит функцию, позволяющую контролировать стек. Транслируя исходный код программы, компилятор при необходимости генерирует вызовы этой функции. Она обеспечивает корректную передачу страниц физической памяти стеку потока.
Возьмем, к примеру, небольшую функцию, требующую массу памяти под свои локальные переменные SomeFunction() {
int nValues[4000];
// здесь что то делаем с массивом nValues[0] = 0; // а тут что то присваиваем
}
Для размещения целочисленного массива функция потребует минимум 16 байтов стекового пространства, так как каждое целое значение занимает 4 байта. Код,
генерируемый компилятором, обычно выделяет такое пространство в стеке простым уменьшением указателя стека процессора на 16 000 байтов. Однако система не передаст физическую память этой нижней области стека, пока не произойдет обращения поданному адресу.
В системе с размером страниц поили Кб это могло бы создать проблему. Если первое обращение к стеку проходит по адресу, расположенному ниже сторожевой страницы (как в показанном выше фрагменте кода, поток обратится к зарезервированной памяти, и возникнет нарушение доступа. Поэтому, чтобы можно было спокойно писать функции вроде приведенной выше, компилятор и вставляет в код вызовы библиотечной функции для контроля стека.
При трансляции программы компилятору известен размер страниц памяти, используемых целевым процессором (4 Кб для
x86 и 8 Кб для Alpha). Встречая в программе ту или иную функцию, компилятор определяет требуемый для нее объем стека и, если он превышает размер одной страницы, вставляет вызов функции, контролирующей стек.
Ниже показан псевдокод, который иллюстрирует, что именно делает функция,
контролирующая стек. (Я говорю псевдокод потому, что обычно эта функция реализуется поставщиками компиляторов на языке ассемблера стандартной библиотеке C "известен" размер страницы в целевой системе _M_ALPHA
#define PAGESIZE (8 * 1024)
// страницы по 8 Кб
#else
#define PAGESIZE (4 * 1024)
// страницы по 4 Кб
#endif void StackCheck(int nBytesNeededFromStack) {
// Получим значение указателя стека. В этом месте указатель стека еще НЕ был уменьшен для учета локальных переменных функции pbStackPtr = (указатель стека процессора (nBytesNeededFromStack >= PAGESIZE) {
// смещаем страницу вниз по стеку должна быть сторожевой pbStackPtr = PAGESIZE;
см. след. стр.

404
Ч АС Т Ь I I I
УПРАВЛЕНИЕ ПАМЯТЬЮ обращаемся к какому нибудь байту на сторожевой странице, вызывая тем самым передачу новой страницы и сдвиг сторожевой страницы вниз pbStackPtr[0] = 0;
// уменьшаем требуемое количество байтов в стеке nBytesNeededFromStack = PAGESIZE;
}
// перед возвратом управления функция StackCheck устанавливает регистр указателя стека на адрес, следующий за локальными переменными функции
}
В компиляторе Microsoft Visual C++ предусмотрен параметр, позволяющий контролировать пороговый предел числа страниц, начиная с которого компилятор автоматически вставляет в программу вызов функции
StackCheck. Используйте этот параметр, только если Вы точно знаете, что делаете, и если это действительно нужно. В процентах из ста приложения и DLL не требуют применения упомянутого параметра.
Программа-пример Summation
Эта программа, «16 Summation.exe» (см. листинг на рис. 16 6), демонстрирует использование фильтров и обработчиков исключений для корректного восстановления после переполнения стека. Файлы исходного кода и ресурсов этой программы находятся в каталоге 16 Summation на компакт диске, прилагаемом к книге. Возможно, Вам придется сначала прочесть главы по SEH, чтобы понять, как работает эта программа.
Она суммирует числа от 0 дох, где х — число, введенное пользователем. Конечно,
проще было бы написать функцию с именем
Sum, которая вычисляла бы по формуле = (x * (x + 1)) / Но для этого примера я сделал функцию
Sum рекурсивной, чтобы она использовала большое стековое пространство.
При запуске программы появляется диалоговое окно, показанное ниже.
В этом окне Вы вводите число и щелкаете кнопку Calculate. Программа создает поток, единственная обязанность которого — сложить все числа от 0 дох. Пока он выполняется, первичный поток программы, вызвав
WaitForSingleObject, просит систему не выделять ему процессорное время. Когда новый поток завершается, система вновь выделяет процессорное время первичному потоку. Тот выясняет сумму, получая код завершения нового потока вызовом
GetExitCodeThread, и — это очень важно закрывает свой описатель нового потока, так что система может уничтожить объект ядра потоки утечки ресурсов не произойдет.
Далее первичный поток проверяет код завершения суммирующего потока. Если он равен UINT_MAX, значит, произошла ошибка суммирующий поток переполнил стек при подсчете суммы тогда первичный поток выведет окно с соответствующим сообщением. Если же код завершения отличен от UINT_MAX, суммирующий поток

405
Г ЛАВА 16
Стек потока отработал успешно код завершения и есть искомая сумма. В этом случае первичный поток просто отображает результат суммирования в диалоговом окне.
Теперь обратимся к суммирующему потоку. Его функция —
SumThreadFunc. При создании этого потока первичный поток передает ему в единственном параметре
pvParam количество целых чисел, которые следует просуммировать. Затем его функция инициализирует переменную
uSum значением UINT_MAX, те. изначально предполагается, что работа функции не завершится успехом. Далее
SumThreadFunc активизирует так, чтобы перехватывать любое исключение, возникающее при выполнении потока. После чего для вычисления суммы вызывается рекурсивная функция
Sum.
Если сумма успешно вычислена,
SumThreadFunc просто возвращает значение переменной оно и будет кодом завершения потока. Но, если при выполнении Sum
возникает исключение, система сразу оценивает выражение в фильтре исключений.
Иначе говоря, система вызывает
FilterFunc, передавая ей код исключения. В случае переполнения стека этим кодом будет EXCEPTION_STACK_OVERFLOW. Чтобы увидеть,
как программа обрабатывает исключение, вызванное переполнением стека, дайте ей просуммировать числа от 0 до Моя функция
FilterFunc очень проста. Сначала она проверяет, произошло ли исключение, связанное с переполнением стека. Если нет, возвращает EXCEPTION_CON
TINUE_SEARCH, а если да — EXCEPTION_EXECUTE_HANDLER. Это подсказывает системе, что фильтр готов к обработке этого исключения и что надо выполнить код в блоке
except. В данном случае обработчик исключения ничего особенного не делает,
просто закрывая поток с кодом завершения UINT_MAX. Родительский поток, получив это специальное значение, выводит пользователю сообщение с предупреждением.
И последнее, что хотелось бы обсудить почему я выделил функцию
Sum в отдельный поток вместо того, чтобы просто создать SEH фрейм в первичном потоке ивы зывать
Sum из его блока try. На то есть три причины.
Во первых, всякий раз, когда создается поток, он получает стек размером 1 Мб.
Если бы я вызывал
Sum из первичного потока, часть стекового пространства уже была бы занята, и функция не смогла бы использовать весь объем стека. Согласен, моя программа очень проста и, может быть, не займет слишком большое стековое пространство. А если программа посложнее Легко представить ситуацию, когда
Sum подсчитывает сумму целых чисел от 0 дои стек вдруг оказывается чем то занят, — тогда его переполнение произойдет, скажем, еще при вычислении суммы от 0 до Таким образом, работа функции
Sum будет надежнее, если предоставить ей полный стек, неиспользуемый другим кодом.
Вторая причина в том, что поток уведомляется об исключении переполнение стека лишь однажды. Если бы я вызывал
Sum из первичного потока и произошло бы переполнение стека, то это исключение было бы перехвачено и корректно обработано. Но к тому моменту физическая память была бы передана под все зарезервированное адресное пространство стека, ив нем уже не осталось бы страниц с флагом защиты. Начни пользователь новое суммирование, и функция
Sum переполнила бы стека соответствующее исключение не было бы возбуждено. Вместо этого возникло бы исключение нарушение доступа, и корректно обработать эту ситуацию уже не удалось бы.
И последнее, почему я использую отдельный поток физическую память, отведенную под его стек, можно освободить. Рассмотрим такой сценарий пользователь просит функцию
Sum вычислить сумму целых чисел от 0 до 30 000. Это требует передачи региону стека весьма ощутимого объема памяти. Затем пользователь проводит не

406
Ч АС Т Ь I I I
УПРАВЛЕНИЕ ПАМЯТЬЮ
сколько операций суммирования — максимум до 5000. И окажется, что стеку передан порядочный объем памяти, который больше не используется. А ведь эта физическая память выделяется из страничного файла. Так что лучше бы освободить ее и вернуть системе. И поскольку программа завершает поток
SumThreadFunc, система автоматически освобождает физическую память, переданную региону стека.
Summation.cpp
/******************************************************************************
Модуль: Автор Copyright (c) 2000, Джеффри Рихтер (Jeffrey Richter)
******************************************************************************/
#include "..\CmnHdr.h"
/* см. приложение А */
#include
#include
#include
// для доступа к _beginthreadex
#include
#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)));
}
///////////////////////////////////////////////////////////////////////////////
LONG WINAPI FilterFunc(DWORD dwExceptionCode) {
return((dwExceptionCode == STATUS_STACK_OVERFLOW)
? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);
}
///////////////////////////////////////////////////////////////////////////////
// Отдельный поток, отвечающий за вычисление суммы Я использую его последующим причинам 1. Отдельный поток получает собственный мегабайт стекового пространства 2. Поток уведомляется о переполнении стека лишь однажды 3. Память, выделенная для стека, освобождается по завершении потока WINAPI SumThreadFunc(PVOID pvParam) {
// параметр pvParam определяет количество суммируемых чисел uSumNum = PtrToUlong(pvParam);
Рис. 16-6.
Программа-пример Summation

407
Г ЛАВА 16
Стек потока
Рис. 16-6.
продолжение
// 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);
// мы принимаем не более чем девятизначные целые числа, 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:
// получаем количество целых чисел, которые пользователь хочет просуммировать uSum = GetDlgItemInt(hwnd, IDC_SUMNUM, NULL, FALSE);
// создаем поток (с собственным стеком, отвечающий за суммирование dwThreadId;
HANDLE hThread = chBEGINTHREADEX(NULL, 0,
SumThreadFunc, (PVOID) (UINT_PTR) uSum, 0, &dwThreadId);
см. след. стр.

408
Ч АС Т Ь I I I
УПРАВЛЕНИЕ ПАМЯТЬЮ
Рис. 16-6.
продолжение
// ждем завершения потока, INFINITE);
// код завершения — результат суммирования, (PDWORD) &uSum);
// закончив, закрываем описатель потока чтобы система могла разрушить объект ядра "поток обновляем содержимое диалогового окна if (uSum == UINT_MAX) {
// если код завершения равен UINT_MAX,
// произошло переполнение стека, IDC_ANSWER, TEXT("Error"));
chMB("The number is too big, please enter a smaller number");
} else {
// сумма вычислена успешно, IDC_ANSWER, uSum, FALSE);
}
break;
}
}
///////////////////////////////////////////////////////////////////////////////
INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog);
chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand);
}
return(FALSE);
}
///////////////////////////////////////////////////////////////////////////////
int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) {
DialogBox(hinstExe, MAKEINTRESOURCE(IDD_SUMMATION), NULL, Dlg_Proc);
return(0);
}
//////////////////////////////// Конец файла //////////////////////////////////

409
Г ЛАВА 7
Проецируемые в память файлы
О
перации с файлами — это то, что рано или поздно приходится делать практически во всех программах, и всегда это вызывает массу проблем. Должно ли приложение просто открыть файл, считать и закрыть его, или открыть, считать фрагмент в буфер и перезаписать его в другую часть файла В Windows многие из этих проблем решаются очень изящно — с помощью
проецируемых в память файлов (memory mapped Как и виртуальная память, проецируемые файлы позволяют резервировать регион адресного пространства и передавать ему физическую память. Различие между этими механизмами состоит в том, что в последнем случае физическая память не выделяется из страничного файла, а берется из файла, уже находящегося на диске. Как только файл спроецирован в память, к нему можно обращаться так, будто он целиком в нее загружен.
Проецируемые файлы применяются для:
í
загрузки и выполнения EXE и DLL файлов. Это позволяет существенно экономить как на размере страничного файла, таки на времени, необходимом для подготовки приложения к выполнению;
í
доступа к файлу данных, размещенному на диске. Это позволяет обойтись без операций файлового ввода вывода и буферизации его содержимого;
í
разделения данных между несколькими процессами, выполняемыми на одной машине. (Весть и другие методы для совместного доступа разных процессов к одним данным — но все они так или иначе реализованы на основе проецируемых в память файлов.)
Эти области применения проецируемых файлов мы и рассмотрим в данной главе.
Проецирование в память EXE- и DLL-файлов
При вызове из потока функции
CreateProcess система действует так:
1.
Отыскивает EXE файл, указанный при вызове
CreateProcess. Если файл не найден, новый процесс не создается, а функция возвращает Создает новый объект ядра «процесс».
3.
Создает адресное пространство нового процесса.
4.
Резервирует регион адресного пространства — такой, чтобы в него поместился данный EXE файл. Желательное расположение этого региона указывается внутри самого EXE файла. По умолчанию базовый адрес EXE файла — в 64 разрядном приложении под управлением 64 разрядной Windows этот адрес может быть другим. При создании исполняемого файла приложения базовый адрес может быть изменен через параметр компоновщика /BASE.

410
Ч АС Т Ь I I I
УПРАВЛЕНИЕ ПАМЯТЬЮ
5.
Отмечает, что физическая память, связанная с зарезервированным регионом, —
EXE файл на диске, а не страничный файл.
Спроецировав EXE файл на адресное пространство процесса, система обращается к разделу EXE файла со списком DLL, содержащих необходимые программе функции. После этого система, вызывая
LoadLibrary, поочередно загружает указанные (а при необходимости и дополнительные) DLL модули. Всякий раз, когда для загрузки вызывается
LoadLibrary, система выполняет действия, аналогичные описанным выше в пп. 4 и Резервирует регион адресного пространства — такой, чтобы в него мог поместиться заданный DLL файл. Желательное расположение этого региона указывается внутри самого DLL файла. По умолчанию Microsoft Visual C++ присваивает модулям базовый адрес 0x10000000 (в 64 разрядной DLL под управлением разрядной Windows 2000 этот адрес может быть другим. При компоновке это значение можно изменить с помощью параметра /BASE. У всех стандартных системных DLL, поставляемых с Windows, разные базовые адреса, чтобы не допустить их перекрытия при загрузке водно адресное простран ство.
2.
Если зарезервировать регион по желательному для DLL базовому адресу не удается (из за того, что он слишком мал либо занят каким то еще EXE или файлом, система пытается найти другой регион. Но по двум причинам такая ситуация весьма неприятна. Во первых, если в DLL нет информации о возможной переадресации (relocation information), загрузка может вообще не получиться. (Такую информацию можно удалить из DLL при компоновке с параметром. Это уменьшит размер DLL файла, но тогда модуль
должен грузиться только по указанному базовому адресу) Во вторых, системе приходится выполнять модификацию адресов (relocations) внутри DLL. В Windows 98 эта операция осуществляется по мере подкачки страниц в оперативную память. Нов на это уходит дополнительная физическая память, выделяемая из страничного файла, да и загрузка такой DLL займет больше времени.
3.
Отмечает, что физическая память, связанная с зарезервированным регионом, —
DLL файл на диске, а не страничный файл. Если Windows 2000 пришлось выполнять модификацию адресов из за того, что DLL не удалось загрузить по желательному базовому адресу, она запоминает, что часть физической памяти для DLL связана со страничным файлом.
Если система почему либо не свяжет EXE файл с необходимыми ему DLL, на экране появится соответствующее сообщение, а адресное пространство процесса и объект процесс будут освобождены. При этом
CreateProcess вернет FALSE; прояснить причину сбоя поможет функция
GetLastError.
После увязки EXE и DLL файлов с адресным пространством процесса начинает исполняться стартовый код EXE файла. Подкачку страниц, буферизацию и кэширо вание система берет на себя. Например, если код в EXE файле переходит к команде,
не загруженной в память, возникает ошибка. Обнаружив ее, система перекачивает нужную страницу кода из образа файла на страницу оперативной памяти. Затем отображает страницу оперативной памяти на должный участок адресного пространства процесса, тем самым позволяя потоку продолжить выполнение кода. Все эти операции скрыты от приложения и периодически повторяются при каждой попытке процесса обратиться к коду или данным, отсутствующим в оперативной памяти.

411
Г ЛАВА 17
Проецируемые в память файлы
Статические данные не разделяются несколькими
экземплярами EXE или DLL
Когда Вы создаете новый процесс для уже выполняемого приложения, система просто открывает другое проецируемое в память представление (view) объекта проекция файла (file mapping object), идентифицирующего образ исполняемого файла, и создает новые объекты процесс и поток (для первичного потока. Этим объектам присваиваются идентификаторы процесса и потока. С помощью проецируемых в память файлов несколько одновременно выполняемых экземпляров приложения может совместно использовать один и тот же код, загруженный в оперативную память.
Здесь возникает небольшая проблема. Процессы используют линейное (flat) адресное пространство. При компиляции и компоновке программы весь ее код и данные объединяются в нечто, так сказать, большое и цельное. Данные, конечно, отделены от кода, но только в том смысле, что они расположены вслед за кодом в EXE файле. Вот упрощенная иллюстрация того, как код и данные приложения загружаются в виртуальную память, а затем отображаются на адресное пространство процесса:
Раздел кода из 3 страниц
Раздел данных из 2 страниц
Виртуальная
память
Страница кода Страница кода Страница данных Страница кода Страница данных 1
Адресное пространство
программы
Страница кода Страница кода Страница кода Страница данных Страница данных 2
Исполняемый
файл на диске
Теперь допустим, что запущен второй экземпляр программы. Система просто напросто проецирует страницы виртуальной памяти, содержащие код и данные файла,
на адресное пространство второго экземпляра приложения:
Страница кода Страница кода Страница кода Страница данных Страница данных 2
Адресное пространство
второго экземпляра
Виртуальная
память
Страница кода Страница кода Страница данных Страница кода Страница данных Страница кода Страница кода Страница кода Страница данных Страница данных 2
Адресное пространство
первого экземпляра
Если один экземпляр приложения модифицирует какие либо глобальные переменные, размещенные на странице данных, содержимое памяти изменяется для всех экземпляров этого приложения. Такое изменение могло бы привести к катастрофическим последствиями поэтому недопустимо.
1
На самом деле содержимое файла разбито на отдельные разделы (sections). Код находится водном разделе, а глобальные переменные — в другом. Разделы выравниваются по границам страниц. Приложение определяет размер страницы через функцию
GetSystemInfo. Вили файле раздел кода обычно предшествует разделу данных.

412
Ч АС Т Ь I I I
УПРАВЛЕНИЕ ПАМЯТЬЮ
Система предотвращает подобные ситуации, применяя механизм копирования при записи. Всякий раз, когда программа пытается записывать что тов файл, спрое цированный в память, система перехватывает эту попытку, выделяет новый блок памяти, копирует в него нужную программе страницу и после этого разрешает запись в новый блок памяти. Благодаря этому работа остальных экземпляров программы не нарушается. Вот что получится, когда первый экземпляр программы попытается изменить какую нибудь глобальную переменную на второй странице данных:
Страница кода Страница кода Страница кода Страница данных Страница данных 2
Адресное пространство
второго экземпляра
Виртуальная
память
Страница кода Страница кода Страница кода Страница данных Страница данных 2
Адресное пространство
первого экземпляра
Страница кода Страница кода Страница данных Страница кода Страница данных Новая страница
Система выделяет новую страницу и копирует на нее содержимое страницы данных. Адресное пространство первого экземпляра изменяется так, чтобы отобразить новую страницу данных на тот же участок, что и исходную. Теперь процесс может изменить глобальную переменную, не затрагивая данные другого экземпляра Аналогичная цепочка событий происходит и при отладке приложения. Например,
запустив несколько экземпляров программы, Вы хотите отладить только один из них.
Вызвав отладчик, Выставите в строке исходного кода точку прерывания. Отладчик модифицирует Ваш код, заменяя одну из команд на языке ассемблера другой — заставляющей активизировать сам отладчик. И здесь Вы сталкиваетесь стой же проблемой. После модификации кода все экземпляры программы, доходя до исполнения измененной команды, приводили бык его активизации. Чтобы этого избежать, система вновь использует копирование при записи. Обнаружив попытку отладчика изменить кодона выделяет новый блок памяти, копирует туда нужную страницу и позволяет отладчику модифицировать код на этой копии.
При загрузке процесса система просматривает все страницы образа файла.
Физическая память из страничного файла передается сразу только тем страницам, которые должны быть защищены атрибутом копирования при записи.
При обращении к такому участку образа файла в память загружается соответствующая страница. Если ее модификации не происходит, она может быть выгружена из памяти и при необходимости загружена вновь. Если же страница файла модифицируется, система перекачивает ее на одну из ранее переданных страниц в страничном файле.
Поведение Windows 2000 ив подобных случаях одинаково,
кроме ситуации, когда в память загружено два экземпляра одного модуля и никаких данных не изменено. Тогда процессы под управлением Windows могут совместно использовать данные, а в Windows 98 каждый процесс получает свою копию этих данных. Но если в память загружен лишь один экземпляр модуля или же данные были модифицированы (что чаще всего и бывает 2000 и Windows 98 ведут себя одинаково.
1   ...   37   38   39   40   41   42   43   44   ...   68


База данных защищена авторским правом ©nethash.ru 2019
обратиться к администрации

войти | регистрация
    Главная страница


загрузить материал