
Роббинс Д. - Отладка приложений для Microsoft .NET и Microsoft Windows - 2004
.pdf
|
ГЛАВА 17 Стандартная отладочная библиотека C и управление памятью |
633 |
||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
size_t |
nDataSize |
; |
|
|
|
|
int |
|
nBlockUse |
; |
|
|
|
long |
|
lRequest |
; |
|
|
|
unsigned char |
gap[nNoMansLandSize] |
; |
|
|
|
|
/* после чего располагаются: |
|
|
|
|
|
|
* |
unsigned char |
data[nDataSize]; |
|
|
|
|
* |
unsigned char |
anotherGap[nNoMansLandSize]; |
|
|
|
|
*/ |
|
|
|
|
|
|
} _CrtMemBlockHeader; |
|
|
|
|
|
|
#define pbData(pblock) ((unsigned char *) \ |
|
|
|
||
|
|
|
((_CrtMemBlockHeader *)pblock + 1)) |
|
|
|
|
#define pHdr(pbData) (((_CrtMemBlockHeader *)pbData)71) |
|
|
|
||
|
#endif |
// _CRTDBG_INTERNALS_H |
|
|
|
|
|
|
|
|
|
|
|
Если вам удобнее работать с DBGINT.H напрямую, можете заменить определе ние структуры в файле CRTDBG_INTERNALS.H директивой #include DBGINT.H. При этом вам понадобится добавить выражение «$(VCInstallDir)VC7\CRT\SRC» в систем ную переменную среды INCLUDE и список включаемых файлов, для доступа к ко торому нужно открыть диалоговое окно Options (свойства), папку Projects (про екты) и выбрать страницу свойств VC++ Directories (каталоги VC++). Не все про граммисты устанавливают исходный код библиотеки CRT, хотя делать это следо вало бы, поэтому я решил включить определение структуры непосредственно.
_CrtMemBlockHeader также позволяет извлечь более подробную информацию из структур _CrtMemState, заполняемых функцией _CrtMemCheckpoint, потому что пер вый элемент в _CrtMemState — указатель на _CrtMemBlockHeader. Надеюсь, в следую щую версию библиотеки DCRT войдут настоящие функции доступа к информа ции о блоке памяти.
Просматривая исходный код в файле MEMDUMPERVALIDATOR.CPP из проекта BUGSLAYERUTIL.DLL (он находится на CD), вы заметите, что для внутреннего управ ления памятью я использовал простые API функции семейства HeapCreate. Я сде лал это, поскольку функции создания дампа и функции ловушки, применяемые для работы с библиотекой DCRT, были бы реентерабельными при использовании функций стандартной библиотеки. Заметьте, что я не имею в виду многопоточ ную реентерабельность. Если бы моя ловушка выделяла память при помощи malloc, она была бы реентерабельной, потому что ловушки вызываются при каждом вы делении памяти.
Инициализация и завершение в программах C++
Завершив реализацию MemDumperValidator и начав его тестирование, я с удов летворением отметил, что все работает так, как было запланировано. Однако при рассмотрении всех способов, которыми программа может выделять память в куче, я покрылся холодным потом. При выделении памяти статическими конструкто рами могли возникнуть проблемы. Взглянув на первоначальный код MemDumper Validator, я обнаружил серьезный пробел в своей логике.

634 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
Иногда, хоть и не очень часто, память выделяется до достижения точки входа
впрограмму. Поэтому мне нужно было гарантировать, что нужные флаги устанав ливаются функцией _CrtSetDbgFlag до любого выделения памяти.
Работая над MemDumperValidator, я ни в коем случае не хотел, чтобы вы вы зывали до использования библиотеки некоторую функцию инициализации. Нам хватает проблем со структурами BSMDVINFO при программировании на C. Я хотел сделать MemDumperValidator как можно более автоматизированным, чтобы рабо тать с ним было удобно большинству программистов.
Кмоей радости, замешательство не продлилось слишком долго, потому что я вспомнил о директиве #pragma init_seg, благодаря которой можно управлять по рядком инициализации и уничтожения статических значений. Эта директива может принимать значения compiler, lib, user, section name и funcname. Важными являются первые три.
Значение compiler зарезервировано для компиляторов Microsoft; все объекты этой группы создаются первыми, а уничтожаются последними. Объекты, отмечен ные как lib, создаются во вторую очередь, а уничтожаются предпоследними. На конец, отмеченные как user создаются последними, а уничтожаются первыми.
Так как код MemDumperValidator должен инициализироваться до вашего кода, я мог просто указать lib в директиве #pragma init_seg и покончить со всем этим. Однако при создании своих библиотек вы также отмечаете их как сегменты lib (и правильно), поэтому мне нужен был другой способ инициализации своего кода
впервую очередь. Чтобы справиться с этим непредвиденным обстоятельством, я указываю в директиве #pragma init_seg значение compiler. Хотя при инициализа ции сегментов всегда нужно следовать правилам, применение в отладочном коде значения compiler вполне безопасно.
Описанная идея инициализации работает только в коде C++, поэтому в Mem DumperValidator входит специальный статический класс AutoMatic, который про сто вызывает функцию _CrtSetDbgFlag. Я вынужден был пойти на все это, так как это единственный способ установки флагов DCRT до инициализации любых других библиотек. Кроме того, как вы увидите ниже, для преодоления некоторых огра ничений проверки утечек памяти, свойственнных библиотеке DCRT, я должен был реализовать кое какие специфические действия в деструкторе класса. Пусть Mem DumperValidator имеет интерфейс C, но я все равно воспользовался преимущества ми C++ для инициализации этого расширения и своевременного приведения его
врабочее состояние.
И куда же подевались все сообщения об утечках памяти?
Наконец, я справился со всеми проблемами инициализации и заставил MemDum perValidator работать. Я был доволен всем за одним исключением: когда програм ма, вызывавшая утечку памяти, завершала работу, я не видел красиво отформати рованных данных, выводимых моими функциями записи дампов. Вместо этого отображались стандартные старые дампы библиотеки DCRT. Я отследил «пропав шие» отчеты об утечках памяти и с удивлением обнаружил, что функции завер шения библиотеки DCRT вызывали _CrtSetDumpClient с параметром NULL, аннули руя, таким образом, перед вызовом _CrtDumpMemoryLeaks мою ловушку записи дам
ГЛАВА 17 Стандартная отладочная библиотека C и управление памятью |
635 |
|
|
пов. Я огорчился, но скоро понял, что завершающую проверку утечек памяти я дол жен был выполнять сам. Подходящее место для этого у меня уже имелось.
Выше я говорил, что для инициализации класса AutoMatic до вашего кода и вызова его деструктора после вашего кода я использовал директиву #pragma init_seg(com7 piler), поэтому мне нужно было просто проверить в деструкторе утечку памяти и отключить после этого флаг _CRTDBG_LEAK_CHECK_DF, чтобы библиотека DCRT не генерировала собственный отчет. Этот подход имеет единственный недостаток: при компоновке программы с ключом /NODEFAULTLIB вы должны гарантировать, что выбранная вами библиотека CRT компонуется раньше BUGSLAYERUTIL.LIB. Биб лиотеки CRT не подчиняются директиве #pragma init_seg(compiler), вследствие чего нет никакой гарантии, что данные BUGSLAYERUTIL.LIB будут инициализировать ся первыми и уничтожаться последними, а значит, вы сами должны позаботиться о правильном порядке компоновки.
Очищение всех установленных ловушек записи дампа библиотекой DCRT не лишено смысла. Если бы ваша ловушка записи дампа использовала какие то функ ции CRT, такие как printf, она могла бы нарушить завершение вашей программы, потому что во время вызова _CrtDumpMemoryLeaks библиотека находится в середине процесса прекращения своей работы. Если вы следуете указанным правилам и всегда компонуете свою программу сначала с библиотекой DCRT и только потом со всеми остальными библиотеками, все будет в порядке, потому что функции MemDumperValidator отключаются до завершения работы библиотеки DCRT. Тем не менее для избежания проблем используйте в своих функциях записи дампов только макросы _RPTn и _RPTFn, потому что _CrtDumpMemoryLeaks работает только с этими макросами.
Использование MemStress
Пора добавить в вашу жизнь каплю стресса. Как хотите, но стресс может быть по лезным. Увы, подвергнуть стрессу приложения Win32 сейчас гораздо сложнее, чем раньше. Во времена 16 разрядных ОС Windows мы могли выполнять наши при ложения под управлением STRESS.EXE, полезной программы из SDK. Она позво ляла вам мучить свое приложение всеми способами, в том числе отнимать у него дисковое пространство или пространство кучи интерфейса графических устройств (GDI) и расходовать описатели файлов. Даже ее значок был великолепен: слон, идущий по канату.
Чтобы испытать приложения Win32 в стрессовых условиях, можно установить ловушку для системы выделения памяти библиотеки DCRT и управлять выделением памяти. Расширение MemStress дает вам возможность испытать выделение памя ти на языке C или C++ (написание кода расходования дискового пространства я оставил вам). Чтобы сделать MemStress простым в использовании, я написал при помощи Windows Forms интерфейсную часть, позволяющую точно указать усло вия, в которых вы хотели бы проверить свою программу.
Расширение MemStress позволяет форсировать неудачи выделения памяти, опираясь на различные критерии: для всех выделений памяти, при каждом n ом выделении памяти, после выделения x байт, при запросе более y байт, для всех выделений памяти из исходного файла и из конкретной строки исходного фай ла. Кроме того, вы можете указать расширению MemStress выводить при каждом

636 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
выделении памяти окно, спрашивающее, хотите ли вы, чтобы это конкретное выделение памяти завершилось неудачей, а также установить флаги библиотеки DCRT, влияющие на выполнение вашего приложения. На CD находится програм ма MemStressDemo; этот написанный при помощи MFC пример позволяет экспе риментировать с параметрами пользовательского интерфейса MemStress и уви деть соответствующие результаты, а еще выполняет функцию блочного теста для MemStress.
Использовать расширение MemStress относительно просто. Для этого вы дол жны включить в свою программу файл BUGSLAYERUTIL.H и вызвать макрос MEMST7 RESSINIT, передав ему имя своей программы. Для прекращения работы ловушки выделения памяти служит макрос MEMSTRESSTERMINATE. Вы можете подключать и останавливать ловушку при работе своей программы сколько угодно.
После компиляции своей программы запустите пользовательский интерфейс MemStress, нажмите на кнопку Add Program (добавить программу) и введите то же имя, что вы указали в макросе MEMSTRESSINIT. Выбрав условия ошибки, нажмите на кнопку Save Program Settings (сохранение настроек программы) для сохранения конфигурации в файле MEMSTRESS.INI. После этого вы можете запускать свою программу и изучать ее поведение при неудачном выделении памяти.
К расширению MemStress следует относиться очень избирательно. Так, указав, чтобы неудачей завершались все выделения блоков памяти, превышающих 100 байт, и включив макрос MEMSTRESSINIT в функцию InitInstance своего приложения MFC, вы скорее всего нарушите работу MFC, так как она не сможет инициализировать ся. Самые лучшие результаты вы получите, если ограничите работу MemStress клю чевыми областями своего кода, чтобы их можно было протестировать по отдель ности.
Основная часть реализации MemStress касается чтения и обработки файла MEMSTRESS.INI, в котором хранятся все параметры для отдельных программ. С точки зрения библиотеки DCRT, особую важность представляет вызов функции _CrtSetAllocHook при инициализации MemStress, потому что он устанавливает фук нцию ловушку выделения памяти AllocationHook. Если ловушка выделения памяти возвращает TRUE, обработка запроса на выделение памяти может продолжаться. Возвращая FALSE, ловушка выделения памяти может заставить библиотеку DCRT провалить запрос на выделение памяти. Библиотека DCRT предъявляет к ловушке выделения памяти только одно строгое требование: если тип блока, определяе мый параметром nBlockUse, имеет значение _CRT_BLOCK, функция ловушка должна возвращать TRUE, чтобы выделение могло увенчаться успехом.
Ловушка выделения памяти получает управление при вызове любого типа фун кции выделения памяти. Эти типы, передаваемые ловушке в первом ее парамет ре, могут иметь значения _HOOK_ALLOC, _HOOK_REALLOC и _HOOK_FREE. Если моя ловушка AllocationHook получает тип _HOOK_FREE, я пропускаю весь код, определяющий ус пешную или неудачную обработку запроса на выделение памяти. При получении типов _HOOK_ALLOC и _HOOK_REALLOC моя функция AllocationHook выполняет ряд опе раторов if, определяя, выполняется ли какое нибудь из условий неудачи. Если хотя бы одно из условий выполняется, я возвращаю FALSE.
ГЛАВА 17 Стандартная отладочная библиотека C и управление памятью |
637 |
|
|
Интересные проблемы с MemStress
При тестировании MemStress на консольном примере все работало отлично, и я был очень доволен. Однако, закончив работу над программой MemStressDemo, основанной на MFC, я столкнулся с одной странной проблемой. Если я приказы вал MemStress спрашивать меня, хочу ли я, чтобы выделение памяти провалилось, я слышал несколько звуковых сигналов, и MemStressDemo прекращала свою ра боту. Ошибка воспроизводилась при каждом запуске программы, но я никак не мог найти ее причин, что стало меня не на шутку раздражать.
После нескольких запусков я, наконец, получил информационное окно, одна ко оно находилось не в центре экрана, а в правом нижнем углу. Когда окна появ ляются в правом нижнем углу экрана, вы можете быть почти уверены в том, что столкнулись с ситуацией, в которой вызов API функции MessageBox почему то стал реентерабельным. Я предположил, что где то в середине MessageBox вызывалась моя ловушка выделения памяти. Для проверки этой гипотезы я установил точку пре рывания на первой команде AllocationHook и «перешагнул» (step over) через вы зов MessageBox. Все подтвердилось: отладчик остановился на точке прерывания.
Яприступил к изучению стека и увидел, что вызов API функции MessageBox почему то проходил через MFC. Пробираясь через код и наблюдая за тем, что происходит, я попал в функцию _AfxActivationWndProc на строку, вызывавшую CWnd::FromHandle. Этот вызов приводил к выделению памяти, нужной для того, чтобы MFC могла создать CObject. Я был слегка удивлен тем, как я там оказался, однако комментарии в коде гласили, что _AfxActivationWndProc служит для обработки ак тивизации диалоговых окон и их закрашивания в серый цвет. MFC использует ловушку приложений компьютерной профессиональной подготовки (computer based training (CBT) hook) для перехвата создания окон в адресном пространстве процесса. При создании нового окна — в моем случае простого информацион ного окна (message box) — MFC создает подкласс окна с его собственной окон ной процедурой.
Когда я понял суть проблемы, то пришел в еще большее замешательство, по тому что не знал, как с ней справиться. Поскольку реентерабельность имела мес то в одном потоке, я не мог использовать объект синхронизации, такой как сема фор, потому что это привело бы к блокировке потока. Поразмыслив, я решил, что мне нужен флаг рекурсии, указывающий на реентерабельность AllocationHook, но он должен быть отдельным для каждого потока. У меня уже была критическая сек ция, защищающая AllocationHook от многопоточной реентерабельности.
Сформулировав проблему таким образом, я понял, что мне нужна только пе ременная в локальной памяти потока, которую я проверял бы в начале AllocationHook. Если бы ее значение превышало 0, это указывало бы на реентерабельность Alloca7 tionHook во время обработки MessageBox, и в этом случае я должен был бы немед ленно покидать функцию. Я быстро реализовал динамичное решение на основе локальной памяти потока, и уровень моего беспокойства значительно снизился, так как все начало работать так, как я и планировал.
Ядумал, что теперь все будет в порядке, но не тут то было. При тестировании кода, вызывавшего неудачу выделения памяти для конкретного файла и строки, имя исходного файла имело значение NULL, а номер строки — 0. Я писал программу MemStressDemo при помощи MFC и полагал, что она будет правильно использо

638 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
вать функции выделения памяти CRTDBG.H для передачи в них исходного файла
иномера строки. Увы, это было не так.
Японял, что в начале STDAFX.H нужно указать определение _CRTDBG_MAP_ALLOC, а в конце включить файл CRTDBG.H. Конечно, стоило мне скомпилировать такой вариант программы, число ошибок, указывавших на переопределения и неожи даные константы, меня просто сразило. Как я только ни пытался заставить при ложение MFC скомпилироваться и использовать правильные версии функций выделения памяти! Спустя некоторое время я обнаружил, что единственный при емлемый вариант решения этой проблемы состоял в извлечении определений из CRTDBG.H и включении их в файл STDAFX.H вручную. Чтобы в точности узнать, что именно вам нужно делать, можете изучить файл STDAFX.H программы Mem StressDemo.
Кучи операционной системы
Возможно, вы думали, что я уже рассказал про систему куч все, однако есть еще один набор куч, про который вам непременно следует знать, — это кучи ОС. При запуске приложения под управлением отладчика Windows включает проверку кучи ОС. Это не отладочная куча стандартной библиотеки C — это куча Windows для тех куч, что создаются при помощи API функции HeapCreate. Куча стандартной биб лиотеки C — отдельная сущность. Кучи ОС интенсивно используются процесса ми, например, для преобразования строк ANSI в формат Unicode при работе с функциями ANSI, поэтому вы можете видеть информацию о кучах ОС при нор мальных операциях, вот почему их так важно рассмотреть. Если вы подключаете отладчик к своему приложению позднее, а не сразу начинаете выполнение про граммы под отладчиком, вы не активизируете проверку куч ОС. Очень часто меня спрашивают: «Вне отладчика моя программа работает отлично, но в отладчике она вызывает исключение пользовательской точки прерывания. Почему моя программа не работает?» Ответ: из за проверки куч ОС.
При включенной проверке куч ОС ваше приложение будет работать медлен нее, потому что при вызове функций работы с кучей ОС будет выполнять соот ветствующую проверку. В число примеров к этой книге я включил программу Heaper (листинг 17 5), повреждающую кучу. Запустив Heaper в отладчике, вы увидите, что она дважды вызывает DebugBreak на первой функции HeapFree. Будет также выведе на информация об ошибке, пример которой приведен ниже. Да, вывод останав ливается на буквах «of a» и не отображает размер блока, что могло бы быть весь ма полезным. Если вы запустите эту программу вне отладчика, она проработает до своего завершения безо всяких проблем.
HEAP[Heaper.exe]: Heap block at 00311E98 modified at 00311EAA past
requested size of a
Листинг 17-5. HEAPER.CPP — пример повреждения кучи Windows
void main(void)
{
// Создание кучи операционной системы.
HANDLE hHeap = HeapCreate ( 0 , 128 , 0 ) ;

ГЛАВА 17 Стандартная отладочная библиотека C и управление памятью |
639 |
|
|
//Выделение памяти для 107байтового блока. LPVOID pMem = HeapAlloc ( hHeap , 0 , 10 ) ;
//Запись 12 байт в 107байтовый блок (запись после конца блока). memset ( pMem , 0xAC , 12 ) ;
//Выделение нового 207байтового блока.
LPVOID pMem2 = HeapAlloc ( hHeap , 0 , 20 ) ;
//Запись данных на 1 байт до начала второго блока. char * pUnder = (char *)( (DWORD_PTR)pMem2 7 1 ); *pUnder = 'P' ;
//Освобождение первого блока. Этот вызов HeapFree приведет
//к срабатыванию точки прерывания в отладочном коде кучи ОС. HeapFree ( hHeap , 0 , pMem ) ;
//Освобождение второго блока. Заметьте: этот
//вызов не приводит к сообщениям о проблеме.
HeapFree ( hHeap , 0 , pMem2 ) ;
//Освобождение несуществующего блока.
//Этот вызов также не приводит к проблемам. HeapFree ( hHeap , 0 , (LPVOID)0x1 ) ;
HeapDestroy ( hHeap ) ;
}
Если вы используете собственные кучи ОС или хотите, чтобы приложение включило проверку куч ОС при выполнении вне отладчика, вы можете установить дополнительные флаги для получения более подробного диагностического вывода. Небольшая утилита GFLAGS.EXE из пакета Debugging Tools for Windows поможет установить некоторые глобальные флаги, которые Windows проверяет при пер вом запуске приложения. На рис. 17 1 показаны параметры GFLAGS.EXE для HEA PER.EXE, программы из листинга 17 5. Многие из параметров System Registry (си стемный реестр) и Kernel Mode (режим ядра) глобальны, поэтому вам нужно быть чрезвычайно внимательным при их изменении, так как это может оказать значи тельное влияние на производительность системы или вообще нарушить ее рабо ту. Изменять параметры Image File Options (настройки файла образа), показанные на рис. 17 1, гораздо безопасней, потому что они ограничены только одним ис полняемым файлом. Кстати, несмотря на всю полезность GFLAGS.EXE, для проверки повреждений кучи можно использовать и средство Application Verifier, о котором я расскажу ниже.
Наконец, чтобы завершить рассказ про GFLAGS.EXE, я хочу обратить ваше вни мание на один очень полезный параметр Show Loader Snaps (показывать снимки загрузчика). Если вы отметите этот флажок и запустите свою программу, вы уви дите, где Windows загружает DLL и как она собирается настраивать импортируе

640 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
мые функции для вашего приложения, что называется деланием снимков (snapping). Если вам нужно точно узнать, что делает загрузчик Windows при загрузке прило жения (если у вас есть проблема), этот флажок следует отключить. Подробне о снимках загрузчика см. раздел «Under the Hood» Мэтта Питрека (Matt Pietrek) в «Microsoft Systems Journal» (1999, сентябрь).
Рис. 17 1. Параметры GFLAGS.EXE для программы HEAPER.EXE
Советы по отслеживанию проблем с памятью
Теперь вы должны хорошо понимать доступные вам отладочные системы памя ти, поэтому я могу перейти к методам нахождения тех проблем с памятью, кото рые случаются только в готовых системах и не желают показываться в отладчике.
Обнаружение записи в неинициализированную память
Нет ничего хуже ошибки, которая происходит из ниоткуда и не соответствует никакой ветви выполнения кода. Если вы попали в такую ситуацию, она объясня ется скорее всего записью в неинициализированную память, также известной как запись по случайному адресу (wild write). Причина таких ошибок таится в неини циализированном указателе, который по воле случая указывает на корректную область памяти. Обычно это происходит со стековыми указателями или, иначе говоря, с локальными переменными. Так как при выполнении вашей программы стек постоянно изменяется, ничто не сможет сказать вам, куда указывает неини циализированный указатель, вот почему он может казаться случайным.
Когда нас просят помочь решить подобную коварную проблему, мы всегда встречаем совершенно растерянных программистов, утверждающих, что попро бовали буквально все способы ее обнаружения. Так как они уже проделали «все», они отчаянно желают узнать, что мы наколдуем. Я отвечаю на это, что мы соби раемся применить запатентованный и зарегистрированный Магический Способ
ГЛАВА 17 Стандартная отладочная библиотека C и управление памятью |
641 |
|
|
Отладки Неинициализированной Памяти по Методике Ниндзя (MNUMDT). Все собираются посмотреть на это и ерзают в креслах, ожидая чуда, особенно когда я упоминаю, что MNUMDT сработает, только если мне помогут двое наименее опыт ных разработчиков группы.
В этот момент ко мне подходят двое младших программистов, всем своим видом показывающие, что намерены с достоинством нести свою ношу, и я описываю им MNUMDT:
каждый из нас берет на себя по одной трети кода;
каждый должен внимательно прочитать каждую строку кода, отыскивая все объявления указателей;
при нахождении объявления неинициализированного указателя мы инициа лизируем его значением NULL;
при обнаружении каждого вызова выделения памяти не для классов мы добав ляем после него вызов memset или ZeroMemory для обнуления памяти;
после любого освобождения памяти мы снова присваиваем указателю нулевое значение;
при нахождении вызова memset или операции копирования строк мы должны проверить, что каждая операция правильно подсчитывает размер блока памяти;
каждая переменная члена класса должна инициализироваться в конструкто ре(ах).
Выслушивая правила MNUMDT, парни готовы выбежать из комнаты, но я не даю им сделать этого. Если вы думаете, что это грубое насилие, вы абсолютно правы: так и есть.
Я имел дело с тысячами ошибок, вызванных неинициализированными указа телями, и знаю, что тут не поможет никакая отладка. Вы просто потратите время. Отладка станет гораздо эффективнее, если перед ее началом выполнить только что описанные мной действия. Очень вероятно, что проблему вызывает один из указателей, которые вы проинициализируете. После этого программа не сможет исказить память и продолжить свое выполнение, а тут же потерпит крах, пытаясь записать данные по указателю NULL.
Некоторые думают, что это не сработает, но я могу привести сотни случаев, когда программисты неделями пытались отыскать проблему и не получали ника ких результатов. Когда они обращались к нам, мы находили проблему за день или два. Иногда разработчики пытаются перехитрить сами себя, не желая прибегать к такому грубому подходу, однако он просто великолепен в подобных ситуациях. Почему я прибегаю к помощи наименее опытных программистов? Да потому, что они менее высокомерны, чем старшие разработчики, и, находясь перед лицом всех своих коллег, чрезвычайно ответственно подходят к возложенным на них задачам.
Нахождение записи данных после окончания блока
Запись данных после окончания блока памяти следует искать не только при по мощи DCRT и таких средств, как BoundsChecker компании Compuware, но и при тестировании программы. Увы, не все программисты относятся к тестированию так же серьезно, как вы, поэтому вы все равно будете сталкиваться в их коде с этими мерзкими ошибками. Кроме того, некоторые случаи записи данных по оконча

642 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
нии блока происходят только в наиболее напряженных условиях. Ситуация ухуд шается еще и тем, что, когда такая ошибка происходит в заключительной компо новке, в конце блока памяти нет пустого пространства, что может привести к коварному искажению данных, которое неделями может оставаться незамечен ным, пока не потерпит крах важное серверное приложение.
Один из недостатков проверки записи данных после блока памяти в DCRT в том, что о них сообщается, только когда операция с памятью выполнит соответ ствующую проверку. Было бы лучше, если б программа, допустившая такую ошибку, тут же прекращала свою работу. Программисты Microsoft также желали получить эту возможность, поэтому несколько лет назад они выпустили средство PageHeap.
PageHeap тесно взаимодействует с ОС и использует уникальный трюк для не медленного обнаружения записи после выделенного блока. Когда вы выделяете 16 байт, программа PageHeap на самом деле выделяет 8 кб! Сначала она выделяет 4 кбайтную страницу, наименьший блок памяти, к которому применяются права доступа. PageHeap предоставляет права на чтение этой страницы и ее запись. Сразу за страницей для чтения/записи PageHeap выделяет следующую страницу, отме чая ее как не имеющую доступа. PageHeap выполняет некоторые манипуляции с указателями и предоставляет вашей программе адрес, находящийся за 16 байт до конца первой страницы. Таким образом, когда вы попытаетесь записать данные в 17 ый байт, начиная от этого адреса, вы попадете на страницу с запрещенным доступом и немедленно вызовете нарушение доступа. Формат памяти, выделяемой PageHeap, показан на рис. 17 2.
|
Возвращаемый |
|
Память для чтения/записи |
16-байтный |
Память, не имеющая доступа |
|
блок |
|
|
|
|
|
|
|
0x00310000 |
0x00310FF0 |
0x00311000 |
Рис. 17 2. Память, выделяемая программой PageHeap
Как вы можете представить, PageHeap использует гораздо больше памяти, чем нужно. Однако эта цена не имеет значения, если вы сможете найти запись дан ных вне блока. Если вы работаете над крупным приложением, установите в тес товый компьютер как можно больше памяти. Можете «позаимствовать» пару мо дулей из компьютера своего начальника — все равно он этого не заметит.
Рассказывая о PageHeap, я обязательно должен упомянуть, что все возвращен ные вам указатели выравниваются по границе 16 байт. Это значит, что если вы выделяете 10 байт, то для попадания на страницу с запрещенным доступом вы должны будете записать в память 7 дополнительных байт. Пусть это вас не сму щает: при выходе за пределы блока памяти обычно выполняется запись не 1–2 байт, а довольно приличного их числа, поэтому на эффективности PageHeap это не скажется. Это также значит, что PageHeap заслуживает внимания, только когда вы работаете с заключительной компоновкой своей программы. Отладочная ком поновка сама по себе включает дополнительное пространство до и после выде ляемых блоков памяти, поэтому в таких случаях PageHeap ничего не обнаружит.