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

         

Что делать дальше с DeadlockDetection?


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

 создайте автономное приложение для манипуляций с файлом DEADLOCKDETECTION.INI. Ваша программа могла бы даже улучшиться, если бы она разрешила вам устанавливать DeadDetExt DLL и проверяла, что выбранный DeadDetExt DLL экспортировал правильные функции; можно было бы улучшить оптимизацию функций подключения, если бы они не выполняли никакой регистрации. В этом случае не все значения регистров нужно копировать;  сейчас DeadlockDetection просто пропускает подключение тех DLL, которые ей известны. Хорошо бы разработать программный механизм, определяющий, какие DLL следует пропускать.

История отладочной войны

Незавершенные транзакции с объединенными СОМ-объектами Сражение

Питер Иерарди (Peter lerardi) рассказал мне об интересной многопоточной ошибке, с которой он столкнулся. Он работал над большим DCOM-проектом,который использовал многопоточную DCOM-службу для координации транзакций базы данных. DCOM-служба управляла транзакциями, создавая пул внут-рипроцессных централизованных СОМ-объектов, которые использовались, чтобы записывать и считывать данные из реляционной системы управления базами данных (RDBMS). Межкомпонентная связь выполнялась через сервер очереди сообщений Microsoft (Microsoft Message Queue (MSMQ) Server). Несмотря на то, что выполнялась явная фиксация транзакций, данные в базу данных не записывались. Однако после 3—5-кратных повторных попыток записи, данные, как по мановению волшебной палочки, наконец появлялись в базе. Очевидно, чрезмерные повторения уменьшали производительность приложения и тот факт, что данные не записывались в базу, был причиной для беспокойства.



Результат

После нескольких тяжелых сеансов отладки Питер нашел, что DCOM-служба выполняла чтение и запись на отдельных, несинхронизированных потоках.
Чтение происходило прежде, чем отдельный экземпляр СОМ-объекта базы данных записывал данные. Такое поведение не было очевидным во время отладочных сессий, потому что отладчик форсировал надлежащее таймирование и синхронизацию. Он в конечном счете решил проблему с помощью маркирования экземпляров объекта в Event Log (журнале регистрации событий).

Урок

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

Что касается ошибки Питера, то она объясняется очень просто: циклы операций чтения/записи MSMQ выполнялись намного быстрее, чем в базе данных. Хотя Питер и его команда тщательно планировали и прорабатывали многопо-точность в своем проекте, им все еще не доставало некоторого исходного понимания того, насколько быстрее будут выполняться некоторые операции вне их проекта, в реальном мире.



DeadlockDetection и проблемы высокоуровневого проектирования


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

Обдумывая, как собрать информацию, необходимую для удовлетворения первых четырех требований, я понял, что надо перехватывать (или подключаться по адресу1) функции, указанные в табл 12.1, для того чтобы регистрировать (записывать) получение и освобождение объектов синхронизации. Подключение функций через адрес — нетривиальная задача (реализация соответствующего кода рассмотрена в разделе "Подключение импортированных функций" чуть ниже). Такое подключение накладывает на DeadlockDetection следующее ограничение: ее код должен постоянно храниться в DLL, потому что такие подключения применяются только к адресному пространству, в котором он создан. Это ограничение означает, что пользователь должен загрузить DLL с программой DeadlockDetection в ее адресное пространство. Это требование не слишком жестко по сравнению с получаемыми от него преимуществами. Как и DLL, утилита будет легко объединяться с программой пользователя (см. условие, указанное в п. 7 списка требований предыдущего раздела).

Сбор информации, удовлетворяющей требованиям 1—4, является прямым следствием выбора особого подхода "с подключением функций в процессе выполнения программы" (in-process function hooking approach). Такой подход означает, что каждая многопоточная функция и функция синхронизации будет вызываться прямо в процессе выполнения DeadlockDetection вместе со всей необходимой информацией.

Таблица 12.1. Функции, мониторинг которых осуществляет DeadlockDetection

                 Тип              Функция

Функции, относящиеся к потокам

CreateThread                                                      ExitThread                                                             SuspendThread                                                         ResumeThread                                                 TerminateThread

Функции критической секции

Initial izeCr it icalSection

InitializeCriticalSectionAndSpinCount                              

DeleteCr it icalSection                                                       

EnterCr it icalSection                                                      

LeaveCr it icalSection                                

SetCriticalSectionSpinCount                      

TryEnterCriticalSection

Функции мьютекса

CreateMutexA                                                       CreateMutexW                                                       OpenMutexA                                                       OpenMutexW                                                      ReleaseMutex


Функции семафора

CreateSemaphoreA                                  CreateSemaphoreW                                             OpenSemaphoreA                                               OpenSemaphoreW                                           ReleaseSemaphore

Функции событий

CreateEventA                                                                 CreateEventW                                                             OpenEventA                                                                  OpenEventW                                                                        PulseEvent                                                                      ResetEvent                                                                       SetEvent

Функции блокировки

WaitForSingleObject                                   WaitForSingleObjectEx                             WaitForMultipleObjects                       WaitForMultipleObjectsEx                   MsgWaitForMultipleObjects    MsgWaitForMultipleObjectsEx                                 SignalOb j ectAndWait

Специальные функции

CloseHandle                                                                ExitProcess                                                         GetProcAddress




Требование минимального вмешательства DeadlockDetection в программу пользователя (п. 5 списка требований) довольно трудно удовлетворить. Рассчитывая на то, что разработчик должен знать, какие типы объектов синхронизации используются в его программе, я сгруппировал типы объектов так, чтобы можно было указывать лишь те функции, которые требуется подключить. Например, если проверяются только блокировки на мьютексах, то можно обрабатывать только мьютекс-функции.

Работая с DeadlockDetection, можно указать "на лету", какой набор функций объектов синхронизации надо наблюдать. Вы можете также включать и выключать DeadlockDetection столько раз, сколько необходимо. Можно даже назначить для приложения клавишу быстрого вызова или специальный пункт меню, который подключает всю DeadlockDetection-систему целиком. Реализация этих узких возможностей удовлетворяет требованиям п. 5 и помогает реализовать требование п. 7.

Что касается требования п. 6 — сделать обработку вывода настолько расширяемой, насколько возможно, то пользователю предоставляется возможность самостоятельно оформлять вывод. Организуя раздельное хранение кода основных подключений и кода обработки вывода, удалось достичь большей степени повторного использования кода, потому что единственную изменяемую часть кода — выводную — намного легче разрабатывать, чем его ядро. Я называю выводящие части кода расширениями утилиты DeadlockDetection (или еще короче — DeadDetExi). DeadDetExt это просто DLL, которая содержит несколько экспортируемых функций. DeadlockDetection отыскивает и вызывает эти функции, когда в них возникает необходимость.

Настало время объяснить, как можно использовать DeadlockDetection. Понимая рассмотренные требования и возможности применения этой утилиты, легче разобраться и в ее реализации.



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


Первым шагом в использовании утилиты DeadlockDetection является размещение файла ее инициализации DEADLOCKDETECTION.DLL и соответствующего файла расширения DeadDetExt DLL в том же каталоге, в котором находится сама программа. Файл инициализации — это простой INI-файл, который, как минимум, должен указывать имя файла расширения (в нашем случае DeadDetExt), чтобы загружать его. В следующем примере показан файл инициализации DEADLOCKDETECTION.INI, который загружает файл TEXTFILEDDEXT.DLL

[Initialization]

Единственное обязательное значение — имя файла DeadDetExt,

который будет обрабатывать вывод

ExtDll = "TextFileDDExt.dll"

Если StartlnDllMain равен 1, DeadlockDetection будет

инициализирована в своей функции DllMain, чтобы регистрация

могла начинаться как можно раньше.

 StartlnDllMain = О

Если StartlnDllMain равно 1, InitialOpts указывает

начальные режимы для DeadlockDetection. Это значение

является комбинацией DDOPT_*-флажков.

InitialOpts = 0

Как можно видеть по некоторым установкам, инициализация DeadlockDetection возможна только в том случае, если в ней уже вызвана функция LoadLibrary. Хорошей идеей профилактической (упреждающей) отладки было создание "закулисной" инициализации приложения, которая вызывает LoadLibrary для DLL с указанным именем, если приложение обнаруживает специальный ключ реестра или переменную окружения. Этот альтернативный подход к инициализации приложения означал бы, что условная компиляция не нужна, а пользователю предоставлены средства прямого получения DLL-файлов в свое адресное пространство. Конечно, все это предполагает, что все DLL, которые загружаются таким способом, достаточно "разумны", чтобы совершенно самостоятельно инициализироваться в своих DiiMain-секциях и не требовать вызова никаких других экспортируемых функций в DLL.

Если вместо использования INI-файла ваш код устанавливает параметры инициализации, то нужно включить в приложение файл заголовков DEADLOCKDETECTION.H и компоновать его с библиотечным файлом DEADLOCKDETECTION.LIB.
Для того чтобы инициализировать DeadlockDetection самостоятельно, надо лишь вызвать в подходящий момент функцию openDeadiockDetection, которая принимает единственный параметр — режимы начальных отчетов (initial reporting options). Все DDOPT_*-флажки перечислены в табл. 12.2. Чтобы получить возможность записывать (регистрировать) всю ключевую информацию об объектах синхронизации, функцию OpenDeadiockDetection необходимо вызвать перед тем, как приложение начнет создание потоков.

Таблица 12.2. Режимы отчетов DeadlockDetection

Флажок

Пределы регистрации

DDOPT_THREADS

Функции, относящиеся к потокам

DDOPT CRITSEC

Функции критической секции

DDOPT_MUTEX

Функции мьютекса

DDOPT SEMAPHORE

Функции семафора

DDOPT_EVENT

Функции событий

DDOPT ALL

Все подключаемые функции

В любой точке программы можно изменять режимы отчетов, вызывая функцию SetDeadiockDetectionOptions. Эта функция принимает тот же набор флажков (объединенных операцией OR), что и функция OpenDeadiockDetection. Чтобы получить текущий режим отчетов, нужно вызвать функцию GetDeadlockDetectionOptions. Во время выполнения программы можно изменять текущий режим сколько угодно раз. Если нужно приостановить или возобновить регистрацию, вызывайте функцию ResumeDeadlockDetection ИЛИ SuspendDeadlockDetection.

Наряду с исходным кодом DeadlockDetection, сопровождающий компакт-диск содержит и DLL-файл расширения DeadDetExt (TEXTFILEDDEXT.DLL). Это относительно простое расширение записывает всю информацию в текстовый файл. Когда DeadlockDetection выполняется с TEXTFILEDDEXT.DLL, расширение создает текстовый файл в том же каталоге, где находится выполняемая программа. Текстовый файл использует имя выполняемого файла с расширением .DD. Например, если выполняется SIMPTEST.EXE, то в результате будет создан файл SIMPTEST.DD. Листинг 12-1 показывает пример вывода из TEXTFILEDDEXT.DLL.

 Листинг 12-1. Вывод DeadlockDetection с использованием TEXTFILEDDEXT.DLL 



TID Ret Addr C/R Ret Value Function & Params 

0x000000F? [Ox004011AC] (R) 0x00000000 InitializeCriticalSection

0x00403110

 0x000000F7 [0x00401106] (R) 0x00000290 CreateEventA 0x00000000, 1, 0,

0x004030F0 [The event name] 

0x000000F? [Ox004011E9] (R) 0x00000294 CreateThread 0x00000000,

0x00000000, 0x00401000,

0x00000000, 0x00000000,

0x0012FF68 

0x000000F7 [0x0040120C] (R) 0x00000298 CreateThread 0x00000000,

0x00000000, 0x004010BC,

0x00000000, 0x00000000,

0x0012FF68

0x000000FV [0x00401223] (C) EnterCriticalSection 0x00403110 

0x000000F7 [0x00401223] (R) 0x00000000 EnterCriticalSection 0x00403110 0x000000F? [0x00401238] (C) WaitForSingleObject 0x00000290,

INFINITE

0x000000FF [Oxl020B973] (C) EnterCriticalSection 0xl025CE90

 0x000000FF [Oxl020B973] (R) 0x00000000 EnterCriticalSection 0xl025CE90

 0x00000l0C [Ox004010F3] (R) 0x000002A4 OpenEventA 0x001F0003, 0,

0x004030BC

[The event name]

Заметьте, что информация о функции и параметрах (в столбце Function & Params) в листинге 12-1 не поместилась в одной строке и поэтому перенесена на следующие строки (в этом же столбце). Информация выводится на экран в следующем порядке:

1. Идентификатор (ID) выполняющегося потока.

2. Адрес возврата, указывающий, какая из ваших функций вызвала функцию синхронизации. Используя утилиту CrashFinder, рассмотренную в главе 8, можно просмотреть адреса возвратов и узнать, как вы входили в ситуации блокировки.

3. Индикатор вызова/возврата (Call/Return — C/R), помогающий идентифицировать действия, которые происходят до и после конкретной функции.

4. Возвращаемое значение функции, если программа возвращает функции отчетов.

5. Имя функции синхронизации.

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

Если запускаемое приложение блокируется, то для того чтобы увидеть последний вызванный элемент синхронизации, аннулируйте процесс и просмотрите файл вывода.TEXTFILEDDEXT.DLL сохраняет последнее файловое обновление, сбрасывая буферы файлов каждый раз, когда вызываются функции WaitFor*, EnterCriticalSection И TryEnterCriticalSection.

Предостережение

Если включить полную регистрацию (всех функций), то можно довольно долго генерировать чрезвычайно большой файл. Используя Visual С++-приложение MTGDI, я генерировал текстовый файл объемом в 11 Мбайт за одну или две минуты, если создавал пару потоков.



Основные моменты реализации


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

Чтобы показать эту простоту, поговорим о том, как была написана DeadDetExt DLL. DeadDetExt DLL должна иметь три экспортируемых функций. Две первых — DeadDetExtOpen И DeadDetExtClose — самоочевидны. Интересна функция DeadDet, которую вызывает каждая функция подключения, когда имеется информация для записи. DeadDetProcessEvent принимает единственный параметр — указатель на структуру DDEVENTINFO:

typedef struct tagDDEVENTINFO 

{

// Идентификатор, который указывает, что содержит остальная часть

// этой структуры

eFuncEnum eFunc ;

// Индикатор pre- или post-вызова 

ePrePostEnum ePrePost ;

// Адрес возврата. Этот адрес помогает в нахождении вызова

// функции.

DWORD dwAddr;

// ID вызывающего потока

DWORD dwThreadld;

// Возвращаемое значение для post-вызовов.

DWORD dwRetValue ;

// Информация параметра. Преобразовать тип этой информации

//в подходящую структуру для функции (как описано ниже).

// При доступе к параметрам трактовать их как read-only

// (только-для-чтения).

DWORD dwParams 

} DDEVENTINFO-, * LPDDEVENTINFO;

Полный вывод для единственной функции, которая появляется в листинге 12-1, формируется информацией структуры DDEVENTINFO. Большинство полей в ней самоочевидны, но поле dwParams нуждается в специальном упоминании.
Листинг 12-3. DD_FUNCS.H

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

"Debugging Applications" (Microsoft Press)

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

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Прототипы для всех функций подключения и код пролога/эпилога

- - - - - - - - - - - - - - - - - - - - - - - - - - - - -

#ifndef J3D_FUNCS_H 

#define _DD_FUNCS__H 

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

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

1. Функции, написанные на С, не имеют никакого контроля над тем, какие регистры используются или когда компилятор сохраняет исходные значения регистров. Отсутствие контроля над регистрами означает, что получение адреса возврата почти невозможно. Для проекта DeadlockDetection адрес возврата имеет решающее значение.

2. Я также хотел передавать параметры в обрабатывающую функцию расширяющей DLL без необходимости копировать большие объемы данных при каждом вызове функции.

3. Поскольку почти все функции подключения ведут себя одинаково, я установил общие переменные, необходимые во всех функциях.

4. Функции подключения не могут изменять возвращаемых значений

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

Основная функция подключения напоминает следующую функцию: 

HANDLE NAKEDDEF DD_OpenEventA ( DWORD dwDesiredAccess,

BOOL blnheritHandle , 

LPCSTR IpNarne )

{

// Любые локальные переменные функции должны быть специфицированы

// перед рабочим кодом.



// HOOKFN_PROLOG должен быть специфицирован сразу же после

// локальных переменных.

HOOKFN_PROLOG ();

// Включена ли регистрация типа функции?

if ( TRUE == DoLogging ( DDOPT_EVENT))

{

// Используйте макрос FILL_EVENTINFO, чтобы заполнить переменную 

// stEvtlnfo, которая объявлена в макросе HOOKFN_PROLOG. Все

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

 // переменные, помогающие стандартизировать их код.

 FILL_EVENTINFO ( eOpenEventA);

// НЕОБХОДИМО вызвать макрос REAL_FUNC_PRE_CALL перед вызовом

 // реальной функции, или если регистры ESI и EDI не будут 

// сохраняться во время вызова. 

REAL_FUNC_PRE_CALL ();

// Вызвать реальную функцию. Возвращаемое значение, сохраняемое

 //в ЕАХ, сохраняется как часть обработки REAL_FUNC_POST_CALL. 

OpenEventA ( dwDesiredAccess, blnheritHandle , IpName );

//Вы должны назвать макрос REAL_FUNC_POST_CALL после вызова 

// реальной функции. Значения регистров и последней ошибки 

// сохраняются как часть REAL_FUNC_POST_CALL. 

REAL_FUNC_POST_CALL ();

// Вызвать код регистрации для регистрации события. 

ProcessEvent ( sstEvtlnfo); 

}

else 

{

// См. комментарии выше. Предложение else обрабатывает случай,

 // когда функция не зарегистрирована.

 REAL_FUNC_PRE_CALL (); 

OpenEventA ( dwDesiredAccess, blnheritHandle , IpName ); 

REAL_FUNC_POST_CALL (); 

}

// HOOKFN_EPILOG — последний макрос в функции. Его параметр — это

 // число параметров функции, так что стек будет очищен правильно.

// Макрос HOOKFN_EPILOG заботится также об установке во всех

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

// функция.

HOOKFN_EPILOG ( 3) ; 

}

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

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

Структура состояния регистра. Я использую эту структуру, чтобы гарантировать, что ВСЕ регистры возвращаются точно в том состоянии, в каком они прибыли из реальной функции. 



Заметьте, что регистры ЕВР и ESP обрабатываются как часть пролога.

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

typedef struct tag__REGSTATE 

{

DWORD dwEAX;

DWORD dwEBX;

DWORD dwECX;

DWORD dwEDX;

DWORD dwEDI;

DWORD dwESI;

DWORD dwEFL;

 } REGSTATE, * PREGSTATE;

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

Общий код пролога для всех DD_*-функций

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

 #define HOOKFN_PROLOG() \ 

/* Все функции подключения автоматически получают три одинаковые */\ 

/* переменные. */\ 

DDEVENTINFO stEvtlnfo ; /* Информация о событиях для функции */\ 

DWORD dwLastError; /* Значение последней ошибки */\

 REGSTATE stRegState ; /* Состояние регистра */\ 

{ \ 

_asm PUSH EBP /* ЕВР всегда сохраняйте явно. */\ 

_asm MOV EBP, ESP /* Переместить стек */\ 

_asm MOV EAX, ESP /* Получить указатель стека для вычисления /\

/* адреса возврата и параметров. */\

_asm SUB ESP, _LOCAL_SIZE /* Сохранить пространство для локальных*/\

/ * переменных * / \ 

_asm ADD EAX, 04h + 04h /* Счет для PUSH EBP и адреса возврата. */\

/* Сохранить начало параметров в стеке. */\

_asm MOV [stEvtlnfo.dwParams], EAX \ 

_asm SUB EAX, 04h /* Вернуться к адресу возврата. */\ 

_asm MOV EAX, [EAX] /* EAX теперь содержит адрес возврата. */\

/* Сохранить адрес возврата. */\

_asm MOV [stEvtlnfo.dwAddr], EAX. \

 _asm MOV dwLastError, 0 /* Инициализировать dwLastError. */\

/* Инициализировать информацию событий. */\

_asm MOV [stEvtlnfo.eFunc], eUNINITIALIZEDFE \

 _asm MOV [stRegState.dwEDI], EDI /* Сохранить два регистра /\

 _asm MOV [stRegState.dwESI], ESI /* на время вызовов функций. */\ 

}

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

Общий код эпилога для всех 00_*-функций. INumParams -число параметров функции, которая используется для восстановления стека после вызова подключения.



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

 #define HOOKFN_EPILOG(iNumParams) \ 

{ \ 

SetLastError ( dwLastError); /* Установить значение последней */\

/* ошибки реальной функции. */\

 _asm ADD ESP, _LOCAL_SIZE /* Добавить размер локальных */\

/* переменных. */\

_asm MOV EBX, [StRegState.dwEBX]/* Восстановить все регистры так, */\

 _asm MOV ECX, [stRegState.dwECX]/* чтобы данный вызов выглядел */\

 _asm MOV EDX, [stRegState.dwEDX]/* идентично перехваченной */\ 

_asm MOV EDI, [stRegState.dwEDI]/*функции. */\

 _asm MOV ESI, [StRegState.dwESI] \ 

_asm MOV EAX, [StRegState.dwEFL] \ 

_asm SAHF . \ 

_asm MOV EAX, [stRegState.dwEAX] \ 

_asm MOV ESP, EBP /* Передвинуть назад ESP. */\

_asm POP EBP /* Восстановить сохраненный EBP. */\ 

_asm RET iNumParams * 4 /* stdcall восстановление стека */\ 

}

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

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

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

 #define REAL_FUNC_PRE_CALL() \ 

{ \ 

_asm MOV EDI, [stRegState.dwEDI] /* Восстановить реальный EDI. /\

 _asm MOV ESI, [stRegState.dwESI] /* Восстановить реальный ESI. */\ 



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

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

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

 #define REAL_FUNC_POST_CALL(} \ 



{ \ 

_asm MOV [stRegState.dwEAX], EAX /* Сохранить значение EAX. */\ 

_asm MOV [stRegState.dwEBX], EBX /* Сохранить значение ЕВХ. */\

_asm MOV [stRegState.dwECX], ECX /* Сохранить значение ЕСХ. */\ 

_asm MOV [stRegState.dwEDX], EDX /* Сохранить значение EDX. */\

 _asm MOV [stRegState.dwEDI], EDI /* Сохранить значение EDI. */\ 

_asm MOV [stRegState.dwESI], ESI /* Сохранить значение ESI. */\

 _asm XOR . EAX, EAX /* Обнулить ЕАХ. */\ 

_asm LAHF /* в АН.*/\

_asm MOV [stRegState.dwEFL], EAX /* Загрузить значение флажков. */\ 

} \ 

dwLastError = GetLastError (); /* Сохранить значение последней */\

/* ошибки. */\

{ \ 

_asm MOV EAX, [stRegState.dwEAX] /* Восстановить ЕАХ */\

/* к его исходному значению. */\ 

/* Установить возвращаемое */\ 

/* значение для информации. */\

_asm MOV [stEvtlnfo.dwRetValue] , ЕАХ \ 

}

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

Удобный макрос для заполнения информационной структуры события

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

 #define FILL__EVENTINFO(eFn) \

stEvtlnfo.eFunc = eFn ; \

stEvtlnfo.ePrePost = ePostCall; \

stEvtlnfo.dwThreadld = GetCurrentThreadld ()

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

Объявление для всех DD_*-определений

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

#define NAKEDDEF _declspec(naked)

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

БОЛЬШОЕ ПРИМЕЧАНИЕ

Все следующие прототипы похожи на cdecl-функции. Но это не так — все они stdcall-функции! Заказной пролог и эпилог гарантируют, что используется правильное соглашение о вызовах!

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

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

// Обязательные функции, которые должны быть перехвачены, чтобы выполнить

// системную работу

HMODULE DD_LoadLibraryA ( LPCSTR IpLibFileName);



HMODULE DD_LoadLibraryW ( LPCWSTR IpLibFileName);

HMODULE DD_LoadLibraryExA ( LPCSTR IpLibFileName,

HANDLE hFile , 

DWORD dwFlags );

HMODULE DD_LoadLibraryExW ( LPCWSTR IpLibFileName,

HANDLE hFile , DWORD dwFlags );

 VOID DD_ExitPr

FARPROC DD_GetProcAddress ( HMODULE hModule, LPCSTR IpProcName);

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

Функции, специфические для потоков

HANDLE DD_CreateThread (LPSECURITY_ATTRIBUTES IpThreadAttributes ,

DWORD dwStackSize , 

LPTHREAD_START_ROUTINE IpStartAddress ,

 LPVOID IpParameter , 

DWORD dwCreationFlags ,

 LPDWORD IpThreadld );

 VOID DD_ExitThread ( DWORD dwExitCode); 

DWORD DD_SuspendThread ( HANDLE hThread);

 DWORD DD_ResumeThread ( HANDLE hThread);

BOOL DDjrerminateThread ( HANDLE hThread, DWORD dwExitCode);

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

Ожидание и специальные функции

DWORD DD_WaitForSingleObject ( HANDLE hHandle ,

DWORD dwMilliseconds );

DWORD DD_WaitForSingleObjectEx ( HANDLE hHandle ,

DWORD dwMilliseconds, 

BOOL bAlertable );

DWORD DD_WaitForMultipleObjects( DWORD nCount ,

CONST HANDLE * IpHandles , 

BOOL bWaitAll , 

DWORD dwMilliseconds );

DWORD DD_WaitForMultipleObjectsEx( DWORD nCount ,

CONST HANDLE * IpHandles ,

BOOL bWaitAll ,

 DWORD dwMilliseconds,

BOOL bAlertable };

DWORD DD_MsgWaitForMultipleObjects ( DWORD nCount ,

LPHANDLE pHandles , 

BOOL fWaitAll ,

 DWORD dwMilliseconds,

 DWORD dwWakeMask );

DWORD DD_MsgWaitForMultipleObjectsEx ( DWORD nCount ,

LPHANDLE pHandles , 

DWORD dwMilliseconds , 

DWORD dwWakeMask , 

DWORD dwFlags );

DWORD DD_SignalObjectAndWait ( HANDLE hObjectToSignal,

HANDLE hObjectToWaitOn, 

DWORD dwMilliseconds ,

 BOOL bAlertable );

BOOL DD_CloseHandle ( HANDLE hObject);

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

// Функции критической секции

VOID DD_InitializeCriticalSection(LPCRITICAL_SECTION IpCriticalSection);



BOOL DD_InitializeCriticalSectionAndSpinCount (

LPCRITICAL_SECTION IpCriticalSection, 

DWORD dwSpinCount );

VOID DD_DeleteCriticalSection(LPCRITICAL_SECTION IpCriticalSection); 

VOID DD_EnterCriticalSection ( LPCRITICAL_SECTION IpCriticalSection); 

VOID DD_LeaveCriticalSection ( LPCRITICAL_SECTION IpCriticalSection); 

DWORD DD__SetCriticalSectionSpinCount (

LPCRITICAL_SECTION IpCriticalSection, 

DWORD dwSpinCount );

BOOL DD_TryEnterCriticalSection ( LPCRITICAL_SECTION IpCriticalSection); 

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

// Функции мьютекса

HANDLE DD_CreateMutexA ( LPSECURITY_ATTRIBUTES IpMutexAttributes,

BOOL blnitialOwner , 

LPCSTR IpName ) ;

HANDLE DD_CreateMutexW ( LPSECURITY_ATTRIBUTES IpMutexAttributes,

BOOL blnitialOwner ,

 LPCWSTR IpName );

 HANDLE DD_OpenMutexA ( DWORD dwDesiredAccess,

BOOL blnheritHandle ,

 LPCSTR IpName ) ;

HANDLE DD_OpenMutexW ( DWORD dwDesiredAccess,

BOOL blnheritHandle , 

LPCWSTR IpName ); 

BOOL DD_ReleaseMutex ( HANDLE hMutex);

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

 // Функции семафора

DD_CreateSemaphoreA ( LPSECURITY_ATTRIBUTES IpSemaphoreAttributes,

LONG UnitialCount , 

LONG IMaximumCount , 

LPCSTR IpName ); 

HANDLE

DD_CreateSemaphoreW ( LPSECURITY_ATTRIBUTES IpSemaphoreAttributes,

LONG UnitialCount , 

LONG IMaximumCount , 

LPCWSTR IpName );

HANDLE DD_OpenSemaphoreA ( DWORD dwDesiredAccess,

BOOL blnheritHandle , 

LPCSTR IpNaroe );

HANDLE DD_OpenSemaphoreW ( DWORD dwDesiredAccess,

BOOL blnheritHandle , 

LPCWSTR IpNarae );

BOOL DD_ReleaseSemaphore ( HANDLE hSemaphore ,

LONG IReleaseCount ,

 LPLONG IpPreviousCount );

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

// Функции событий

HANDLE DD__CreateEventA ( LPSECURITY_ATTRIBUTES IpEventAttributes,

BOOL bManualReset ,

 BOOL blnitialState , 



LPCSTR IpName );

HANDLE DD_CreateEventW ( LPSECURITY_ATTRIBUTES IpEventAttributes,

BOOL bManualReset ,

 BOOL blnitialState , 

LPCWSTR IpName ); 

HANDLE DD_OpenEventA ( DWORD dwDesiredAccess,

BOOL blnheritHandle , 

LPCSTR IpName );

HANDLE DDjOpenEventW ( DWORD dwDesiredAccess,

BOOL blnheritHandle , LPCWSTR IpName ); 

BOOL DD_PulseEvent ( HANDLE hEvent); 

BOOL DD_ResetEvent ( HANDLE hEvent); 

BOOL DD_SetEvent ( HANDLE hEvent); 

#endif // _DD_FUNCS_H

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

Во-вторых, DeadlockDetection подключает функции вне DLL, когда подключается к программе через функцию LoadLibrary. Однако она может получить управление только после того, как выполнилась функция DllMain этой DLL, поэтому если какие-нибудь объекты синхронизации создаются или используются во время выполнения DllMain, то DeadlockDetection может пропустить их.

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

Я подключаю ExitProcess, потому что, когда приложение заканчивается, необходимо отключить и завершить DeadlockDetection так, чтобы она не завершилась аварийно или не подвесила вашу программу. Поскольку не существует никакого способа управлять порядком разгрузки DLL во время завершения программы, можно легко попасть в ситуации, в которых DLL, связанная с DeadlockDetection (например, DeadDetExt), была бы разгружена раньше самой DeadlockDetection.К счастью, очень немногие разработчики организуют основное многопоточное управление после того, как приложение вызывает функцию ExitProcess.

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



Отказ от многопоточной организации


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

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



Подключение импортированных функций


Существует много способов подключения вызовов функций в программу. Довольно сложный способ заключается в следующем: нужно отыскать все CALL-инструкции вызовов и заменить указанный в них адрес одним из своих собственных. Этот подход сложен и чреват ошибками. К счастью, в случае DeadlockDetection функции, которые надо подключать, являются импортированными и их намного легче обрабатывать, чем CALL-инструкции.

Импортированной называют функцию, которая приходит из DLL. Например, когда программа вызывает функцию outputoebugstring, то она обращается к функции, которая постоянно находится в KERNEL32.DLL. Только начав заниматься программированием для Microsoft Win32, я думал, что вызов импортированной функции ведет себя точно так же, как вызов любой другой функции: инструкция CALL или инструкция ветвления передает управление на нужный адрес и начинает выполнять импортированную функцию. Единственным различием могло бы быть то, что в случае импортированной функции программный загрузчик операционной системы должен был бы пробежаться через выполняемый файл и подправить адреса вызовов так, чтобы учесть адрес загруженной в память DLL. Когда же я посмотрел, как на самом деле организовано обращение к импортированной функции, то был поражен простотой и красотой ее реализации.

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

Как же загрузчик сообщает вашему приложению, где можно найти импортированную функцию? Решение дьявольски остроумно. Подумав, куда направлены обращения к OutputDebugstring, вы скоро поймете, что каждое обращение должно направляться по одному и тому же адресу — адресу загрузки OutputDebugstring в памяти.
Конечно, ваше приложение не может знать этот адрес заранее, поэтому все вызовы OutputDebugstring направляются через единственный косвенный адрес. Когда программный загрузчик загружает выполняемый файл и связанные с ним DLL-файлы в память, загрузчик устанавливает этот косвенный адрес так, чтобы он соответствовал окончательному адресу загрузки OutputDebugstring. Компилятор заставляет эту косвенную адресацию работать, генерируя переход к косвенному адресу каждый раз, когда код вызывает импортированную функцию. Этот косвенный адрес сохраняется в секции импорта (.idata1) выполняемого файла. Если импорт выполняется через объявление _declspec(dllimport), то вместо косвенного перехода код выполняет косвенное обращение, экономя, таким образом, пару инструкций на вызове функции.

Idata — первый символ в этом имени (i) от англ, import (импорт). — Пер.

При подключении импортированной функции выполняются следующие операции: поиск секции импорта выполняемого файла, поиск адреса конкретной функции, которую вы хотите подключить, и затем запись адреса подключаемой функции на свое место (в коде). Хотя поиск и замена адресов функций могут показаться довольно трудоемким занятием, но тут уж ничего не поделать — так организован РЕ2-формат файлов в Win32.

РЕ - Portable Executable. - Пер.

В главе 10 своей превосходной книги "Секреты системного программирования Windows 95" (Matt Pietrek. System Programming Secrets, — IDG Books, 1995) Мэт Пьетрек описывает метод подключения импортированных функций. Код Мэта просто отыскивает секцию импорта модуля и, используя значение, возвращаемое из вызова функции GetProcAddress, организует циклический просмотр списка импортированных функций. Обнаружив нужную функцию, он перезаписывает адрес подключаемой функции на исходный адрес импортированной функции.

После выхода книги Мэта в этой методике произошло два небольших изменения. Во-первых, когда Мэт писал книгу, большинство разработчиков не объединяло секцию импорта с другими РЕ-секциями. Поэтому, если секция импорта находится в памяти, защищенной от записи (с атрибутом доступа "read-only"), то запись адреса подключения вызывает нарушение доступа.


Я решил эту проблему простой переустановкой значения атрибута доступа к виртуальной памяти на "read-write" (перед записью адреса подключаемой функции). Вторая проблема, справиться с которой немного труднее, появляется из-за того, что при работе под Windows 98 иногда не удается подключить импортированные функции.

При использовании DeadlockDetection желательно иметь возможность переадресовать поточные функции во время выполнения приложения, даже когда оно выполняется под отладчиком. Хотя можно предположить, что подключение функций при работе под отладчиком не должно вызывать особых проблем, это не так. При выполнении программы под Windows 2000 или под Windows 98 вне отладчика, когда вы вызываете GetProcAddress, чтобы найти адрес функции, и затем просматриваете секцию импорта, отыскивая этот адрес в ее списке, вы всегда будете находить данный адрес. Если же программа выполняется под отладчиком в Windows 98, то вызов GetProcAddress возвращает другой адрес — не тот, что при выполнении без отладчика. В этом случае GetProcAddress возвращает адрес "отладочного переходника" (debug thunk1) — специальной оболочки вокруг реального вызова.

Thunk — "переходник" (небольшая секция кода, выполняющая преобразование (напр., типов) или обеспечивающая вызов 32-разрядного кода из 16-разрядного и наоборот). Здесь речь идет о специальном отладочном переходнике, который используется при отладке приложения. — Пер.

Как сказано в главе 4, в операционной системе Windows 98 не реализовано "копирование-при-записи". Отладочный переходник, возвращаемый при выполнении под отладчиком, — это то средство, с помощью которого Windows 98 предохраняет отладчики от попытки входа в системные функции, расположенные выше 2 Гбайтной отметки памяти. В целом, отсутствие "копирования-при-записи" — небольшая проблема для большинства разработчиков. Она важна только для тех, кто пишет отладчики или хочет правильно подключать функции независимо от того, выполняются они под отладчиком или нет.



К счастью, получение реального адреса для импортированной функции не слишком сложная задача — требуется только немного больше работы, и нужно избегать вызовов функции GetProcAddress. Структура IMAGE_IMPORT_ DESCRIPTOR РЕ-файла, которая содержит всю информацию о функциях, импортированных из конкретной DLL, имеет указатели на два массива в выполняемом файле. Эти массивы называются таблицами адресов импорта (Import Address Tables — IAT) или, иногда, массивами данных переходников (thunk data arrays). Первый указатель ссылается на реальную IAT, которую программный загрузчик устанавливает, когда загружает выполняемый файл.

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

Листинг 12-2 показывает функцию HookimportedFunctionsByName, предназначенную для организации подключения импорта. В табл. 12.3 показаны и описаны все параметры этой функции. Желая сделать подключение максимально обобщенным, я побеспокоился о том, чтобы разрешить подключение множества функций, одновременно импортируемых из одной и той же DLL. Как видно из названия этой функции (HookimportedFunctionsByName), она подключает только функции, импортируемые по имени. В главе 14 обсуждается подключение функций, импортируемых по порядковому номеру (которое используется в утилите LIMODS).

Таблица 12.3. Описания параметров функции HookimportedFunctionsByName

Параметр

Описание

hModule

Модуль, в котором будет подключен импорт

szImportMod

Имя модуля, чьи функции импортируются

Count

Количество подключаемых функций. Этот параметр указывает размер массивов paHookArray И paOrigFuncs

paHookArray

Массив структур дескрипторов функций, который перечисляет, какие функции нужно подключать. Массив не должен быть упорядочен по szFunc-именам (хотя разумно хранить массив отсортированным в порядке имен функций, потому что в будущем можно было бы лучше организовать поиск). Кроме того, если конкретный рРгос-указатель равен NULL (пустой), то HookimportedFunctionsByName пропускает этот элемент. Структура каждого элемента в paHookArray проста: имя подключаемой функции и указатель на процедуру нового подключения. Поскольку у вас может появиться желание подключать или не подключать функции, HookimportedFunctionsByName возвращает все исходные адреса импортируемых функций

paOrigFuncs

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

NULL

pdwHooked

Возвращает число подключенных функций (в массиве paHookArray)






ЛИСТИНГ 12-2. Функция HookImportedFunctionsByNaine из файла 

                                 HOOKIMPORTEDFUNCTIONSBYNAME.CPP . 

BOOL BUGSUTIL_DLLINTERFACE _stdcall

HooklmportedFunctionsByName ( HMODULE hModule ,

LPCSTR szImportMod,

 UINT uiCount 

LPHOOKFUNCDESCA paHookArray, 

PROC * paOrigFuncs, 

LPDWORD pdwHooked )

 {

// Проверить параметры.

ASSERT ( FALSE == IsBadReadPtr ( hModule

sizeof ( IMAGE_DOS_HEADER) ));

ASSERT ( FALSE == IsBadStringPtr ( szImportMod, MAX_PATH)); 

ASSERT ( 0 != uiCount); 

ASSERT ( NULL != paHookArray);

 ASSERT ( FALSE == IsBadReadPtr ( paHookArray,

sizeof (HOOKFUNCDESC) * uiCount));

// В отладочных построениях выполнить тщательную проверку paHookArray.

 #ifdef _DEBUG

if ( NULL != paOrigFuncs)

 {

ASSERT ( FALSE == IsBadWritePtr ( paOrigFuncs,

sizeof ( PROC) * uiCount)); 

}

if ( NULL != pdwHooked)

 {

ASSERT ( FALSE == IsBadWritePtr ( pdwHooked, sizeof ( UINT)));

}

// Проверить каждый элемент массива подключения.

 {

for ( UINT i = 0; i < uiCount; i++) 

{

ASSERT ( NULL != paHookArray[ i ].szFunc );

ASSERT ( '\0' != *paHookArray[ i ].szFunc);

// If the function address isn't NULL, it is validated.

if ( NULL != paHookArray[ i ].pProc)

{

ASSERT ( FALSE == IsBadCodePtr ( paHookArray[i].pProc)); 

}

 } 



#endif

// Выполнить проверку ошибок параметров.  

if ( ( 0 == uiCount ) | | 

( NULL == szIinportMod ) | | 

( TRUE == IsBadReadPtr ( paHookArray,

sizeof ( HOOKFUNCDESC) * uiCount))) 

{

SetLastErrorEx ( ERROR_INVALID_PARAMETER, SLE_ERROR);

return ( FALSE);

 }

if ( ( NULL != paOrigFuncs) && 

( TRUE == IsBadWritePtr ( paOrigFuncs,

sizeof ( PROC) * uiCount)) ) 

{

SetLastErrorEx ( ERROR_INVALID_PARAMETER, SLE_ERROR);



return ( FALSE); 

}

 if ( ( NULL != pdwHooked) &&

( TRUE == IsBadWritePtr ( pdwHooked, sizeof ( UINT))) )

 {

SetLastErrorEx ( ERROR_INVALID_PARAMETER, SLE_ERROR);

return ( FALSE); 

}

// Это системная DLL, которую Windows 98 не

// разрешает загружать (из-за того, что адрес загрузки >2 Гбайт)? 

if ( ( FALSE == IsNT .()) && ( (DWORD)hModule >= 0x80000000)) 

{

SetLastErrorEx ( ERROR_INVALID_HANDLE, SLE_ERROR);

return ( FALSE); 

}

// Должен ли каждьй элемент массива подключений проверяться .

 //в выпускных построениях?

 if ( NULL != paOrigFuncs) 



// Установить все значения paOrigFuncs в NULL.

memset ( paOrigFuncs, NULL, sizeof ( PROC) * uiCount);

 }

if ( NULL != pdwHooked) 

{

// Установить число функций, подключенных к 0.

*pdwHooked = 0; 

}

// Получить специальный дескриптор импорта.

PIMAGE_IMPORT_DESCRIPTOR plmportDesc =

GetNamedlmportDescriptor ( hModule, szImportMod); 

if ( NULL == plmportDesc)

{

// Затребованный модуль не был импортирован.

 // Ошибку не возвращать.

 return ( TRUE); 

}

// Получить информацию об исходных переходниках для этого DLL. 

// Невозможно использовать информацию переходников, хранящуюся в 

// p!mportDesc->FirstThunk, т. к. загрузчик уже изменил этот массив

 // при установке всех импортов. Исходный переходник обеспечивает

 // доступ к именам функций.

 PIMAGE_THUNK_DATA pOrigThunk =

MakePtr ( PIMAGE_THUNK_DATA

hModule , 

plmportDesc-XDriginalFirstThunk );

// Получить массив p!mportDesc->FirstThunk, в котором я буду

// выполнять подключения и всю черную работу.

 PIMAGE_THUNK_DATA pRealThunk = MakePtr { PIMAGE_THUNK_DATA

hModule , pImportDesc->FirstThunk ); 

// Цикл поиска подключаемых функций,

 while ( NULL != pOrigThunk->ul.Function) 

{

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

// а не те, что импортируются по порядковому номеру.



 if ( IMAGE_ORDINAL_FLAG !=

( pOrigThunk->ul.Ordinal & IMAGE_ORDINAL_FLAG)) 

{

// Поиск имени данной импортируемой функции.

PIMAGE_IMPORT_BY_NAME pByName;

pByName = MakePtr ( PIMAGE_IMPORT_BY_NAME,

hModule ,

 pOrigThunk->ul.AddressOfData );

// Если имя начинается с NULL, то пропустить элемент,

 if ( '\0' == pByName->Name[ 0 ]) 

{

continue; 

}

// Определить, подключилась ли функция

 BOOL bDoHook = FALSE;

// Здесь можно рассмотреть возможность двоичного поиска. 

// Посмотреть, есть ли имя импортируемой функции в массиве

 // подключения. Обдумайте требования к paHookArray для 

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

// вать двоичный поиск, который увеличил бы скорость поиска.

 // Однако размер параметра uiCount, получаемого этой

// функцией, должен быть скорее небольшим для успешного

 // поиска по всему массиву paHookArray для каждой функции,

 // импортированной модулем szImportMod.

 for ( DINT i = 0; i < uiCount; i++) 

{

if ( ( paHookArray[i].szFunc[0] ==

pByName->Name [0]) & & 

( 0 == strcmpi ( paHookArray[i].szFunc,

(char*)pByName->Name ) ) )

 {

// Если адрес функции есть NULL, то — немедленный

 // выход; иначе приступить к подключению функции.

 if ( NULL != paHookArray[ i ].pProc) 

{

bDoHook = TRUE; 

}

break; 

}

 }

if ( TRUE == bDoHook}

 {

// Функция для подключения найдена. Теперь нужно

 // изменить защиту памяти на "read-write" (для записи), 

// прежде чем перезаписывать указатели функций. Заметьте,

 // что в реальную область переходников запись 

//не производится!

MEMORY_BASIC_INFORMATION mbi_thunk;

 VirtualQuery ( pRealThunk , 

&mbi_thunk  ,

 sizeof ( MEMORY_BASIC_INFORMATION));

if ( FALSE — VirtualProtect ( mbi_thunk.BaseAddress,

mbi_thunk.RegionSize , 

PAGE_READWRITE , 



&mbi_thunk.Protect )) 

{

ASSERT ( !"VirtualProtect failed!");

 SetLastErrorEx ( ERROR_INVALID_HANDLE, SLE_ERROR);

 return ( FALSE); 

}

// Сохранить исходный адрес, если потребуется.

 if ( NULL != paOrigFuncs)

 {

paOrigFuncs[i] = (PROC)pRealThunk->ul.Function; 

}

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

 // РIМАСЕ_ТНЦМК_ОАТА-полей для будущей поддержки Win64.

 // Используем самый последний набор заголовков

 // из W2K RC2 Platform SDK, и заставим иметь с ними дело

 // заголовки из Visual C++ 6 Service Pack 3. 

// Подключить функцию.

DWORD * pTemp = (DWORD*)&pRealThunk->ul.Function;

 *pTemp = (DWORD)(paHookArray[i].pProc); 

DWORD dwOldProtect;

// Изменить защиту обратно к тому состоянию, которое 

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

VERIFY ( VirtualProtect ( mbi_thunk.BaseAddress,

mbi_thunk.RegionSize , mbi_thunk.Protect , SdwOldProtect )) ;

 if ( NULL != pdwHooked)

 {

// Инкремент общего количества подключенных функций.

 *pdwHooked += 1; 

}

 }

}

// Инкремент обеих таблиц. pOrigThunk++; pRealThunk++; 

}

// Все OK, Jumpmaster! 

SetLastError ( ERROR_SUCCESS);

 return ( TRUE); 

}

Уточнить принципы работы HooklmportedFunctionsByName не так  уж трудно. После выполнения обычной для практики профилактической отладки полной проверки (макросами утверждений) каждого параметра вызывается вспомогательная функция GetNamedimportDescriptor, чтобы найти структуру IMAGE_IMPORT_DESCRIPTOR для требуемого модуля. После получения указателей на исходную и реальную IAT-таблицы выполняется циклический просмотр исходной IAT-таблицы, проверяется каждая импортированная по имени функция, присутствует ли она в списке подключения paHookArray. Если функция находится в этом списке, то устанавливается атрибут доступа PAGE_READWRITE для памяти реальной  IAT-таблицы, чтобы благополучно записать в нее адрес подключения, затем этот адрес заносится в ячейку указателя соответствующей реальной функции и защита памяти восстанавливается в ее исходное состояние.Если вы не совсем разобрались в том, что происходит, воспользуйтесь функцией блочного тестирования для пошагового выполнения HookimportedFunctionsByName (эта функция включена в исходный код BUGSLAYERUTIL.DLL на сопровождающем компакт-диске).

Теперь, рассмотрев основные идеи подключения импортированных функций, перейдем к реализации остальной части DeadlockDetection.



Постоянный просмотр кода


Если приложение действительно нуждается в мультипоточности, то приходится тратить много времени на полный просмотр его кода. Данный прием заключается в том, чтобы для подобных обзоров прикрепить по одному человеку на каждый поток и на каждый объект синхронизации в коде. Во многих отношениях обзор кода в многопоточном программировании — действительно "многопоточный" обзор.

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

Работая с обзором кода, имейте в виду, что операционная система имеет свои собственные объекты синхронизации, которые она применяет к вашему процессу, и эти объекты также могут вызвать блокировки. Критическая секция процесса, объясненная ниже в примечании "История отладочной войны. Блокировка не имеет смысла" данной главы, и печально известные 16-разрядные мьютексы из Windows 98 являются объектами синхронизации, которые операционная система использует в вашем процессе. Если не проявлять осторожности, то причиной блокировок может также быть и обработка сообщений. Если поток А — это UI-поток, ожидающий критическую секцию, в настоящее время принадлежащую потоку В, и если поток В посылает сообщение HWND-объекту в поток А с помощью функции SendMessage, то произойдет блокировка. Убедитесь, что контролировали эти действия во время обзора кода.

Мьютекс (miitex object) — специальный синхронизирующий объект в межпроцессном взаимодействии, подающий сигнал, когда он не захвачен каким-либо потоком. — Пер.



Реализация DeadlockDetection


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



Реализация потоков мелкими дискретными частями


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



Рекомендации и приемы работы с многопоточностью


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

 реализуйте потоки только мелкими дискретными частями;   синхронизируйте потоки на самом низком уровне;  постоянно просматривайте код;  выполняйте тестирование на многопроцессорных машинах.



Многопоточное программирование весьма трудное занятие.



Многопоточное программирование весьма трудное занятие. Это область, в которой вы столкнетесь с некоторыми из самых трудных ошибок. В данной главе представлена методика и рекомендации, которые должны помочь разработчику избежать блокировок с самого начала проекта. Как подчеркнуто ранее в этой главе, в многопоточном программировании жизненно важно предварительное планирование, и нужно добиться того, чтобы команде разработчиков было предоставлено достаточно времени и ресурсов для тщательного и правильного планирования многопоточного приложения. Однако, если вы столкнетесь с неизбежными многопоточными блокировками, не поддавайтесь панике: в этой главе представлена утилита DeadlockDetection, которая позволит вычислить, какие потоки заблокированы и на каком объекте синхронизации.
Наконец, — и я не могу не подчеркнуть этот момент, — если вы занимаетесь многопоточным программированием, то нужно разрабатывать, выполнять и тестировать свои проекты на мультипроцессорных компьютерах. Если вы этого не сделаете, то, вероятно, не должны заниматься многопоточным программированием, потому что остаетесь незащищенными от некоторых чрезвычайно серьезных ошибок.

Синхронизация потоков на самом низком уровне


Помещайте методы синхронизации на самом низком уровне кода. Если в программе нужна критическая секция для защиты части данных, то необходимо помещать функции EnterCriticalSection И LeaveCriticalSection лишь вокруг данных фактического доступа. Это размещение гарантирует, что действительно защищается только тот элемент, который требуется защищать, и ничего более. Ограничение области видимости объектов синхронизации — лучшая защита от случайных блокировок. Одной из самых неприятных блокировок, с которыми я когда-либо встречался, был захват синхронизации объекта двумя указанными выше функциями.



Тестирование на мультипроцессорных машинах


Многопоточное приложение предъявляет к тестированию намного более высокие требования, чем однопоточное. Самая важная рекомендация для тестирования многопоточного приложения — необходимо выполнять тестирование на мультипроцессорных системах. При этом не имеется в виду простое выполнение приложения по частям; речь идет о непрерывном тестировании программы во всех возможных сценариях. Даже если приложение отлично выполняется на однопроцессорных машинах, мультипроцессорная машина выявляет такие блокировки, которые никогда не казались возможными. 

Лучший подход к этому виду тестирования состоит в том, чтобы иметь в команде специальных разработчиков, выполняющих приложение на мультипроцессорных машинах каждый день. Если вы менеджер и не имеете никакой мультипроцессорной машины в своем распоряжении, прекращайте чтение прямо сейчас и немедленно снабдите половину разработчиков и QA-испытателей мультипроцессорными машинами! Если вы — разработчик без мультипроцессорной машины, покажите эту главу своему менеджеру и потребуйте для работы надлежащее оборудование!



Требования утилиты DeadlockDetection


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

Вот список основных требований, учтенных при разработке DeadlockDetection:

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

2. Утилита должна показывать, какой объект синхронизации стал причиной блокировки.

3. Утилита должна показывать, какая Windows-функция блокирована и какие параметры ей переданы. Это помогает увидеть как значения тайм-аута, так и значения параметров, переданных в функцию.

4. Утилита должна определить, какой поток вызвал блокировку.

5. Утилита должна быть достаточно "легковесной", т. е. как можно меньше вмешиваться в работу программы пользователя.

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

7. Утилита должна легко объединяться с программами пользователя.

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



История отладочной войны Блокировка

Сражение

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

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

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

Результат

Всегда с удовольствием вспоминаю эту ситуацию, потому что это был один из тех немногих случаев, когда уже через 5 минут после запуска отладчика я стал похож на героя. Как только команда дублировала блокировку, я быстро взглянул на окно Call Stack и заметил, что программа ожидала на дескрипторе потока внутри функции DllMain. Когда загружается некоторая DLL, эта функция, являясь частью архитектуры DLL, стартует другой поток и затем немедленно вызывает функцию WaitForSingleObject из объекта события подтверждения приема, чтобы гарантировать, что порожденный поток способен должным образом инициализировать некоторые важные разделяемые объекты перед продолжением остальной части обработки в DllMain.

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


Операционная система использует эту секцию, чтобы синхронизировать различные действия, которые случаются "за сценой" процесса. Одной из ситуаций, в которых используется критическая секция, является сериализация1выполнения DllMain для следующих четырех случаев ее вызова: DLL_PROCESS_ATTACH (присоединение DLL-процесса), DLL_THREAD_ATTACH (присоединение DLL-потока), DLL_THREAD_ DETACH (отсоединение DLL-потока) и DLL_PROCESS_DETACH (отсоединение DLL-процесса). Причину обращения к DliMain указывает ее второй параметр.

 Сериализация — преобразование в последовательную форму. — Пер.

В приложении, над которым работала команда, запрос к LoadLibrary заставил операционную систему захватить критическую секцию процесса для того, чтобы вызывать DliMain для случая DLL_PROCESS_ATTACH. Затем функция DliMain порождала второй поток. Всякий раз, когда процесс порождает новый поток, операционная система захватывает критическую секцию процесса так, чтобы она могла вызывать функцию DliMain каждой загружаемой DLL для случая DLLJTHREAEKATTACH. В этой конкретной программе второй поток блокировался, потому что критическую секцию процесса содержал первый поток. К сожалению, первый поток затем вызывал функцию WaitForSingleObject, чтобы гарантировать, что второй поток способен должным образом инициализировать некоторые разделяемые объекты. Поскольку второй поток был блокирован на критической секции процесса, удерживаемой первым потоком, а первый поток блокирован при ожидании второго потока, результатом была обычная взаимоблокировка.

Урок

Очевидный урок таков: необходимо избегать любых вызовов ожидающих (Wait*-) функций внутри DliMain. Однако проблемы с критической секцией процесса касаются не только Юа11:*-функций. Операционная система использует критическую секцию процесса и при вызове других фунций (CreateProcess, GetModuleFileName, GetProcAddress, LoadLibrary И FreeLibrary), так ЧТО не нужно вызывать любую из этих функций в DliMain. Поскольку DliMain обзаводится критической секцией процесса, то она может одновременно выполнять только один поток.

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