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

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

.pdf
Скачиваний:
6269
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

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++