Отладка приложений

         

Глубокие проверки корректности


Та часть расширения MemDumperValidator, которая имеет дело с выдачей дампов, бесспорно полезна, но можно задать вопрос: "Зачем вообще нужен метод проверки корректности, даже если он позволяет осуществлять "глубокую" проверку блока памяти?" Если класс содержит всего пару строчных переменных, то во многих случаях функция проверки корректности может быть "пустой". Но даже в этом случае такая функция может оказаться бесценной для разработчика, потому что она обеспечивает его превосходными отладочными возможностями. Одной из целей, которой я стремился достичь, начиная пользоваться глубокой проверкой корректности памяти, было обеспечение второго уровня проверки корректности данных на наборе разработанных мной базовых классов. Функция проверки корректности не должна заменять обычных проверок параметров и вводимых данных, но может повысить степень уверенности в правильности этих данных. Глубокая проверка корректности может также быть второй линией защиты против "непредсказуемых" (wild) записей.

Лучше всего применять функции проверки корректности для двойной проверки сложных структур данных после того, как на них были выполнены некоторые операции. Например, однажды я попал в довольно сложную ситуацию, когда две отдельные рекурсивные структуры данных (self-referential data structures) использовали (по соображениям экономии памяти) одни и те же распределенные объекты. Заполнив эти структуры большими наборами данных, я с помощью функции проверки корректности просматривал индивидуальные блоки кучи и проверял корректность ссылок. Можно было написать обстоятельную программу для просмотра каждой структуры данных, но я знал, что любая такая программа стала бы новым рассадником ошибок. А функция проверки корректности позволила "проскакивать" через распределенные блоки, используя уже протестированный код, и проверять структуры данных, начиная с различных позиций, потому что память была выстроена в порядке распределения, а не в отсортированном порядке.
Операции распределения памяти языка С более сложны, чем в языке C++, но функции проверки корректности памяти в обоих языках используются одинаково. Все что нужно делать — это вызывать макрос VALIDATEALLBLOCKS. В отладочных построениях этот макрос расширяется до вызова подпрограммы vaiidateAHBiocks. В качестве параметра она использует любое значение, которое вы хотите передать функциям проверки корректности, зарегистрированным вместе с библиотекой. Раньше при помощи этого параметра я определял глубину проверок корректности, выполняемых функцией. Имейте в виду, что validateAHBlocks пересылает данное значение каждой зарегистрированной подпрограмме проверки корректности, чтобы можно было координировать эти значения в команде разработчиков.

Чтобы понять, как работают функции расширения MemDumperValidator, просмотрите программу с именем Dump, показанную в листинге 15-2. Dump — это "пустая", незаполненная программа, в которой показано все,, что нужно для использования этого расширения. Я не привожу пример кода, но MemDumperValidator хорошо работает и с MFC, потому что MFC будет вызывать любые предварительно зарегистрированные функции подключения клиентских дампов.

Листинг 15-2. DUMP.CPP 

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

"Debugging Applications" (Microsoft Press)

Copyright (c) 1997-2000 John Robbins — All rights reserved.

- - - - - - - - - - - - - - - - - - - - - - - - - -*/

#include <stdio.h>

#include <stdlib.h>




#include <memory.h> 

#include <string.h> 

#include <iostream.h>

#include "BugslayerUtil.h"

class TestClass

{

public:

TestClass ( void)

{

strcpy ( m_szData, "TestClass constructor data!");

}

TestClass ( void)

{

m_szData[ 0 ] = '\0';

}

// Объявление средств отладки памяти для классов языка C++

DECLARE_MEMDEBUG ( TestClass); 

private :

char m_szData[ 100 ]; 

};

// Этот макрос устанавливает статическую структуру DVINFO .



 IMPLEMENT_MEMDEBUG ( TestClass);

Listing 15-2DUMP.CPP *

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

 #ifdef _DEBUG

void TestClass::ClassDumper ( const void * pData)

 {

TestClass * pClass = (TestClass*JpData; 

_RPT1 ( _CRT_WARN,

" TestClass::ClassDumper : %s\n", pClass->m_szData);

 }

 void TestClass::ClassValidator ( const void * pData ,

const void * )

 {

// Проверка корректности данных.

 TestClass * pClass = (TestClass*)pData;

 _RPT1 ( _CRT_WARN ,

 " TestClass::ClassValidator : %s\n",

 pClass->m_szData ); 

}

#endif

typedef struct tag_SimpleStruct 

{

char szNamef 256 ]; char szRank[ 256 ]; 



SimpleStruct;

// Методы выдачи дампов и проверки корректности для памяти, 

// содержащей простые строчные данные 

void DumperOne ( const void * pData} 

{

_RPT1 ( _CRT_WARN, " Data is : %s\n", pData); 

}

void ValidatorOne ( const void * pData, const void * pContext) 



 // Проверка корректности строчных данных

. _RPT2 ( _CRT_WARN,

" Validator called with : %s : Ox%08X\n",

  pData, pContext); 

}

// Методы вьщачи дампов и проверки корректности для структур

 void DumperTwo ( const void * pData)

 {

_RPT2 ( _CRT_WARN

" Data is Name : %s\n"

 " Rank : %s\n" ,

  ((SimpleStruct*)pData)->szName , 

((SimpleStruct*)pData)->szRank ); 

}

void ValidatorTwo ( const void * pData, const void * pContext)

 {

// Проверка корректности структур.

_RPT2 ( _CRT_WARN , " Val%dator called with :\n" 

" Data is Name : %s\n"

 " Rank : %s\n" , 

((SimpleStruct*)pData)->szName ,

  ((SimpleStruct*)pData)->szRank ); 

}

// К сожалению, функции языка С используют собственные структуры

 // DVINFO. Эти структуры необходимо определять как внешние ссылки, а 



// для макроса MEMDEBUG нужно создавать собственный макрос-оболочку,

 static DVINFO g_dvOne; 

static DVINFO g_dvTwo; 

void main ( void) 

{

cout « "At start of main\n";

// Инициализация отладки памяти для типа One.

INITIALIZE_MEMDEBUG ( &g_dvOne, DumperOne, ValidatorOne) ;

// Инициализация отладки памяти для типа Two.

INITIALIZE_MEMDEBUG ( Sg_dvTwo, DumperTwo, ValidatorTwo) ;

// Распределить память для класса с новым MEMDEBUG.

TestClass * pstClass;

//pstClass = MEMDEBUG_NEW TestClass;

pstClass = new TestClass;

// Распределить память для двух типов языка С.

char * р = (char*)MEMDEBUG_MALLOC '( &g_dvOne, 10);

strcpy ( р, "VC VC");

SimpleStruct * pSt =

(SimpleStruct*)MEMDEBUG_MALLOC ( Sg_dvTwo,

sizeof ( SimpleStruct));

strcpy ( pSt->szName, "Pam");

strcpy ( pSt->szRank, "CINC");

// Проверить корректность всех блоков в списке.

VALIDATEALLBLOCKS ( NULL);

cout « "At end of main\n";

// Дамп каждого блока будет выведен как часть проверки утечки памяти

 }



Инициализация и завершение в C++


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

Хотя большинство разработчиков не очень часто используют этот прием, в некоторых случаях память распределяется перед точкой входа приложения.

При моем подходе к разработке MemDumperValidator проблема состояла в следующем: нужно было гарантировать, что установки подходящих флажков для функции _CrtsetDbgFiag будут выполняться прежде, чем произойдут какие-либо распределения памяти.

И, наконец, последний момент, который надо подчеркнуть в связи с расширением MemDumperValidator: перед началом использования DCRT-биб-лиотеки не забывайте вызывать какую-нибудь, хотя бы самую простую, функцию инициализации. В С-программах не очень удобно иметь дело со структурами DVINFO. Я хотел сделать MemDumperValidator настолько автоматическим, насколько это возможно, чтобы применение его разработчиками не вызывало каких-либо затруднений.

К счастью, существует директива #pragma init_seg, позволяющая управлять инициализацией и порядком ликвидации статически объявленных значений. Директиве #pragma init_seg можно передать один из следующих параметров, которые, no-существу, специфицируют тип инициализируемого значения: compiler, lib, user, section name И funcname. Наиболее важными ЯВЛЯЮТСЯ три первых параметра.

Параметр compiler зарезервирован для компилятора Microsoft. Любые объекты, маркированные меткой compiler, создаются первыми, а разрушаются последними. Объекты, маркированные как lib, создаются после и разрушаются перед объектами compiler, а объекты, маркированные меткой user, строятся последними, а разрушаются первыми.

 Напомним, что в языке C++ созданием и разрушением объектов в динамической памяти занимаются специальные методы — конструкторы и деструкторы.
Первые занимаются выделением памяти и инициализацией объекта, а вторые — освобождением выделенной объекту памяти, т. е. его ликвидацией, разрушением. — Пер

Поскольку код расширения MemDumperValidator должен быть инициализирован перед тем, как будет инициализирован ваш код, можно просто передать lib как параметр директиве #pragma init_seg и на этом закончить работу. Однако если вы создаете библиотеки и маркируете их в виде lib-сегментов (как и должно быть) и хотите использовать мой код, то его нужно инициализировать перед инициализацией вашего кода. Для этого данной директиве надо передать параметр: #pragma init_seg (compiler). Хотя нужно всегда следовать правилам, приводящим к правильной инициализации кодовых сегментов, применение параметра compiler в отладочном коде оказывается достаточно безопасным занятием.

Поскольку идеи инициализации работают только в кодах языка C++, в MemDumperValidator включен специальный статический класс (с именем AutoMatic), который просто вызывает функцию _CrtSetDbgFiag. Приходится идти на все эти ухищрения только потому, что это единственный способ установить DCRT-флажки перед инициализацией других библиотек. Кроме того, чтобы обойти некоторые ограничения при проверке утечек памяти в DCRT-библиотеке, необходимо выполнять некоторую специальную обработку при разрушении объектов соответствующего класса. Даже если бы MemDumperValidator имел только С-интерфейс, то всегда можно было бы использовать преимущества языка C++ для его установки и запуска, чтобы он всегда был готов для вызова.



Интересная "стрессовая" проблема


Первоначально, при работе с примером небольшой консольной программы, MemStress вела себя великолепно. Однако, завершая работу над программой MemStressDemo (которая использует библиотеку классов MFC), я обратил внимание на следующую проблему. Если в режиме диалога с MemStress пользователь (через панель сообщений) требовал, чтобы распределения терпели неудачу, то раздавался ряд звуковых сигналов, и программа MemStressDemo переставала работать. Поскольку ситуация устойчиво воспроизводилась (иначе говоря, я имел возможность дублировать эту проблему), то серьезный стресс возник уже у меня самого, потому что я никак не мог уловить, в чем суть этой проблемы.

После нескольких прогонов, наконец, удалось вновь открыть панель сообщения. Но, вместо того чтобы находиться в центре экрана, она открылась в его нижнем правом углу. Такое поведение панелей сообщений позволяет с достаточной долей уверенности считать, что вызов API-функции MessageBox каким-то образом стал реентерабельным (повторно входимым). Мне показалось, что подключение распределения происходило где-то в середине вызова функции MessageBox. Для проверки гипотезы я установил точку прерывания на первой инструкции функции AiiocationHook, как раз перед вызовом функции MessageBox. Отладчик, как и следовало ожидать, остановился на точке прерывания.

Просмотр стека показал, что прямое обращение к API-функции MessageBox почему-то проходило через код MFC. Трассировка этого кода привела меня внутрь функции _AfxActivationWndProc в строку, которая вызывала метод CWND::FromHandie, выполняющий распределения памяти для того, чтобы MFC могла создавать объекты типа (класса) cobject. Я был немного озадачен — как я там оказался? Но комментарий в коде указал, что функция _AfxActivationWndProc используется для управления активизацией и создает "серые" (неактивные) диалоговые панели. MFC использует СВТ-подключение, чтобы перехватывать создание окна в пространстве процесса. Когда новое окно создается (в моем случае это простая панель сообщения — message box), MFC создает подчиненное окно со своей собственной оконной процедурой.


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

Такая формулировка проблемы позволила понять, что нужна только переменная в локальной памяти потока, обеспечивающая доступ к функции AiiocationHook. Если ее значение больше 0, значит функция AiiocationHook была повторно вызвана в процессе выполнения функции MessageBox, и нужно только освободить эту функцию. Я реализовал специальное решение, и все заработало так, как и было запланировано.

  СВТ — Computer-Based Training, машинное обучение. — Пер.

Автор называет это решение "quick dynamic thread local storage solution" быстрым решением для динамической локальной памяти потока. — Пер.



Использование DCRT-библиотеки


Первым шагом к применению DCRT-библиотеки является включение ее в проект, что позволяет немедленно начать извлекать выгоду из мониторинга памяти. Для этого перед директивами #inciude главного файла проекта (или любого другого файла вашего проекта) необходимо добавить следующую строку:

#define _CRTDBG_MAP_ALLOC

Потом к имеющимся в программе заголовочным файлам следует добавить файл CRTDBG.H. Определение _CRTDBG_MAP_ALLOC будет переадресовывать обращения к обычным функциям распределения и освобождения памяти к специальным версиям этих функций, которые будут записывать в отчет имя и номер строки исходного файла каждой операции распределения или освобождения памяти.

Затем необходимо включить являющийся частью DCRT-библиотеки код управления кучей. Как говорилось в начале этой главы, большинство свойств DCRT-библиотеки по умолчанию выключено. Документация утверждает, что это сделано для того, чтобы сохранять небольшой размер кода и увеличивать скорость его выполнения. Хотя размер и скорость могут быть важны для сборки релиза (release build, выпускного построения), основной целью отладочного построения (debug build) является поиск ошибок! При отладке увеличенный размер и уменьшенная скорость выполнения приложения не имеют особого значения. Поэтому без колебаний включайте все свойства, которые, по вашему мнению, могут оказаться полезными. Функция _CrtsetDbgFiag принимает набор флагов, перечисленных в табл. 15.2. Чтобы включать различные режимы библиотеки DCRT, их можно объединять друг с другом операцией ок.

После сборки приложения с указанными выше директивами #inciude и #define и вызова функции _crtsetDbgFlag обеспечен полный доступ к библиотеке DCRT, многочисленные функции которой помогут управлять использованием памяти и получать соответствующие отчеты. Эти функции можно вызывать в любой точке приложения, а многие из них использовать внутри утверждений, что позволяет отлавливать проблемы памяти вблизи источника.

Одна из наиболее полезных функций DCRT — _CrtcheckMemory.
Она просматривает всю распределенную память и проверяет, не записывались ли какие-нибудь данные в начало или в конец блока, а также не перераспределялись ли предварительно освобожденные блоки памяти. Только из-за одной этой функции применение библиотеки DCRT оправдано!

Другой набор функций позволяет проверять корректность данных любой области памяти. Функции _CrtIsValidHeapPointer, _CrtIsMemoryBlock И _crtisVaiidPointer удобно использовать в качестве отладочных параметров функций проверки корректности. Вместе с _crtcheckMemory эти функции являются превосходными средствами проверки памяти.

Таблица 15.2. Флажки библиотеки DCRT

Флажок

Описание

_CRTDBG_ALLOC_MEM_DF

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

_CRTDBG_CHECK_ALWAYS_DF

Контролирует и проверяет корректность всей памяти при каждом запросе на распределение и освобождение (памяти). Включение этого флажка отлавливает записи и перезаписи сразу же, как только они происходят

_CRTDBG_CHECK_CRT_DF

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

CRTDBG_DELAY_FREE_MEM_DF

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



(прод.)

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

_CRTDBG_LEAK_CHECK_DF

Проверять утечки памяти в конце программы. Включение этого чрезвычайно полезного флажка обязательно




Обратите внимание еще на одну полезную группу функций в DCRT — это функции  состояния памяти _CrtMemCheckpoint, _CrtMemDifference И _CrtMemDumpStatistics. Чтобы увидеть различные неполадки при работе кучи, эти функции полезно выполнять перед и после операцией сравнения областей кучи. Например, если используется обычный (не отладочный) вариант CRT-библиотеки, то можно сделать предварительные и последующие дампы кучи при вызове функций, сообщающих об утечке памяти или о размере памяти, используемой некоторой операцией.

Библиотека DCRT позволяет подключаться к потоку функций распределения и освобождения памяти, что помогает проследить каждый вызов этих функций. Если функция подключения к этому потоку возвращает значение TRUE, то распределение может продолжаться. Если же эта функция возвращает FALSE, то это означает, что был сбой в процессе распределения. Впервые обнаружив эти возможности, я подумал, что мог бы без особых усилий получить средства тестирования кода при некоторых действительно неприятных граничных условиях (которые в иной ситуации будет очень трудно дублировать). Результат можно увидеть в приложении MemStress (входящем в состав библиотеки BUGSLAYERUTIL.DLL). Эта программа, по существу, расширяет DCRT-библиотеку и позволяет форсировать отказы в процедурах распределения памяти (это приложение будет представлено в конце данной главы).

Кроме того, библиотека DCRT позволяет подключать функции дампов памяти и перечислять клиентские блоки (т. е. память, выделенную программе). Можно также заменять штатные функции дампов памяти собственными, которые "знают" все о ваших данных. Теперь, вместо просмотра загадочной выгруженной памяти (т. е. дампа), которую вы получаете по умолчанию (и которая, кроме того, что ее трудно расшифровывать, не так уж и полезна), можно получить точную информацию о содержимом блока памяти и вывести ее в удобном формате. Для этой цели в MFC имеется функция Dump, но она работает только с классами, производными от cobject. Если же вы (как и я) программируете не только в MFC, то вам нужны более общие функции дампов, приспособленные к различным типам кодов.

Свойство перечисления клиентских блоков, судя по названию, позволяет перечислять выделенные блоки памяти. Эта очень полезное свойство поможет создавать некоторые интересные утилиты. Например, в функциях MemDumperVaiidator из BUGSLAYERUTIL.DLL, я вызываю обработчики дампов из функции перечисления клиентских блоков, так что перечисление может выполнять дамп и проверку корректности многих типов распределенной памяти в одной операции. Это позволяет выполнять более глубокую проверку содержимого памяти (по сравнению с проверкой поверхности записей underwrites и overwrites). Под глубокой проверкой корректности я понимаю специальный алгоритм, которому известны форматы данных в блоке памяти и который, опираясь на знание этих форматов, гарантирует, что каждый элемент данных является корректным.



Использование MemDumperValidator


Расширение MemDumperValidator из DCRT-библиотеки значительно облегчает отладку памяти. По умолчанию DCRT-библиотека выдает сообщения об утечках памяти и проверках корректности тех блоков памяти, в которых оба типа записей (с начала — underwrites или с конца блока — overwrites) не подвергались разрушению. Оба отчета могут быть очень полезны, но если отчет об утечках памяти выглядит так, как показано ниже, то довольно трудно точно определить, в памяти какого типа произошла утечка:

Detected memory leaks

Dumping objects ->

с:\vc\INCLUDE\crtdbg.h(552) : (596} normal block at Ox008CD5BO,

24 bytes long.

Data: < k w k > 90 6B 8C 00 BO DD 8C 00 00 00 80 77 90 6В 8С 00

Object dump complete.

Как говорилось ранее, средств проверки корректности памяти, заданных по умолчанию, может оказаться недостаточно. Для отлавливания некоторых специальных видов записей в память необходима более глубокая проверка корректности (иначе вы вообще не сможете перехватывать такие записи). Дополнительные проверки корректности и информация отчетов об утечках памяти, обеспечиваемые расширением MemDumperValidator, особенно полезны во время отладки. Чем больше информации получено во время отладки, тем меньше времени она занимает.

Расширение MemDumperValidator использует в своей работе идентификаторы блоков памяти DCRT-библиотеки, что позволяет ему ассоциировать тип блока со специфическим набором функций работы с памятью, которым что-то известно о содержимом соответствующего блока. Каждому блоку памяти, распределенному через библиотеку DCRT, назначается специальный идентификатор, как показано в табл. 15.3. Типы блоков являются параметрами следующих функций распределения памяти библиотеки DCRT: _nh_maiioc_ dbg (new), _malloc_dbg (malloc), _calloc_dbg (calloc) И _realloc_dbg (realloc).

Таблица 15.3. Идентификаторы блоков памяти

Идентификатор блока Описание

_NORMAL_BLOCK

Вызов обычной функции распределения (new, malloc или calloc) создает нормальные блоки. Определение

#define _CRTDBG_MAP_ALLOC

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

_CRT_BLOCK

Блоки памяти, распределяемые многими функциями библиотеки времени выполнения, помечаются как CRT-блоки, поэтому они могут быть обработаны отдельно (иначе, чем блоки других типов). В частности, появляется возможность исключить такие блоки из процедур обнаружения утечек памяти и других операций проверок корректности. Ваше приложение никогда не должно распределять, перераспределять или освобождать блоки подобного типа

CLIENT BLOCK

Если нужно, чтобы приложение выполняло специальную трассировку блоков распределенной памяти, то можно вызывать особые отладочные функции распределения, передавая им в качестве параметра специальное значение CLIENT BLOCK VALUE (см. ниже вызов heap alloc dbg после директивы #define). Можно прослеживать подтипы клиентских блоков, помещая 1 6-разрядное значение в 16 верхних разрядов значения блока, как показано ниже:

#define CLIENT_BLOCK_VALUE(x) \

 (_CLIENT_BLOCK | (x«16) )

 heap alloc dbg ( 10, 

 CLIENT BLOCK VALUE ( OxA) , 

_ FILE _ ,

 _ LINE _ ) ;

Для дампов блоков памяти этого типа (т. е. памяти, зарегистрированной в форме клиентских блоков) приложение может обеспечить функцию-обработчик (через функцию CrtSetDumpClient). Функция-обработчик будет вызываться всякий раз, когда функции DCRT-библиотеки потребуется выполнить дамп клиент-


(прод.)

ского блока. Кроме того, имеется специальная функция (__CrtDoForAlldientObjects), которая позволяет получить список всех клиентских блоков, распределенных к моменту ее вызова. MFC использует данный идентификатор для всех классов производных от класса cobject. Расширение MemDumperValidator тоже использует описанный механизм

FREE BLOCK

Вызов подпрограммы освобождения памяти обычно удаляет память из списков отладочной кучи. Однако если при обращении к функции CrtSetDbgFlag вы устанавливаете флажок CRTDBG DELAY FREE MEM DF, то память не освобождается, а выравнивается влево и заполняется символами OxDD

IGNORE BLOCK

Если вы временно отключаете трассировку DCRT-библиотеки, то любые распределения, выполненные после этого, будут помечаться как блоки Ignore

Расширение MemDumperValidator работает следующим образом: после установки подходящего класса (в языке C++) или типов данных (в языке С), DCR-библиотека будет вызывать MemDumperValidator всякий раз, когда нужно выполнить дамп блока памяти. Расширение просматривает значения данных в блоке и если находит подходящую дамп-функцию, то вызывает ее для выполнения дампа памяти. Аналогичным образом работает и та часть расширения, которая проверяет корректность данных в блоках памяти. Процесс проверки корректности отличается от процесса выдачи дампа только в том, что DCRT-библиотека вызывает не функции дампа, а функции проверки корректности данных соответствующего блока памяти.

Описать MemDumperValidator не так уж сложно, но вот заставить его работать — немного сложнее. В листинге 15.1 показан заголовочный файл MEMDUMPERVALIDATOR.H, который выполняет основную часть работ по инициализации. Включая в программу файла BUGSLAYERUTIL.H, вы автоматически включаете и MEMDUMPERVALIDATOR.H.

Листинг 15-1, JWEMDUMPERVAL1DATOR.H 

/*- - - - - - - - - - - - - - - - - - - - - - - - - - - -

"Debugging Applications" (Microsoft Press)

Copyright (с) 1997-2000 John Robbins — All rights reserved. 



- - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

 #ifndef _MEMDUMPERVALIDATOR_H

#define _MEMDUMPERVALIDATOR_H

// He включайте этот файл напрямую; вместо него включайте BUGSLAYER.H

#ifndef _BUGSLAYERUTIL_H

#error "Include BUGSLAYERUTIL.H instead of this file directly!"

#endif // _BUGSLAYERUTIL_H

// Включить заголовочный файл CRTDBG.H.

#include "MSJDBG.h"

#ifdef _cplusplus

extern "C" {

#endif // _ _cplusplus

// Эту библиотеку можно использовать только в отладочных построениях.

#ifdef _DEBUG

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

// Директивы typedef для функций вьдачи дампов и проверки корректности

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

// Функция выдачи дампов памяти. Единственный параметр этой функции -

// указатель на блок памяти. Эта функция выводит данные блока памяти

// одним из нескольких доступных ей способов, но, для того чтобы быть

// состоятельной, она использует механизм формирования отчетов,

// которым пользуется остальная часть DCRT-библиотеки.

typedef void (*PFNMEMDUMPER)(const void *);

// Функция проверки корректности (validating function).

//Ее первый параметр — блок памяти,

// корректность которого нужно проверить, а второй — контекстная

// информация, пересылаемая в функцию ValidateAllBlocks function.

typedef void (*PFNMEMVALIDATOR)(const void *, const void *);

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

// Полезный макрос

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

// Макрос, используемый для установки значения подтипа Client-блока.

// Использование этого макроса — единственное санкционированное средство

// установки значения поля dwValue в структуре DVINFO (см. ниже).

tdefine CLIENT_BLOCK_VALUE(x) (_CLIENT_BLOCK|(x«16))

// Макрос для выбора подтипа

Idefine CLIENT_BLOCK_SUBTYPE(х) ((х » 16) & 0xFFFF)

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



// Заголовок, используемый для инициализации функций дампа и проверки

// корректности Client-блока специфического подтипа

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

typedef struct tag_DVINFO

{

// Значение подтипа Client-блоков. Это значение должно быть 

// установлено с помощью определенного выше макроса. 

// CLIENT_BLOCK_VALUE. Чтобы выяснить, как расширение назначает 

// это число, см. функцию AddClientDV.

unsigned long dwValue ; 

// Указатель на функцию дампа 

PFNMEMDUMPER pfnDump

// Указатель на функцию проверки корректности 

PFNMEMVALIDATOR pfnValidate; 

} DVINFO, * LPDVINFO;

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

ФУНКЦИЯ : AddClientDV 

ОБСУЖДЕНИЕ :

Добавляет в список функции дампа и проверки корректности

 Client-блока. Если поле dwValue в структуре DVINFO равно О, 

то назначается следующее значение из списка. Возвращаемое значение

 должно всегда пересылаться в функцию _malloc_dbg в качестве

 значения Client-блока.

Если значение подтипа устанавливается с помощью макроса

 CLIENT_BLOCK__VALUE, то его можно использовать в качестве значения,

 передаваемого в функцию _malloc_dbg.

Заметим, что соответствующей функции удаления не существует.

 Почему возникает риск введения ошибок в отладочный код? Проблема

 производительности отходит на задний план, когда речь заходит

 о поиске ошибок. 

ПАРАМЕТРЫ :

IpDVInfo — Указатель на структуру DVINFO

 ВОЗВРАЩАЕТ :

1 — функции дампа и проверки корректности клиентского блока были

успешно добавлены. 

0 — функции дампа и проверки корректности клиентского блока не могут

быть добавлены.

- - - - - - - - - - - - - - - - - - - -  - - - - - - - - */

int BUGSUTIL_DLLINTERFACE _stdcall AddClientDV (LPDVINFO IpDVInfo);

/*- - - - - - - - - - - - - - - - - - - - - - - - - - - - -

ФУНКЦИЯ : ValidateAllBlocks

 ОБСУЖДЕНИЕ :

Проверяет все распределения памяти за пределами локальной кучи.


Кроме

 того, просматривает все Client-блоки и вызывает специальные функции

  проверки корректности для различных подтипов этих блоков.

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

ПАРАМЕТРЫ :

pContext — Контекстная информация, которая будет передаваться

в каждую функцию проверки корректности. 

ВОЗВРАЩАЕТ :

Ничего. 

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/

void BUGSOTIL_DLLINTERFACE _stdcall

ValidateAllBlocks ( void * pContext);

 #ifdef _cplusplus

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

// Макросы вспомогательных классов C++

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

// Объявите этот макрос в своем классе как обычный MFC-макрос,

 #define DECLAREJYEMDEBUG(classname)

 public :

static DVINFO m_stDVInfo;

static void ClassDumper ( const void * pData);

static void ClassValidator ( const void * pData,

const void * pContext);

static void * operator new ( size_t nSize)

{

if ( 0 == m_stDVInfo.dwValue)

{

m_stDVTnfо.pfnDump = classname::ClassDumper;

 m_stDVInfo.pfnValidate = classname::ClassValidator; 

AddClientDV ( &m_stDVInfo); 

}

return ( _malloc_dbg ( nSize 

(int)m_stDVlnfо.dwValue, 

_FILE_ ,

 _LINE_ ) ) ;

}

static void * operator new ( size_t nSize ,

char * IpszFileName,

 int nLine )

{

if ( 0 = m_stDVInfo.dwValue)

{

m_stDVInfo.pfnDump = classname::ClassDumper; 

m_stDVInfo.pfnValidate = classname::ClassValidator; 

AddClientDV ( &m_stDVInfo);

 }

return ( _malloc_dbg ( nSize 

(int)m_stDVInfо.dwValue,

 IpszFileName ,

 nLine )) ; 

}

static void operator delete ( void * pData)

 {

_free_dbg ( pData, (int)m_stDVInfo.dwValue);

}

// Объявите этот макрос в начале своего СРР-файла.

 #define IMPLEMENT_MEMDEBUG(classname)

DVINFO classname::m_stDVInfо = { 0, 0, 0 }

// Макрос для отладочных распределений памяти.


Если определено

// символическое имя DEBUG_NEW, то этот макрос использовать нельзя.

#ifdef DEBUG_NEW

tdefine MEMDEBUG_NEW DEBUG_NEW

#else

#define MEMDEBUG_NEW new ( _FILE_, _LINE_)

#endif

#endif // идентификатор _cplusplus определен

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

// Вспомогательные С-макросы

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

// Используйте этот макрос для распределения памяти в С-стиле.

 // Единственной проблемой при этом является необходимость работы со

 // структурой DVINFO.

Idefine INITIALIZE_MEMDEBUG(IpDVInfo, pfnD, pfnV) 

{

ASSERT ( FALSE == IsBadWritePtr ( IpDVInfo,

sizeof ( DVINFO))); 

((LPDVINFO)IpDVInfo)->dwValue = 0; 

((LPDVINFO)IpDVInfo)->pfnDump = pfnD; 

((LPDVINFO)IpDVInfo)->pfnValidate = pfnV;

 AddClientDV ( IpDVInfo); 

}

// Макросы, которые преобразуют функции распределения памяти С-формата

 //в более удобную для применения форму. Он избавляет вас от 

// запоминания и кодирования различных значений блока DVINFO,

 // используемых в функциях работы с памятью.

#define MEMDEBUG_MALLOC(IpDVInfo, nSize) \

_malloc_dbg ( nSize , \

((LPDVINFO)IpDVInfo)->dwValue, \

_FILE_ , \

_LINE_ )

#define MEMDEBUG_REALLOC(IpDVInfo, pBlock, nSize) \ 

__realloc_dbg ( pBlock , \ 

nSize , \ 

((LPDVINFO)IpDVInfo)->dwValue , \ 

_FILE_ , \

 _LINE_ )

#define MEMDEBUG_EXPAND(IpDVInfo, pBlock, nSize) \

  _expand_dbg( pBlock , \ 

nSize , \ 

' ((LPDVINFO)IpDVInfo)->dwValue , \

 _FILE_ , \ 

_LINE_ )

#define MEMDEBUG_FREE(lpDVInfo, pBlock) \ 

_free_dbg ( pBlock , \ 

((LPDVINFO)IpDVInfo)->dwValue)

#define MEMDEBUG_MSIZE(IpDVInfo, pBlock} \

_msize_dbg ( pBlock, ((LPDVINFO)IpDVInfo)->dwValue)

// Макрос для вызова функции ValidateAllBlocks

#define VALIDATEALLBLOCKS(x) ValidateAllBlocks ( x)

#else // _DEBUG не определен.

#ifdef _cplusplus

#define DECLARE_MEMDEBUG(classname) 

#define IMPLEMENT_MEMDEBUG(classname)

#define MEMDEBUG_NEW new

#endif // _cplusplus

#define INITIALIZE_MEMDEBUG(IpDVInfo, pfnD, pfnV)

#define MEMDEBUG_MALLOC(IpDVInfo, nSize) \

malloc ( nSize)

 #define MEMDEBUG_REALLOC(IpDVInfo, pBlock, nSize) \

realloc ( pBlock, nSize)

 #define MEMDEBUG_EXPAND(IpDVInfo, pBlock, nSize) \

_expand ( pBlock, nSize)

 #define MEMDEBUG_FREE(IpDVInfo, pBlock) \

free ( pBlock) ttdefine MEMDEBUG_MSIZE(IpDVInfo, pBlock) \

_msize ( pBlock)

 #define VALIDATEALLBLOCKS(x)

 #endif // _DEBUG ttifdef _cplusplus

 }

#endif // _cplusplus

 #endif // _MEMDUMPERVALIDATOR_H



Использование MemDumperValidator в приложениях C++


К счастью, для того чтобы MemDumperValidator заработал в приложениях C++, нужно выполнить относительно простую операцию — определить особый класс для работы с расширением MemDumperValidator. В объявлении этого класса нужно определить макрос DECLARE_MEMDEBUG с именем класса в качестве параметра. Этот макрос немного похож на макрос MFC, который расширяется в пары объявлений — для данных и методов. При просмотре листинга 15-1 можно найти определения трех встроенных функций — new, delete и new, содержащих информацию об имени исходного файла и номере строки соответствующего вызова. Если в вашем классе определен любой из этих операторов, то необходимо извлечь их код из соответствующих операторов расширения и поместить его в операторы вашего класса.

В файле реализации вашего С++класса нужно использовать макрос IMPLEMENT_MEMDEBUG (и снова с именем класса в качестве параметра), который определяет статическую переменную этого класса. Макросы DECLARE
_MEMDEBUG И IMPLEMENT_MEMDEBUG работают только в отладочных построениях, поэтому их не следует обрамлять директивами условной компиляции.

После определения обоих макросов (и правильного их размещения в исходном коде), необходимо реализовать два метода, которые будут выполнять фактический дамп и проверку корректности блоков памяти. Прототипы этих методов выглядят так:

static void ClassDumper ( const void * pData); 

static void ClassValidator ( const void * pData,

const void * pContext);

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

Параметр pData в обоих методах — это указатель на блок памяти с экземпляром (объектом) данного класса. Для получения работоспособного указателя следует выполнить явное приведение типа значения, хранящегося в pData (т. е. адреса объекта), к типу класса. Во всех операциях, связанных с выдачей дампов и проверкой корректности (памяти), значения параметра pData следует использовать только для чтения, иначе в код может быть введено множество ошибок, которые придется устранять.
Второй параметр метода ClassValidator - pContext — это указатель на контекст, который вы передаете в первый вызов функции ValidateAliBlocks. Подробнее об этой функции рассказано чуть ниже, в разделе "Глубокие проверки корректности" данной главы.

Приведу две рекомендации по реализации метода ClassDumper. Во-первых, для того чтобы вывод форматированного дампа выполнялся в том же месте, что и вывод остальной части DCRT-библиотеки, нужно использовать макросы _RPTn и _RFTFn. Во-вторых, вывод дампов нужно заканчивать комбинацией символов CR/LF (Carriage Return/Line Feed — Возврат Каретки/Перевод Строки), потому что макросы DCRT-библиотеки не выполняют никакого форматирования.

Установка функций вывода дампов и проверок корректности блоков памяти для классов языка C++ является почти тривиальной задачей. Как же обстоят дела с выполнением подобных операций для структур данных языка С? К сожалению, аналогичная обработка С-структур требует немного больше усилий со стороны программиста.



Использование утилиты MemStress


Теперь самое время добавить немного стресса. Как ни странно, но он может играть и положительную роль. К сожалению, довести до этого состояния 32-разрядные приложения Windows в наши дни намного труднее, чем раньше. Приложения прежних 16-разрядных Windows (да еще и современной 16-разрядной подсистемы Windows 98) можно выполнять под управлением изящной программы STRESS.EXE, которая поставляется в составе SDK и позволяет вводить в приложение различного рода искажения. Например, такие, которые заставляют ее буквально "пожирать" дисковое пространство и динамическую память (heap) интерфейса графических устройств (Graphics Device Interface — GDI), а также усиленно использовать файловые дескрипторы. Программа имеет подходящую пиктограмму — изображение слона, идущего по натянутому канату.

Чтобы подвергнуть "стрессу" 32-разрядные приложения Windows, можно подключиться к системе распределения памяти DCRT-библиотеки и контролировать результаты (успехи или неудачи) ее операций. MemStress обеспечивает средства для усиленного распределения памяти в С/С++-приложениях. (Оставлю на усмотрение читателя написание кода, "съедающего" дисковую память.) Чтобы облегчить применение MemStress, я написал интерфейс (на языке Visual Basic), позволяющий точно задать условия отказов.

MemStress обеспечивает принудительное создание отказов в распределениях (памяти), основанных на различных критериях: для всех распределений, для каждого n-ого распределения, после распределения определенного числа байт, по запросам через каждые n байт, для всех распределений вне исходного файла и на конкретной строке в исходном файле. Кроме того, можно заставить MemStress выдавать на любом запросе распределения подсказку в виде панели сообщения, в которой спрашивается, хотите ли вы получить отказ в данном конкретном распределении. Можно также устанавливать флажки DCRT-библиотеки, которые желательным образом влияли бы на программу. MFC-программа MemStressDemo (проект которой поставляется на сопровождающем компакт-диске) позволяет экспериментировать с установкой различных опций пользовательского интерфейса (UI) MemStress и просматривать соответствующие результаты.


Работать с MemStress довольно просто. Включите в свой код заголовочный файл BUGSLAYERUTIL.H и вызовите макрос MEMSTRESSINIT с именем вашей программы. Чтобы прекратить подключение распределений памяти, используйте макрос MEMSTRESSTERMINATE. Во время прогонов программы можно инициировать и останавливать эти подключения сколько угодно раз.

Скомпилировав свою программу, запустите MemStress UI, нажмите кнопку Add Program и введите (с клавиатуры) то же самое имя, которое указано в макросе MEMSTRESSINIT. После выбора желательных опций отказов нажмите кнопку Save Settings For This Program, чтобы сохранить установки в файле MEMSTRESS.INI. Теперь можно запускать программу и следить за ее поведением при отказах в распределении памяти.

Применять MemStress следует аккуратно. Например, если потребовать, чтобы терпели неудачу все распределения, превышающие 100 байт, при условии, что в функции initinstance вашего MFC-приложения имеется макрос MEMSTRESSINIT, то можно будет заподозрить MFC в неспособности инициализировать свои объекты. Лучшие результаты вы получите, если ограничите область действия MemStress тестированием отдельных ключевых областей программы.

Большая часть кода MemStress занимается чтением и обработкой файла MEMSTRESS.INI, в котором хранятся все установки для индивидуальных программ. С точки зрения DCRT-библиотеки, наиболее важной функцией является обращение к функции _CrtSetAilocHook во время инициализации MemStress, потому что для обработки распределений памяти этот вызов устанавливает функцию AllocationHook. Если эта функция возвращает TRUE, то запрос распределения продолжается, а возврат FALSE означает, что DCRT-библиотека отклонила этот запрос. К обработке распределения со стороны DCRT-библиотеки имеется только одно твердое требование: если параметр nBiockUse задает тип блока _CRT_BLOCK, то hook-функция должна возвратить значение TRUE, что позволит распределять блоки этого типа.

Обработка распределения выполняется для функций распределения любого типа. Тип функции указан в ее первом параметре, для которого допустимы следующие значения: _HOOK_ALLOC, _HOOK_REALLOC и _HOOK_FREE).Если в своей функции обработки распределения AllocationHook указать тип _HOOK_FREE, то будет пропущен весь код, который определяет, должен ли запрос памяти передаваться или отклоняться. Для типов _HOOK_ALLOC и _HOOK_REALLOC функция AiiocationHook выполняет ряд if-операторов, чтобы определить, встретилось ли какое-нибудь из условий отказа в распределении. Если условие отказа встретилось, функция возвращает FALSE.



Куда направляются отчеты об утечках памяти?


Решив проблему инициализации, удалось, наконец, получить работающее расширение MemDumperValidator. И все работало хорошо — за исключением того, что функции, выдающие дампы при обнаружении утечек памяти, по завершении программы не генерировали удобно отформатированного вывода. Дампы памяти оказывались стандартными дампами DCRT-биб-лиотеки. Прослеживание "потерявшихся" отчетов об утечках показало, что функции завершения DCRT-библиотеки вызывают функцию _crtsetDumpclient с параметром NULL (пустой указатель), очищая, таким образом, подключение дампа перед вызовом функции _crtDumpMemoryLeaks. Стало понятно, что нужно просто самому выполнить заключительную проверку утечки памяти. К счастью, у меня нашлось подходящее место, чтобы выполнить эту операцию.

Общий Вопрос Отладки

Зачем нужна отладочная DCRT-библиотека, если используется инструмент обнаружения ошибок типа утилиты BoundsChecker?

Инструменты обнаружения ошибок, такие как BoundsChecker от Compuware NuMega и Purify от Rational Software автоматически обрабатывают записи, перезаписи (underwrites и overwrites) и утечки памяти. Если вы используете один из этих инструментов, то, вероятно, думаете, что на DCRT-библиотеку не стоит тратить время и усилия. Технически это так, но, чтобы гарантированно разрешить все проблемы с памятью, необходимо запускать приложение под управлением инструмента обнаружения ошибок каждый раз, когда кто-то из команды разработчиков выполняет отладочное построение (debug build) приложения. Таким партнером по отладке может быть кто угодно — вы сами, ваши коллеги разработчики и, если вы следовали рекомендациям из главы 2, то даже сотрудники отдела контроля качества.

Использование DCRT-библиотеки подобно наличию хорошего страхования от пожара или грабежа. Все надеются, что им никогда не понадобится страховка, но, застраховавшись, можно предохранить себя от многих неприятностей. Проверку корректности данных в приложении необходимо выполнять при любой возможности. DCRT-библиотека не наносит большого ущерба производительности приложения, но помогает находить некоторые серьезные ошибки.
Надо всегда работать с этой библиотекой, какими бы дополнительными инструментами отладки вы ни пользовались.

Поскольку директива #pragma init_seg(compiler) уже использовалась для предварительной инициализации класса AutoMatic (и вызова деструктора после выполнения самого последнего оператора приложения), то в этой же точке исходного кода нужно было выполнять и проверку утечки памяти, а затем выключать флажок _CRTDBG_LEAK_CHECK_DF, чтобы DCRT-библиотека не выводила собственных отчетов. Единственное предостережение — при использовании этого подхода необходимо удостовериться, что выбранная CRT-библиотека включается перед файлом BUGSLAYERUTIL.LIB (если компоновка выполняется с ключом /NODEFAULTLIB). При компоновке файла BUGSLAYERUTIL.LIB может оказаться, что CRT-библиотеки не зависят от соответствующих директив #pragma init_seg(compiler), гарантирующих, что данные этих библиотек инициализируются первыми, а разрушаются последними, так что поддерживать правильный порядок в размещении этих директив программист должен самостоятельно.

Здесь речь идет о размещении указанной директивы перед исходным кодом отлаживаемого приложения. — Пер.

Если внимательно подумать, то установка режима чистки обработчиков функций вывода дампов DCRT-библиотекой имеет определенный смысл. Если бы ваш обработчик использовал какую-нибудь функцию из CRT-библиотеки (скажем, printf), то он мог бы завершить вашу программу аварийно, потому что, когда вызывается функция _crtDumpMemoryLeaks, CRT-библиотека находится где-то в середине процесса завершения своей работы. Надо соблюдать правила и всегда компоновать DCRT-библиотеку перед компоновкой любых других библиотек, т. к. функции расширения MemDumperValidator завершаются прежде, чем заканчивает работу DCRT-библиотека. Чтобы обойти подобные проблемы, применяйте в дамп-функциях только макросы _RPTn и _RPTFH, потому что их использует и функция _CrtDumpMemoryLeaks.



Применение MemDumperValidator в приложениях С


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

Чтобы использовать MemDumperValidator в С-приложении, необходимо придерживаться определенной последовательностью действий. Во-первых, нужно объявить DVINFO-структуру для каждого типа памяти, который вы хотите контролировать через вывод дампа и проверку корректности. В C++ соответствующий макрос автоматически объявляет методы дампов и проверок корректности, но в С следует самостоятельно выполнить некоторую дополнительную работу. Имейте в виду, что все макросы, о которых здесь идет речь, используют указатель на конкретные DVINFO-структуры. Прототипы С-функций вывода дампов и проверки корректности — те же, что и у соответствующих методов в C++, за исключением того, что ключевое слово static в них не используется. Объявления DVINFO-структур для специфических блоков памяти и определения всех С-функций вывода дампов и проверки корректности удобно размещать в объединенном файле. Перед началом конкретных операций работы с памятью1нужно сообщить расширению MemDumperValidator о подтипах используемых клиентских блоков и их функциях вывода дампов и проверки корректности. Всю эту информацию следует передавать в MemDumperValidator с помощью макроса INITIALIZE_MEMDEBUG, который принимает в качестве параметров' соответствующую DVINFO-структуру, функцию вывода дампа и функцию проверки корректности. Этот макрос нужно выполнять перед началом распределения блоков памяти соответствующего типа.

К операциям работы с памятью относятся следующие операции, выполняемые над блоками памяти: выделение/освобождение/повторное выделение (ailocation/free/reallocation), дамп (dump), проверка корректности содержимого (validation), получение и расширение размера блока (get/expand block size). — Пер

Для выполнения операций обработки блоков памяти в С нужно использовать полный набор макросов, которые передают значения блоков памяти соответствующим функциям обработки. Например, если в программе определена DVINFO-структура stdvBiockinfo, то распределения блоков памяти в С-программе следует выполнять с помощью следующего кода:

MEMDEBUG_MALLOC(&stdvBlockInfo, sizeof (х) ) ;

Все макросы для С-функций, работающих с памятью, можно найти в заключительной части листинга 15-1. Запоминать DVINFO-структуры для каждого типа распределения памяти хотя и возможно, но не очень практично, поэтому для обработки различных DVINFO-структур можно определять специальные макросы-оболочки; таким макросам нужно передавать параметры обычных функций работы с памятью.



Реализация MemDumperValidator


Реализация функций расширения MemDumperValidator не очень сложна. Первая неожиданная проблема, с которой мне пришлось столкнуться, состояла в том, что DCRT-библиотека не документирует способ, с помощью которого hook-функции получают значение блока памяти. Этим функциям передается только указатель на данные пользователя, а не весь блок памяти, который распределяет DCRT-библиотека. К счастью, наличие исходного кода DCRT-библиотеки дало возможность точно видеть, как библиотека распределяет блоки памяти. Все блоки памяти распределяются в виде структуры _GrtMemBiockHeader, которая определена в файле DBGINT.H. Кроме того, в файле DBGINT.H определены макросы, обеспечивающие доступ к полям структуры _GrtMemBiockHeader по указателю данных пользователя и, наоборот, доступ к данным пользователя по указателю этой структуры. Чтобы иметь возможность получать информацию заголовков, я скопировал структуру _CrtMemBiockHeader и макрос доступа в файл заголовков CRTDBG_INTERNALS.H (показанный в листинге 15-3). Полагаться на копию определения структуры, когда это определение может изменяться — не очень хорошая практика, но в данном случае это работает, потому что структура _crtMemBiockHeader DCRT-библиотеки не изменялась в версиях Visual C++ от 4-й до 6-й. Однако это не означает, что данная структура не будет изменена в будущей версии Visual C++. При использовании расширения MemDumperValidator нужно быстро проверять каждый пакет обслуживания и главный релиз компилятора (major release of the compiler), чтобы видеть, изменились ли их внутренние структуры.

Для того чтобы непосредственно использовать DBGINT.H, можно заменить определение структуры в файле CRTDBG_INTERNALS.H директивой #indude DBGINT.H. В этом случае нужно будет также добавить каталог <VC98>\CRT\SRC как к главной переменной окружения INCLUDE, так и в список каталогов Include-файлов на вкладке Directories диалогового окна Options из IDE Visual C++. Поскольку не каждый разработчик устанавливает исходный код CRT-библиотеки, а многие не упоминают об этой структуре в файле README.TXT, то было принято решение использовать прямое включение определения этой структуры.


Листинг 15-3.CRTDBG_INTERNALS.H

/*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

"Debugging Applications" (Microsoft Press)

Copyright (c) 1997-2000 John Robbins — All rights reserved.

- - - - - - - - - - - - - - - - - - - - - - - - -  - - - -*/

#ifndef _CRTDBG_INTERNALS_H

#define _CRTDBG_INTERNALS_H 

#define nNoMansLandSize 4

 typedef struct _CrtMemBlockHeader

 {

struct _CrtMemBlockHeader * pBlockHeaderNext ;

struct _CrtMemBlockHeader * pBlockHeaderPrev ;

char * szFileName ; 

int nLine ;

 size_t nDataSize ;

 int nBlockUse ;

 long IRequest ; 

unsigned char gap[nNoMansLandSize] ;

  /* followed by:

* unsigned char data[nDataSize];

* unsigned char anotherGap[nNoMansLandSize];

*/

} _CrtMemBlockHeader; 

#define pbData(pblock) ((unsigned char *) \

((_CrtMemBlockHeader *)pblock + 1) )

tfdefine pHdr(pbData) (((_CrtMemBlockHeader *)pbData)-l) 

#endif // _CRTDBG_INTERNALS_H

Можно также использовать определение структуры _CrtMemBiockHeader, чтобы получить дополнительную информацию из структур _crtMemstate, возвращаемых функцией _crtMemCheckpoint, потому что первым элементом в этой структуре является указатель на _crtMemBiockHeader. Надеюсь, что будущая версия DCRT-библиотеки обеспечит реальные функции доступа для получения информации о блоках памяти.

Просматривая поставляемый на сопровождающем компакт-диске исходный код файла MEMDUMPERVALIDATOR.CPP, являющегося частью проекта BUGSLAYERUTIL.DLL, можно заметить, что для внутреннего управления памятью применяются API-функции семейства HeapCreate, напрямую работающие с кучей. Причина в том, что функции дампов и hook-функции, используемые с DCRT-библиотекой, будут вызываться повторно, если применяются подпрограммы стандартной библиотеки. Имейте в виду, что мы не говорим о многопоточных повторных входах. Если код обработчика распределяет память с помощью обращения к функции malloc, то он будет введен повторно, потому что hook-функции вызываются при каждом распределении памяти.



имеется много замечательных свойств, которые



У отладочной С-библиотеки времени выполнения (DCRT-библиотеки) имеется много замечательных свойств, которые можно включать в различные приложения. В этой главе приведены основные сведения о DCRT-библиотеке и представлены две авторские утилиты — MemDumperValidator и MemStress. Они позволяют получать больше информации о памяти, которая используется приложением, и лучше тестировать приложение в условиях предельных нагрузок.
Изумляет довольно простая расширяемость DCRT-библиотеки. Надеюсь, мне удалось показать читателю мощь этой библиотеки и стимулировать разработку других утилит и вспомогательных кодов, призванных облегчить рутинные операции отладки памяти.

Свойства библиотеки DCRT


Главной причиной популярности DCRT-библиотеки в том, что она поддерживает мониторинг кучи (heap). В отладочных версиях можно проследить всю память, которая распределяется при помощи стандартных С/С++-функций, таких как new, maiioc и calloc. Монитор кучи проверяет как записи (underwrites), которые программа помещает в начало выделенного блока памяти, так и перезаписи (overwrites), размещаемые за концом блока. Этот механизм контролирует также и утечки памяти в приложении (и выдает соответствующие отчеты). Те, кто писал программы с использованием библиотеки MFC, вероятно, знакомы с отчетами об утечке памяти, генерируемыми при завершении приложения частью библиотеки DCRT, которую MFC автоматически включает в приложение.

Overwrite — обычно обозначает перезапись или наложение одной записи поверх другой. По определению автора (program writes past the end of block memory), в перезапись вкладывается несколько иной смысл: запись за концом блока памяти, что, по-видимому, означает выход записываемых данных за пределы выделенного им блока памяти. — Пер

Подсистема отчетов DCRT формирует трассу памяти (с помощью макросов __RPTn и RPTFn, а также поддержки утверждений). Поддержка таких утверждений библиотекой DCRT описана в главе 3, где рассмотрены также некоторые проблемы, связанные с их использованием. Напомним, что утверждения библиотеки DCRT — это довольно сильное средство, но они разрушают значение последней ошибки, что приводит к различному поведению отладочных и выпускных построений. Я настоятельно рекомендую пользоваться утверждениями программы SUPERASSERT, которая является частью BUGSLAYERUTIL.DLL.

Полезной особенностью DCRT-библиотеки является включение ее исходного кода в компилятор. В табл. 15.1 перечислены все файлы, входящие в состав DCRT-библиотеки. Если исходный код библиотеки CRT выполняется во время установки Microsoft Visual Studio (что я настоятельно рекомендую делать), то все исходные коды библиотек CRT и DCRT можно найти в каталоге ..\Microsoft Visual Studio\VC98\CRT\SRC.


Таблица 15.1. Исходные файлы DCRT-библиотеки.
Исходный файл
Описание
DBGDEL.CPP
Глобальная отладочная операция delete
DBGHEAP.C
Все функции обработки кучи
DBGHOOK.C
Функция подключения заглушки распределения памяти
DBGINT.H
Внутренние отладочные заголовки и функции
DBGNEW.CPP
Глобальная отладочная операция new
DBGRPT.C
Функции отладочных сообщений
CRTDBG.H
Файл заголовков, который нужно включать в приложение, пользующееся DCRT-библиотекой. Этот файл находится в стандартном каталоге \lnclude


Выбор правильного варианта CRT-библиотеки


Некоторая неразбериха вокруг применения CRT-библиотек при разработке приложений Windows связана с тем, что необходимо решать, какую библиотеку следует использовать. Существует шесть версий этой библиотеки, которые можно разделить на две категории: отладочную (DCRT) и выпускную (CRT). В каждую категорию входит однопоточная статическая библиотека, многопоточная статическая библиотека и многопоточная библиотека динамической компоновки (DLL).

Статические версии CRT-библиотек включают библиотечные функции прямо в приложение. Эти версии используются по умолчанию для не-МРС-приложений, которые собираются с помощью мастеров. Преимущество статических версий заключается в том, что DLL таких библиотек не нужно отправлять заказчику вместе с разработанным продуктом. Недостатком же является то, что сильно возрастает размер двоичных файлов, так что рабочая конфигурация приложения становится довольно большой. Однопоточный и многопоточный варианты статической библиотеки CRT не требуют объяснений. Если вы создаете DLL и хотите использовать статическую CRT-библиотеку, то нужно выполнять компоновку только с многопоточной версией этой библиотеки (в противном случае приложение не сможет использовать вашу DLL, потому что однопоточные статические CRT-библиотеки не являются потокобезопасными).

DLL-версии библиотек CRT с именами MSVCRT(D).DLL позволяют импортировать их функции. Большим преимуществом этих DLL является то, что размер двоичных файлов радикально уменьшается, значительно сокращая, таким образом, рабочий набор приложения.

Поскольку другие приложения загружают одну и ту же DLL-версию библиотеки CRT, операционная система может разделять таблицы страниц кодовых секций DLL между процессами, так что вся система будет выполняться быстрее. К недостатком таких DLL-версий следует отнести то, что приложения могут быть ориентированы на другие варианты DLL, поэтому вместе с ними придется распространять и соответствующие варианты DLL.

Для приложений чрезвычайно важен выбор одной версии CRT-библиотеки, используемой всеми двоичными файлами, которые загружаются в адресное пространство их главной программы.
Если приложение работает с несколькими DLL, каждая из которых использует свою статическую CRT-библиотеку, то мало того что увеличивается адресное пространство за счет дублирования кода, но также повышается риск введения одной из наиболее коварных ошибок, которую можно отслеживать месяцами. Если вы распределяете область кучи в одной DLL, а пытаетесь освобождать ее в другой, использующей иную версию библиотеки CRT, то легко можно вызвать аварийное завершение, потому что DLL, освобождающая память не знает, откуда взялась распределяемая память. Было бы ошибкой полагать, что в данном случае мы имеем дело с обычной кучей, т. к. наличие различных версий CRT-библиотеки, выполняющихся одновременно, означает, что имеется много версий кода управления кучей.

Я почти всегда использую DLL-версии библиотек CRT и советую всем придерживаться этого правила. Выгода от уменьшения количества и размера компилированных файлов перекрывает любые другие соображения. Например, при разработке игр, где никогда не будет нужна многопоточность и где производительность является суперкритичным параметром, я допускаю даже использование однопоточных статических версий CRT-библиотек, чтобы избежать накладных расходов, возникающих за счет применения механизмов многопоточной блокировки.

В утилите BUGSLAYERUTIL.DLL используются DLL-версии библиотек CRT. Кроме того, в BUGSLAYERUTIL.DLL включено два расширения DCRT-библиотеки: MemDumperValidator и MemStress, которые рассмотрены ниже в этой главе. Ожидается, что вы пользуетесь DLL-версиями этих расширений. Однако если вы хотите, чтобы приложение работало с модулями ЕХЕ, а не их DLL-версиями, то нужно просто взять исходные файлы MEMDUMPERVALIDATOR.CPP, MEMDUMPERVALIDATOR.H, а также MEMSTRESS.CPP, MEMSTRESSCONSTANTS.H и MEMSTRESS.H, изменить компоновку указанных функций и поместить их в приложение.

Хочу обратить ваше внимание на одну дополнительную деталь применения BUGSLAYERUTIL.DLL. Можно наблюдать некоторое замедление приложения в зависимости от того, как распределяется память.Чтобы разрешить полное прослеживание и проверку корректности памяти, я устанавливаю в расширении MemDumperValidator все подходящие флажки DCRT-библио-теки, включая _CRTDBG_CHECK_ALWAYS_DF. Установка этого флажка приводит к тому, что каждый раз, когда вы распределяете или освобождаете кучу, DCRT-библиотека просматривает каждый ее участок и проверяет корректность данных. Если в приложении происходят тысячи распределений небольших областей памяти, то его выполнение ощутимо замедлится. Если замедление недопустимо, то возможны два решения. Первое — перед выполнением распределений памяти нужно сбросить флажок _CRTDBG_CHECK _ALWAYS_DF (обратившись к функции _GrtsetDbgFiag). Второе — проверить алгоритм и посмотреть, нужны ли вообще распределения небольших участков памяти, потому что даже без проверки отладочной кучи операции выделения памяти чрезвычайно замедляют приложение

.