Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009
.pdf648 Часть 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 — подходящий кандидат на отложенную загрузку, поскольку она требуется только на время печати документа. Когда пользователь выбирает ко-