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

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

.pdf
Скачиваний:
6823
Добавлен:
13.08.2013
Размер:
31 Мб
Скачать
☆

648 Часть IV. Динамически подключаемые библиотеки

Рис. 20-3. Операции, выполняемые системой при вызове потоком функции FreeLibrary

Уведомление DLL_THREAD_ATTACH

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

DLL_THREAD_ATTACH.

Если в момент проецирования DLL на адресное пространство процесса в нем выполняется несколько потоков, система не вызывает DllMain со значе-

Глава 20. DLL - более сложные методы программирования.docx 649

нием DLL_YHREAD_ATTACH ни для одного из существующих потоков. Вызов DllMain с этим значением осуществляется, только если DLL проецируется на адресное пространство процесса в момент создания потока.

Обратите также внимание, что система не вызывает функции DllMain со значением DLL_THREAD_ATTACH и для первичного потока процесса. Любая DLL, проецируемая на адресное пространство процесса в момент его создания, получа-

ет уведомление DLL_PROCESS_ATTACH, а не DLL_THREAD_ATTACH.

Уведомление DLL_THREAD_DETACH

Лучший способ завершить поток — дождаться возврата из его стартовой функции, после чего система вызовет ExitThread и закроет поток. Эта функция лишь сообщает системе о том, что поток хочет завершиться, но система не уничтожает его немедленно. Сначала она просматривает все проекции DLL, находящиеся в данный момент в адресном пространстве процесса, и заставляет завершаемый поток вызвать DllMain в каждой из этих DLL со значением DLL_THREAD_DETACH. Тем самым она уведомляет DLL-модули о необходимости очистки, связанной с данным потоком. Например, DLL-версия библиотеки С/С++ освобождает блок данных, используемый для управления многопоточными приложениями.

Заметьте, что DLL может не дать потоку завершиться. Например, такое возможно, когда функция DllMain, получив уведомление DLL_THREAD_ DETACH, входит в бесконечный цикл. А операционная система закрывает поток только после того, как все DLL заканчивают обработку этого уведомления.

Примечание. Если поток завершается из-за того, что другой поток вызвал для него TerminateThread, система не вызывает DllMain со значением DLL_THREAD_DETACH. Следовательно, ни одна DLL, спроецированная на адресное пространство процесса, не получит шанса на выполнение очистки до завершения потока, что может привести к потере данных. Поэтому

TerminateThread, как и TerminateProcess, можно использовать лишь в самом крайнем случае!

Если при отключении DLL еще выполняются какие-то потоки, то для них DllMain не вызывается со значением DLL_THREAD_DETACH. Вы можете проверить это при обработке DLL_PROCESS_DETACH и провести необходимую очистку.

Ввиду упомянутых выше правил не исключена такая ситуация: поток вызывает LoadLibrary для загрузки DLL, в результате чего система вызывает из этой библиотеки DllMain со значением DLL_PROCESS_ATTACH. (В этом случае уведомление DLL_THREAD_ATTACH не посылается.) Затем лоток, загрузивший DLL, завершается, что приводит к новому вызову DllMain — на этот раз со значением DLL_THREAD_DETACH. Библиотека уведомля-

650 Часть IV. Динамически подключаемые библиотеки

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

Как система упорядочивает вызовы DllMain

Система упорядочивает вызовы функции DllMain. Чтобы понять, что я имею в виду, рассмотрим следующий сценарий. Процесс A имеет два потока: А и В. На его адресное пространство проецируется DLL-модуль SomeDLLdll. Оба потока собираются вызвать CreateThread, чтобы создать еще два потока: С и D.

Когда поток A вызывает для создания потока С функцию CreateThread, систе-

ма обращается к DllMain из SomeDLL.dll со значением DLL_THREAD_ ATTACH.

Пока поток С исполняет код DllMain, поток В вызывает CreateThread для создания потока D. Системе нужно вновь обратиться к DllMain со значением DLL_THREAD_ATTACH, и на этот раз код функции должен выполнять поток D. Но система упорядочивает вызовы DllMain, и поэтому приостановит выполнение потока D, пока поток С не завершит обработку кода DllMain и не выйдет из этой функции.

Закончив выполнение DllMain, поток С может начать выполнение своей функции потока. Теперь система возобновляет поток D и позволяет ему выполнить код DllMain, при возврате из которой он начнет обработку собственной функции потока.

Обычно никто и не задумывается над тем, что вызовы DllMain упорядочиваются. Но я завел об этом разговор потому, что один мой коллега как-то раз написал код, в котором была ошибка, связанная именно с упорядочиванием вызовов DllMain. Его код выглядел примерно так:

BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) {

HANDLE hThread;

DWORD dwThreadId;

switch (fdwReason) { case DLL_PROCESS_ATTACH:

//DLL проецируется на адресное пространство процесса

//создаем поток для выполнения какой-то работы hThread = CreateThread(NULL, 0, SomeFunction, NULL,

0, &dwThreadId);

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

WaitForSingleObject(hThread, INFINITE);

Глава 20. DLL - более сложные методы программирования.docx 651

// доступ к новому потоку больше не нужен

CloseHandle(hThread);

break;

case DLL_THREAD_ATTACH:

// создается еще один поток break;

case DLL_THREAD_DETACH:

// поток завершается корректно break;

case DLL_PROCESS_DETACH:

// DLL выгружается из адресного пространства процесса break;

}

return(TRUE);

}

Нашли «жучка»? Мы-то его искали несколько часов. Когда DllMain получает уведомление DLL_PROCESS_ATTACH, создается новый поток. Системе нужно вновь вызвать эту же DllMain со значением DLL_THREAD_ATTACH. Но выполнение нового потока приостанавливается — ведь поток, из-за которого в DllMain было отправлено уведомление DLL_PROCESS_ATTACH. свою работу еще не закончил. Проблема кроется в вызове WaitForSingleObject. Она приостанавливает выполнение текущего потока до тех пор, пока не завершится новый. Однако у нового потока нет ни единого шанса не только на завершение, но и на выполнение хоть какого-нибудь кода — он приостановлен в ожидании того, когда текущий поток выйдет из DllMain. Вот вам и взаимная блокировка — выполнение обоих потоков задержано навеки!

Впервые начав размышлять над этой проблемой, я обнаружил функцию DisableThreadLibraryCalls:

BOOL DisableThreadLibraryCalls(HMODULE hInstDll);

Вызывая ее, вы сообщаете системе, что уведомления DLL_THREAD_ATTACH и DLL_THREAD_DETACH не должны посылаться DllMain той библиотеки, которая указана в вызове. Мне показалось логичным, что взаимной блокировки не будет, если система не станет посылать DLL уведомления. Но, проверив свое решение (см. ниже), я убедился, что это не выход.

BOOL WINAPI DllMain(HINSTANCE hInstDll, DW0RD fdwReason, PVOID fImpLoad) {

HANDLE hThread;

DWORD dwThreadId;

switch (fdwReason) { case DLL_PROCESS_ATTACH:

652Часть IV. Динамически подключаемые библиотеки

//DLL проецируется на адресное пространство процесса

//предотвращаем вызов DllMain при создании

//или завершении потока

DisableThreadLibraryCalls(hInstDll);

//создаем лоток для выполнения какой-то работы hThread = CreateThread(NULL, 0, SomeFunction, NULL,

0, &dwThreadId);

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

WaitForSingleObject(hThread, INFINITE);

//доступ к новому потоку больше не нужен

CloseHandle(hThread);

break;

case DLL_THREAD_ATTACH:

// создается еще один поток break;

case DLL_THREAD_DETACH:

// поток завершается корректно break;

case DLL_PROCESS_DETACH:

// DLL выгружается из адресного пространства процесса break;

}

return(TRUE);

}

Потом я понял, в чем дело. Создавая процесс, система создает и объектмьютекс. У каждого процесса свой объект-мьютекс — он не разделяется между несколькими процессами. Его назначение — синхронизация всех потоков процесса при вызове ими функций DllMain из DLL, спроецированных на адресное пространство данного процесса.

Когда вызывается CreateThread, система создает сначала объект ядра «поток» и стек потока, затем обращается к WaitForSingteObject, передавая ей описатель объекта-мьютекса данного процесса. Как только поток захватит этот мьютекс, система заставит его вызвать DllMain из каждой DLL со значением DLL_THREAD_ATTACH. И лишь тогда система вызовет ReleaseMutex, чтобы освободить объект-мьютекс. Вот из-за того, что система работает именно так, дополнительный вызов DisableThreadLibraryCalls и не предотвращает взаимной блокировки потоков. Единственное, что я смог придумать, — переделать эту часть исходного кода так, чтобы ни одна DllMain не вызывала WaitForSingleObject.

Глава 20. DLL - более сложные методы программирования.docx 653

Функция DllMain и библиотека С/С++

Рассматривая функцию DllMain в предыдущих разделах, я подразумевал, что для сборки DLL вы используете компилятор Microsoft Visual С++. Весьма вероятно, что при написании DLL вам понадобится поддержка со стороны стартового кода из библиотеки С/С++. Например, в DLL есть глобальная переменная — экземпляр какого-то С++-класса. Прежде чем DLL сможет безопасно ее использовать, для переменной нужно вызвать се конструктор, а это работа стартового кода.

При сборке DLL компоновщик встраивает в конечный файл адрес DLLфункции входа/выхода. Вы задаете этот адрес компоновщику ключом / ENTRY. Если у вас компоновщик Microsoft и вы указали ключ /DLL, то по умолчанию он считает, что функция входа/выхода называется _DllMainCRTStartup. Эта функция содержится в библиотеке С/С++ и при компоновке статически подключается к вашей DLL—даже если вы используете DLL-версию библиотеки С/С++.

Когда DLL проецируется на адресное пространство процесса, система на самом деле вызывает именно _DllMainCRTStartup, а не вашу функцию DllMain. Получив уведомление DLL_PROCESS_ATTACH, функция _DllMainCRTStartup инициализирует библиотеку С/С++ и конструирует все глобальные и статические С++-объекты. Закончив, _DllMainCRTStartup вызывает вашу DllMain. Перед на-

правлением уведомлений __DllMainCRTStartup функция _DllMainCRTStartup об-

рабатывает все уведомления DLL_PROCESS_ATTACH согласно параметрам, заданным ключом /GS.

Как только DLL получает уведомление DLL_PROCESS_DETACH, система вновь вызывает _DllMainCRTStartup, которая теперь обращается к вашей функции DllMain, и, когда та вернет управление, _DllMainCRTStartup вызовет деструкторы для всех глобальных и статических С++-объектов. Получив уведомление

DLL_THREAD_ATTACH или DLL_THREAD_DETACH, функция

_DllMainCRTStartup не делает ничего особенного.

Я уже говорил, что реализовать в коде вашей DLL функцию DllMain не обязательно. Если у вас нет этой функции, библиотека С/С++ использует свою реализацию DllMain, которая выглядит примерно гак (если вы связываете DLL со статической библиотекой С/С++):

BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason. PVOID fImpLoad) {

if (fdwReason == DLL_PROCESS_ATTACH) DisableThreadLibraryCalls(hInstDll);

return(TRUE);

}

При сборке DLL компоновщик, не найдя в ваших OBJ-файлах функцию DllMain, подключит DllMain из библиотеки С/С++. Если вы не предоставили свою версию функции DllMain, библиотека С/С++ вполне справедливо будет считать, что вас не интересуют уведомления DLL_TH READ_ATTACH

654 Часть IV. Динамически подключаемые библиотеки

и DLL_THREAD_DETACH. Функция DisableThreadLibraryCalls вызывается для ускорения создания и разрушения потоков.

Отложенная загрузка DLL

Microsoft Visual С++ поддерживает отложенную загрузку DLL — новую, просто фантастическую функциональность, которая значительно упрощает работу с библиотеками. DLL отложенной загрузки (delay-load DLL) — это неявно связываемая DLL, которая не загружается до тех пор, пока ваш код не обратится к какомунибудь экспортируемому из нее идентификатору. Такие DLL могут быть полезны в следующих ситуациях.

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

■Если приложение использует какую-то новую функцию и вы пытаетесь запустить его в более старой версии операционной системы, в которой нет такой функции, загрузчик сообщает об ошибке и не дает запустить приложение. Вам нужно как-то обойти этот механизм и уже в период выполнения, выяснив, что приложение работает в старой версии системы, не вызывать новую функцию. Например, ваша программа должна в Windows Vista использовать новые функции пула потоков, а в прежних версиях Windows — старые функции пула. При инициализации программа должна вызвать GetVersionEx, чтобы определить версию текущей операционной системы, и после этого обращаться к соответствующим функциям. Попытка запуска этой программы в Windows XP может привести к тому, что загрузчик сообщит об ошибке, поскольку в этой системе нет нужных функций. Так вот, и эта проблема легко решается за счет DLL отложенной загрузки.

Ядовольно долго экспериментировал с DLL отложенной загрузки в Visual C++ и должен сказать, что Microsoft прекрасно справилась со своей задачей. DLL отложенной загрузки открывают массу дополнительных возможностей и корректно работают во всех версиях Windows. Однако, обратите внимание вот на что:

■отложенная загрузка DLL, экспортирующих поля, невозможна;

■невозможна отложенная загрузка модуля Kernel32.dll, так он необходим для вызова LoadLibrary и GetProcAddress;

■нельзя вызывать функции отложенной загрузки в DllMain, иначе возможен крах процесса.

■Подробнее об этом см. в статье «Constraints of Delay Loading DLLs» по ссылке http://msdn2.microsoft.com/en-us/library/yx1x886y(VS.80).aspx.

Глава 20. DLL - более сложные методы программирования.docx 655

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

■/Lib:DelayImp.lib

■/DelayLoad:MyDll.dll

Внимание! Нельзя задавать ключи /DELAYLOAD и /DELAY для компоновщика с помощью директивы #pragma comment(linker, “”). Их задают только через свойства проекта параметр Delay Loaded DLLs в меню Configuration Properties | Linker | Input:

Первый ключ заставляет компоновщик внедрить в EXE-модуль специальную функцию, __delayLoadHelper2, а второй — выполнить следующие операции:

■удалить MyDll.dll из раздела импорта исполняемого модуля, чтобы при инициализации процесса загрузчик операционной системы не пытался неявно связывать эту библиотеку с EXE-модулем;

■встроить в EXE-файл новый раздел отложенного импорта (.didata) со списком функций, импортируемых из MyDll.dll;

■в привести вызовы функций из DLL отложенной загрузки к вызовам

__delayLoadHelper2.

656 Часть IV. Динамически подключаемые библиотеки

При выполнении приложения вызов функции из DLL отложенной загрузки (далее для краткости — DLL-функции) фактически переадресуется к __delayLoadHelper2. Последняя, просмотрев раздел отложенного импорта, знает, что нужно вызывать LoadLibrary, а затем GetProcAddress. Получив адрес DLLфункции, __delayLoadHelper2 делает так, чтобы в дальнейшем эта DLL-функция вызывалась напрямую. Обратите внимание, что каждая функция в DLL настраивается индивидуально при первом ее вызове. Ключ /DelayLoad компоновщика указывается для каждой DLL, загрузку которой требуется отложить.

Вот собственно, и все. Как видите, ничего сложного здесь нет. Однако следует учесть некоторые тонкости. Загружая ваш EXE-файл, загрузчик операционной системы обычно пытается подключить требуемые DLL и при неудаче сообщает об ошибке. Но при инициализации процесса наличие DLL отложенной загрузки не проверяется. И если функция __delayLoadHelper2 уже в период выполнения не найдет нужную DLL, она возбудит программное исключение. Вы можете перехватить его, используя SEH, и как-то обработать. Если же вы этого не сделаете, ваш процесс будет закрыт. (О структурной обработке исключений см. главы 23,24

и 25.)

Еще одна проблема может возникнуть, когда __delayLoadHelper, найдя вашу DLL, не обнаружит в ней вызываемую функцию (например, загрузчик нашел старую версию DLL). В этом случае delayLoadHelver также возбудит программное исключение, и все пойдет по уже описанной схеме. В программе-примере, которая представлена в следующем разделе, я покажу, как написать SEH-код, обрабатывающий подобные ошибки. В ней же вы увидите и массу другого кода, не имеющего никакого отношения к SEH и обработке ошибок. Он использует дополнительные возможности (о них — чуть позже), предоставляемые механизмом поддержки DLL отложенной загрузки. Если эта более «продвинутая» функциональность вас не интересует, просто удалите дополнительный код.

Разработчики Visual С++ определили два кода программных исключений:

VcppException(ERROR_SEVERTTY_ERROR, ERROR_MOD_NOT_FOUND) и VcppExceptiontfRROR_SEVERTTY_ERROR, ERROR_PROC_NOT_FOUND). Они уведомляют соответственно об отсутствии DLL и DLL-функции. Моя функция фильтра исключений DelayLoadDllExceptionFilter реагирует на оба кода. При возникновении любого другого исключения она, как и положено корректно написанному фильтру, возвращает EXCEPTION_CONTINUE_SEARCH. (Программа не должна «глотать» исключения, которые не умеет обрабатывать.) Однако, если генерируется один из приведенных выше кодов, функция __delayLoadHelper2 предоставляет указатель на структуру DelayLoadInfo, содержащую некоторую дополнительную информацию. Она определена в заголовочном файле DelayImp.h, поставляемом с Visual C++.

Глава 20. DLL - более сложные методы программирования.docx 657

typedef struct DelayLoadInfo {

 

DWORD

cb;

// размер структуры

PCImgDelayDescr

pidd;

// "сырые" данные (все, что пока не обработано)

FARPROC*

ppfn;

// указатель на адрес функции, которую надо

LPCSTR

szDll;

// загрузить имя DLL

DelayLoadProc

dlp;

// имя или порядковый номер процедуры

HMODULE

hmodCur;

// hInstance загруженной библиотеки

FARPROC

pfnCur;

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

DWORD

dwLastError;

// код ошибки

} DelayLoadInfo, * PDelayLoadInfo;

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

typedef struct DelayLoadProc { BOOL fIroportByName;

union {

LPCSTR szProcName; DWORD dwOrdinal;

};

} DelayLoadProc;

Если DLL загружается, но требуемой функции в ней нет, вы можете проверить элемент hmodCur, в котором содержится адрес проекции этой DLL, и элемент dwLastError, в который помещается код ошибки, вызвавшей исключение. Однако для фильтра исключения код ошибки, видимо, не понадобится, поскольку код исключения и так информирует о том, что произошло. Элемент pfnCur содержит адрес DLL-функции, и фильтр исключения устанавливает его в NULL, так как само исключение говорит о том, что __delayLoadHelper2 не смогла найти этот адрес.

Что касается остальных элементов, то cb служит для определения версии системы, pidd указывает на раздел, встроенный в модуль и содержащий список DLL отложенной загрузки, а ppfn — это адрес, по которому вызывается функция, если она найдена в DLL. Последние два параметра используются внутри __delayLoadHelper2 и рассчитаны на очень «продвинутое» применение — крайне маловероятно, что они вам когда-нибудь понадобятся.

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

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