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

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

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

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

Если функции передан полный путь либо сетевой путь (например, C:\ Apps\Libraries\MyLibrary.dll или \\server\share\MyLibrary.dll), она пытается непосредственно загрузить заданный файл. Если этот файл не существует,

LoadLibraryEx

возвращает

NULL,

а

GetLastError —

ER-

ROR_MOD_NOT_FOUND, на чем поиск и заканчивается.

 

В противном случае путь поиска формируется конкатенацией значения pszDLLPathName с путями к перечисленным ниже папкам:

текущий каталог процесса;

системный каталог Windows;

каталог 16-разрядных системных компонентов, например Windows\System;

каталог Windows;

каталоги, указанные в переменной окружения PATH.

Заметьте, что знаки «.» и «…» в значении параметра pszDLLPathName учитываются при построении относительного пути для каждого этапа поиска. Так, если передан параметр TEXT("..\\MyLibrary.dir), LoadLibraryEx будет искать MyLibrary.dll в следующих папках:

папки в текущем каталоге;

папки системного каталога Windows (т.е. каталога Windows);

каталог 16-разрядных компонентов Windows; Q корневой каталог тома;

родительские папки и каталоги, указанные в переменной окруже-ния

PATH.

Поиск останавливается после загрузки искомой DLL

3.Если вы пишете программу, которая должна динамически загружать библиотеки из стандартных папок, вместо LoadLibraryEx с флагом

LOAD_WITH_ALTERED_SEARCH_PATH вызывайте функцию SetDllDirectory,

принимающую как параметр имя папки, в которой находится искомая библиотека. Эта функция использует LoadLibrary или LoadLibraryEx для поиска в следующих папках:

каталог приложения;

каталог, заданный с помощью SetDllDirectory;

системный каталог Windows (т.е. каталог «Windows»);

каталог 16-разрядных компонентов Windows;

корневой каталог тома;

родительские папки и каталоги, указанные в переменной окружения PATH. Этот алгоритм позволяет хранить файлы приложения и общие DLL в

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

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

строкой — TEXT(“”) — удаляет текущий каталог из пути поиска. Если вместо этого передать NULL, будет восстановлен алгоритм поиска по умолчанию. И последнее: GetDllDirectory возвращает текущее значение данного конкретного каталога.

LOAD_IGNORE_CODE_AUTHZ_LEVEL

Этот флаг отключает защитный механизм WinSafer (известный также как Software Restriction Policies или Safer), появившийся в Windows XP. Этот механизм контролирует права доступа кода во время исполнения (подробнее см. по ссылке http://technet.microsoft.com/en-us/windowsvista/aa940985.aspx), Windows Vista этот механизм заменен User Account Control (UAC), о котором рассказывается в главе

4.

Явная выгрузка DLL

Если необходимость в DLL отпадает, ее можно выгрузить из адресного пространства процесса, вызвав функцию:

BOOL FreeLibrary(HMODULE hInstDll);

Вы должны передать в FreeLibrary значение типа HWSTANCE, которое идентифицирует выгружаемую DLL. Это значение вы получаете после вызова LoadLibrary(Ex).

DLL можно выгрузить и с помощью другой функции:

V0ID FreeLibraryAndExitThread(

HMODULE hInstDll,

DWORD dwExitCode);

Она реализована в Kernel32.dll так:

VOID FreeLibraryAndExitThread(HMODULE hInstDll, DWORD dwExitCode) { FreeLibrary(hInstDll);

ExitThread(dwExitCode);

}

На первый взгляд, в ней нет ничего особенного, и вы, наверное, удивляетесь, с чего это Майкрософт решила ее написать. Но представьте такой сценарий, вы пишете DLL, которая при первом отображении на адресное пространство процесса создает поток. Последний, закончив свою работу, отключает DLL от адресного пространства процесса и завершается, вызывая сначала FreeLibrary, а потом ExitThread.

Если поток станет сам вызывать FreeLibrary и ExitThread, возникнет очень серьезная проблема: FreeLibrary тут же отключит DLL от адресного пространства процесса. После возврата из FreeLibrary код, содержащий вызов ExitThread, окажется недоступен, и поток попытается выполнить неизвестно что. Это приведет к нарушению доступа и завершению всего процесса!

С другой стороны, если поток обратится к FreeLibraryAndExitThread, она вызовет FreeLibrary, и та сразу же отключит DLL. Но следующая исполняе-

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

мая инструкция находится в Kernel32.dll, а не в только что отключенной DLL. Значит, поток сможет продолжить выполнение и вызвать ExitThread, которая корректно завершит его, не возвращая управления.

На самом деле LoadLibrary и LoadLibraryEx лишь увеличивают счетчик числа пользователей указанной библиотеки, а FreeLibrary и FreeLibraryAndExitThread

его уменьшают. Так, при первом вызове LoadLibrary для загрузки DLL система проецирует образ DLL-файла на адресное пространство вызывающего процесса и присваивает единицу счетчику числа пользователей этой DLL. Если поток того же процесса вызывает LoadLibrary для той же DLL еще раз, DLL больше не проецируется; система просто увеличивает счетчик числа ее пользователей — вот и все.

Чтобы выгрузить DLL из адресного пространства процесса, FreeLibrary придется теперь вызывать дважды: первый вызов уменьшит счетчик до 1, второй — до 0. Обнаружив, что счетчик числа пользователей DLL обнулен, система отключит ее. После этого попытка вызова какой-либо функции из данной DLL приведет к нарушению доступа, так как код по указанному адресу уже не отображается на адресное пространство процесса.

Система поддерживает в каждом процессе свой счетчик DLL, т. е. если поток процесса A вызывает приведенную ниже функцию, а затем тот же вызов делает поток в процессе B, то MyLib.dll проецируется на адресное пространство обоих процессов, а счетчики числа пользователей DLL в каждом из них приравниваются

1.

HM0DULE hInstDll = LoadLibrary(TEXT("MyLib.dll"));

Если же поток процесса B вызовет далее:

FreeLibrary(hInstDll);

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

Чтобы определить, спроецирована ли DLL на адресное пространство процесса, поток может вызвать функцию GetModuleHandle:

HM0DULE GetModuleHandle(PCTSTR pszModuleName);

Например, следующий код загружает MyLib.dll, только если она еще не спроецирована на адресное пространство процесса:

HM0DULE hInstDll = GetModuleHandle(TEXT("MyLib"));

// подразумевается

 

// расширение .dll

if (hInstDll == NULL) {

 

hInstDll = LoadLibrary(TEXT("MyLib")); // подразумевается расширение .dll

}

Если передать NULL функции GetModuleHandle, она возвращает описатель исполняемого файла приложения.

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

Если у вас есть значение HINSTANCE/HMODULE для DLL, можно определить полное (вместе с путем) имя DLL или EXE с помощью GetModuleFileName:

DWORD GetModuleFileName(

HMODULE hInstHodule,

PTSTR pszPathName,

DWORD cchPath);

Первый параметр этой функции — значение типа HINSTANCE нужной DLL (или EXE). Второй параметр pszPathName, задает адрес буфера, в который она запишет полное имя файла. Третий, и последний, параметр (cchPath) определяет размер буфера в символах. Если передать NULL в параметре hInstModule, функция GetModuleFileName возвращает имя исполняемого файла приложения в переменной pszPathName. Подробнее об этих функциях, псевдопеременной

__ImageBase и GetModuleHandleEx см. в главе 4.

Одновременное использование LoadLibrary и LoadLibraryEx может привести к проецированию одной и той же DLL в разные области адресного пространства. Рассмотрим для примера следующий код:

HMODULE hDll1 = LoadLibrary(TEXT("MyLibrary.dll"));

HMODULE hDll2 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL,

L0AD_LIBRARY_AS_IMAGE_RESOURCE);

HMODULE hD113 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL,

LOAD_LIBRARY_AS_DATAFILE);

Как вы думаете, какое значение будут иметь переменные hDtt1, hDU2 и hDll3? Ясно, что одинаковое, если загружается файл MyLibrary.dll. Сложнее ответить на этот вопрос, если поменять порядок вызовов следующим образом:

HMODULE hDll1 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL, LOAD_LIBRARY_AS_DATAFILE);

HMODULE hD112 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL, LOAD_LIBRARY_AS_IMAGE_RESOURCE); HMODULE hD113 = LoadLibrary(TEXT("MyLibrary.dll"));

В этом случае значение переменных hDll1, hDll2 и hDll3 будет разным! При вызове LoadLibraryEx с флагами LOAD_LIBRARY_AS_DATAFILE, LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE, или LOAD_LIBRARY_AS_IMAGE_RESOURCE операционная система прежде всего проверяет, не загружена ли уже эта библиотека вызовами LoadLibrary и LoadLibraryEx без указанных флагов. Если это так, возвращается адрес, по которому эта библиотека была спроецирована ранее. Если же эта DLL еще не загружена, Windows проецирует ее в доступную область адресного пространства, но не считает ее полностью загруженной. Если сейчас вызвать GetModuleFileName на описателе такого модуля, эта функция вернет 0. Это удобный способ, позволяющий отличить описатели DLL, непригодные для динамических вызовов функций через GetProcAddress (см. следующий раздел).

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

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

Явное подключение экспортируемого идентификатора

Поток получает адрес экспортируемого идентификатора из явно загруженной

DLL вызовом GetProcAddress:

FARPROC GetProcAddress(

HMODULE hInstDll,

PCSTR pszSymbolName);

Параметр hinstDll — описатель, возвращенный LoadLibrary(Ex) или GetModuleHandle и относящийся к DLL, которая содержит нужный идентификатор. Параметр pszSymbolName разрешается указывать в двух формах. Во-первых, как адрес строки с нулевым символом в конце, содержащей имя интересующей вас функции:

FARPROC pfn = GetProcAddress(hInstDll, “SomeFuncInDll”);

Заметьте: тип параметра pszSymbolName — PCSTR, а не PCTSTR. Это значит, что функция GetProcAddress принимает только ANSI-строки — ей нельзя передать Unicode-строку. А причина в том, что идентификаторы функций и переменных в разделе экспорта DLL всегда хранятся как ANSI-строки.

Вторая форма параметра pszSymbolName позволяет указывать порядковый номер нужной функции:

FARPROC pfn = GetProcAddress(hInstDll, MAKEINTRES0URCE(2));

Здесь подразумевается, что вам известен порядковый номер (2) искомого идентификатора, присвоенный ему автором данной DLL. И вновь повторю, что Майкрософт настоятельно не рекомендует пользоваться порядковыми номерами; поэтому вы редко встретите второй вариант вызова GetProcAddress.

При любом способе вы получаете адрес содержащегося в DLL идентификатора. Если идентификатор не найден, GetProcAddress возвращает NULL.

Учтите, что первый способ медленнее, так как системе приходится проводить поиск и сравнение строк. При втором способе, если вы передаете порядковый номер, не присвоенный ни одной из экспортируемых функций, GetProcAddress может вернуть значение, отличное от NULL. В итоге ваша программа, ничего не подозревая, получит неправильный адрес. Попытка вызова функции по этому адресу почти наверняка приведет к нарушению доступа. Я и сам — когда только начинал программировать под Windows и не очень четко понимал эти вещи — несколько раз попадал в эту ловушку. Так что будьте внимательны. (Вот вам, кстати, и еще одна причина, почему от использования порядковых номеров следует отказаться в пользу символьных имен — идентификаторов.)

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

Чтобы вызвать функцию с помощью указателя, полученного с помощью GetProcAddress, сначала необходимо привести его к правильного тину, соответствующему сигнатуре этой функции. Например typedef void (CALLBACK *PFN_DUMPMODULE)(HMODULE hModule) — сигнатура, соответствующая функции void DynamicDumpModule(HMODULE hModule). Ниже покачано, как ди-

намически вызывать эту функцию из экспортирующей ее DLL:

PFN_DUMPMODULE pfnDumpModule = (PFN_DUMPMODULE)GetProcAddress(hDll, "DumpModule");

if (pfnDumpModule != NULL) { pfnDumpModule(hDll);

}

Функция входа/выхода

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

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

switch (fdwReason) {

case DLL_PROCESS_ATTACH:

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

case DLL_THREAD_ATTACH: // создается поток break;

case DLL_THREAD_DETACH:

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

case DLL_PROCESS_DETACH:

// DLL отключается от адресного пространства процесса break;

}

 

return(TRUE);

// используется только для DLL_PROCESS_ATTACH

}

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

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

Параметр hinstDll содержит описатель экземпляра DLL. Как и hinstExe функции (w)WinMain, это значение — виртуальный адрес проекции файла DLL на адресное пространство процесса. Обычно последнее значение сохраняется в глобальной переменной, чтобы его можно было использовать и при вызовах функций, загружающих ресурсы (типа DialogBox или LoadString). Последний параметр, fImpLoad, отличен от 0, если DLL загружена неявно, и равен 0, если она загружена явно.

Параметр fdwReason сообщает о причине, по которой система вызвала эту функцию. Он принимает одно из четырех значений: DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH, DLL_THREAD_ATTACH или DLL_THREAD_DETACH. Мы рассмотрим их в следующих разделах.

Примечание. Не забывайте, что DLL инициализируют себя, используя ^5EI функции DllMain. К моменту выполнения вашей DllMain другие DLL в том же адресном пространстве могут не успеть выполнить свои функции DllMain, т. е. они окажутся неинициализированными. Поэтому вы должны избегать обращений из DllMain к функциям, импортируемым из других DLL. Кроме того, не вызывайте из DllMain функции LoadLibrary(Ex) и FreeLibrary, так как это может привести к взаимной блокировке.

В документации Platform SDK утверждается, что DllMain должна выполнять лишь простые виды инициализации — настройку локальной памяти потока (см. главу 21), создание объектов ядра, открытие файлов и т. д. Избегайте обращений к функциям, связанным с User, Shell, ODBC, COM, RPC и сокетами (а также к функциям, которые их вызывают), потому что соответствующие DLL могут быть еще не инициализированы. Кроме того, подобные функции могут вызывать LoadLibrary(Ex) и тем самым приводить к взаимной блокировке.

Аналогичные проблемы возможны и при создании глобальных или статических С++-объектов, поскольку их конструктор или деструктор вызывается в то же время, что и ваша DllMain.

Подробнее об этом см. в статье «Best Practices for Creating DLLs» по ссылке http://wwwmicrosoft.com/whdc/driver/kernel/DLL_bestprac.mspx

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

Система вызывает DllMain с этим значением параметра fdwReason сразу после того, как DLL спроецирована на адресное пространство процесса.

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

Аэто происходит, только когда образ DLL-файла проецируется в первый раз. Если затем поток вызовет LoadLibrary(Ex) для уже спроецированной DLL, система просто увеличит счетчик числа пользователей этой DLL; так что DllMain вызывается со значением DLL_PROCESS_ATTACH лишь раз.

Обрабатывая DLL_PROCESS_ATTACH, библиотскадолжна выполнить в процессе инициализацию, необходимую се функциям. Например, в DLL могут быть функции, которым нужна своя куча (создаваемая в адресном пространстве процесса). В этом случае DllMain могла бы создать такую кучу, вызвав HeapCreate при обработке уведомления DLL_PROCESS_ATTACH, а описатель созданной кучи сохранить в глобальной переменной, доступной функциям DLL.

При обработке уведомления DLL_PROCESS_ATTACH значение, возвращаемое функцией DllMain, указывает, корректно ли прошла инициализация DLL. Например, если вызов HeapCreate закончился благополучно, следует вернуть TRUE.

Аесли кучу создать не удалось — FALSE. Для любых других значений fdwRea-

son DLL_PROCESS_DETACH, DLL_THREAD_ATTACH или

DLL_THREAD_DETACH — значение, возвращаемое DllMain, системой игнорируется.

Конечно, где-то в системе должен быть поток, отвечающий за выполнение кода DllMain. При создании нового процесса система выделяет для него адресное пространство, куда проецируется EXE-файл и все необходимые ему DLL-модули. Далее создается первичный поток процесса, используемый системой для вызова DllMain из каждой DLL со значением DLL_PROCESS_ATTACH. Когда все спроецированные DLL ответят на это уведомление, система заставит первичный поток процесса выполнить стартовый код из библиотеки C/C++, а потом — входную функцию EXE-файла (_tmain или _tWinMain). Если DllMain хотя бы одной из DLL вернет FALSE, сообщая об ошибке при инициализации, система завершит процесс, удалив из его адресного пространства образы всех файлов; после этого пользователь увидит окно с сообщением о том, что процесс запустить не удалось. Ниже показано соответствующее окно для Windows Vista.

Теперь посмотрим, что происходит при явной загрузке DLL. Когда поток вызывает LoadLibrary(Ex), система отыскивает указанную DLL и проецирует ее на адресное пространство процесса. Затем вызывает DllMain со значением DLL_PROCESS_ATTACH, используя поток, вызвавший LoadLibrary(Ex). Как только DllMain обработает уведомление, произойдет возврат из

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

LoadLibrary(Ex), и поток продолжит работу в обычном режиме. Если же DllMain вернет FALSE (неудачная инициализация), система автоматически отключит образ файла DLL от адресного пространства процесса, а вызов LoadLibrary(Ex) даст

NULL.

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

При отключении DLL от адресного пространства процесса вызывается ее функция DllMain со значением DLL_PROCESS_DETACH в параметре fdwReason. Обрабатывая это значение, DLL должна провести очистку в данном процессе. Например, вызвать HeapDestroy, чтобы разрушить кучу, созданную ею при обработке уведомления DLL_PROCESS_ATTACH. Обратите внимание: если функция DllMain вернула FALSE, получив уведомление DLL_PROCESS_ATTACH, то ее нельзя вызывать с уведомлением DLL_ PROCESS_DETACH. Если DLL отключается изза завершения процесса, то за выполнение кода DllMain отвечает поток, вызвавший ExitProcess (обычно это первичный поток приложения). Когда ваша входная функция возвращает управление стартовому коду из библиотеки С/С++, тот явно вызывает ExitProcess и завершает процесс.

Если DLL отключается в результате вызова FreeLibrary или FreeLibraryAndExitThread, код DllMain выполняется потоком, вызвавшим одну из этих функций. В случае обращения к FreeLibrary управление не возвращается, пока DllMain не закончит обработку уведомления DLL_PROCESS_DETACH.

Учтите также, что DLL может помешать завершению процесса, если, например, ее DllMain входит в бесконечный цикл, получив уведомление DLL_ PROCESS_DETACH. Операционная система уничтожает процесс только после того, как все DLL-модули обработают уведомление DLL_PROCESS_DETACH.

Примечание. Если процесс завершается в результате вызова TerminateProcess, система не вызывает DllMain со значением DLL_PROCESS_DETACH.

А значит, ни одна DLL, спроецированная на адресное пространство процесса, не получит шанса на очистку до завершения процесса. Последствия могут быть плачевны — вплоть до потери данных. Вызывайте TerminateProcess только в самом крайнем случае!

На рис. 20-2 показаны операции, выполняемые при вызове LoadLibrary, а на рис. 20-3 — при вызове FreeLibrary.

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

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

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