Функция DllMain и библиотека С/С++
Рассматривая функцию DllMain в предыдущих разделах, я подразумевал, чтодля сборки DLL Вы используете компилятор Microsoft Visual C++. Весьма вероятно, что при написании DLL Вам понадобится поддержка со стороны стартового кода из библиотеки С/С++. Например, в DLL есть глобальная переменная — экземпляр какого-то С++класса. Прежде чем DLL сможет безопасно ее использовать, для переменной нужно вьзвать ее конструктор, а это работа стартового кода
При сборке DLL компоновщик встраивает в конечный файл адрес DLL-функции входа/выхода. Вы задаете этот адрес компоновщику ключом /ENTRY. Если у Вас компоновщик Microsoft и Вы указали ключ /DLL, то по умолчанию он считает, что функция входа/выхода называется _DllMainCRTStartup. Эта функция содержится в библиотеке С/С++ и при компоновке статически подключается к Вашей DLL - даже если Вы используете DLL-версию библиотеки С/С++.
Когда DLL проецируется на адресное пространство процесса, система па самом деле вызывает именно _DllMainCRTStartup, а не Вашу функцию DllMain. Получив уведомление DLL_PROCESS_ATTACH, функция _DllMainCRTStartup инициализирует библиотеку С/С++ и конструирует все глобальные и статические С++-объекты Закончив, _DllMainCRTSlartup вызывает Вашу DllMain
Как только DLL получает уведомление DLL_PROCESS_DETACH, система вновь вызывает _DllMainCRTStartup, которая теперь обращаемся к Вашей функции DllMain, и, когда та вернет управление, _DllMainCRTStartup вызовет деструкторы для всех глобальных и статических С++-объекгов. Получив уведомление DLL_THREAD_ATTACH, функция _DllMainCRTStartup не делает ничего особенного. Но в случае уведомления DLL_THREAD_DETACH, она освобождает в потоке блок памяти tiddata, если он к тому времени еще не удален. Обычно в корректно написанной функции потока этот блок отсутствует, потому что она возвращает управление в _threadstartex из библиотеки С/С++ (см. главу 6). Функция _threadstartex сама вызывает _endtbreadex, которая освобождает блок tiddata до того, как поток обращается к ExitThread
Но представьте, что приложение, написанное на Паскале, вьзывяет функции из DLL, написанной на С/С++. В этом случае оно создаст поток, нс прибегая к _begin-
threadex, и такой поток никогда не узнает о библиотеке С/С++ Далее поток вызовет функцию из DLL, которая в свою очередь обратится к библиотечной С-функции. Как Вы помните, подобные функции "на лету" создают блок tiddata и сопоставляют его с вызывающим потоком. Получается, что приложение, написанное на Паскале, может создавать потоки, способные без проблем обращаться к функциям из библиотеки C' Когда сго функция потока возвращает управление, вызывается ExitThread, а библиотека С/С++ получает уведомление DI.I,_THREADDETACH и освобождает блок памяти tiddata, так что никакой утечки памяти не происходит. Здорово придумано, да?
Я уже говорил, что реализовать в коде Вашей 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_THREAD_ATTACH и DLL_THREAD_DETACH. Функция DisableThreadLibraryCalls вызывается для ускорения создания и разрушения потоков.
Функция входа/выхода
В DI,T, может быть лишь одна функция входа/выхода Система вызывает ее в некоторых ситуациях (о чем речь еще впереди) сугубо в информационных целях, и обычно она используется ULL для инициализации и очистки ресурсов в конкретных процессах или потоках Если Вашей 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
}
NOTE:
При вызове 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. Мы рассмотрим их в следующих разделах.
NOTE:
Не забывайте, что DLL инициализируют себя, используя функции DllMain. К моменту выполнения Вашей DllMain другие DLL в том же адресном пространстве могут не успеть выполнить свои функции DllMain, т. e. они окажутся неинициализированными. Поэтому Вы должны избегать обращений из DllMain к функциям, импортируемым из других DLL. Кроме того, не вызывайте из DllMain функции LoadLibrary(Ex) и FreeLibrary, так как это может привести к взаимной блокировке.
В документации Platform SDK утверждается, что DllMain должна выполнять лишь простые виды инициализации — настройку локальной памяти потока (см. главу 21), создание объектов ядра, открытие файлов и т. д. Избегайте обращений к функциям, связанным с User, Shell, ODBC, COM, RPC и сокетами (а также к функциям, которые их вызывают), потому что соответствующие DLL могут быть еще не инициализированы. Кроме того, подобные функции могут вызывать LoadLibrary(Ex) и тем самым приводить к взаимной блокировке.
Аналогичные проблемы возможны и при создании глобальных или статических С++-объектов, поскольку их конструктор или деструктор вызывается в то же время, что и Ваша DllMain.
Известные DLL
Некоторые DLL, поставляемые с операционной системой, обрабатываются по-особому. Они называются известными DLL (known DLLs) и ведут себя точно так же, кяк и любые другие DLL с тем исключением, что система всегда ищет их в одном и том же каталоге. D реестре есть раздел:
HKEY_LOCAL_MACHTNE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
Содержимое этого раздела может выглядеть примерно так, как показано ниже (при просмотре реестра с помощью утилиты RegEdit.exe).
Как видите, здесь содержится набор параметров, имена которых совпадают с именами известных DLL. Значения этих параметров представляют собой строки, идентичные именам параметров, но дополненные расширением .dll. (Впрочем, это не всегда так, и Вы сами убедитесь в этом на следующем примере) Когда Вы вызываете LoadLibrary или LoadLibraryEx, каждая из них сначала проверяет, указано ли имя DLL вместе с расширением .dll. Если нет, поиск DLL ведется по обычным правилам.
Если же расширение .dll указано, функция его отбрасывает и ищет в разделе реестра KnownDLLs параметр, имя которого совпадает с именем DLL. Если его нет, вновь применяются обычные правила поиска А если он есть, система считывает значение этого параметра и пытается загрузить заданную в нем DLL. При этом система ищет
DLL в каталоге, на который указывает значение, связанное с параметром реестра DllDirectory. По умолчанию в Windows 2000 параметру DllDirectory присваивается значение %SystemRoot%\System32
А теперь допустим, что мы добавили в раздел реестра KnownDLLs такой параметр
Имя параметра; SomeLib Значение параметра SomeOtherLib.dll
Когда мы вызовем следующую функцию, система будет искать файл по обычным правилам,
LoadLibrary("SomeLib");
Но если мы вызовем ее так, как показано ниже, система увидит, что в реестре есть параметр с идентичным именем (не забудьте она отбрасывает расширение .dll).
LoadLibrary("SomeLib dll");
Таким образом, система попытается загрузить SomeOtherLib.dll вместо SomcLib dll При этом она будет сначала искать SomeOtherLib.dll в каталоге %SystemRoot%\System32. Если нужный файл в этом каталоге есть, будет загружен именно он. Нет — LoadLibrary(Ex) вернет NULL, a GetLastError - ERROR_FILE_NOT_FOUND (2).
Явная выгрузка DLL
Если необходимость в DLL отпадает, ее можно выгрузить из адресного пространства процесса, вызвав функцию.
BOOL FreeLibrary(HINSTANCE hinstDll);
Вы должны передать в FreeLibrary значение типа HINSTANCE, которое идентифицирует выгружаемую DLL. Это значение Вы получаете после вызова LoadLibrary(Ex).
DLL можно выгрузить и с помощью другой функции:
VOID FreeLibraryAndExitThread( HlNSTANCE hinstDll, DWORD dwExitCode);
Она реализована в Kernel32.dll так:
VOID FreeLibraryAndExitThread(HINSTANCE hinstDll, DWORD dwExitCode)
{
FreeLibrary(hinstDll);
ExitThread(dwExitCode);
}
На первый взгляд, в ней нет ничего особенного, и Вы, наверное, удивляетесь, с чего это Microsoft решила ее написать. Но представьте такой сценарий. Вы пишете DLL, которая при первом отображении на адресное пространство процесса создает поток. Последний, закончив свою работу, отключает DLL от адресного пространства процесса и завершается, вызывая сначала FreeLibrary, а потом ExttThread.
Если поток станет сам вызывать FreeLibrary и ExitThread, возникнет очень серьезная проблема: FreeI.ibrary тут же отключит DLL от адресного пространства процесса. После возврата из FreeLibrary код, содержащий вызов ExttThread, окажется недоступен, и поток попытается выполнить не известно что. Это приведет к нарушению доступа и завершению всего процесса!
С другой стороны, если поток обратится к FreeLibraryAndExitThread, она вызовет FreeLibrary, и та сразу же отключит DLL, Но следующая исполняемая инструкция находится в KerneI32.dlI, а нс в только что отключенной DLL. Значит, поток сможет продолжить выполнение и вызвать ExitThread, которая корректно завершит его, не возвращая управления.
Впрочем, FreeLibraryAndExitThread может и не понадобиться. Мне она пригодилась лишь раз, когда я занимался весьма нетипичной задачей. Да и код я писал под Windows NT 3-1, где этой функции не было. Наверное, поэтому я так обрадовался, обнаружив ее в более новых версиях Windows.
На самом деле LoadLibrary и LoadLibraryEx лишь увеличивают счетчик числа пользователей указанной библиотеки, a FreeLibrary и FreeLibraryAndExitThread его уменьшают Так, при первом вызове LoadLibrary дум загрузки DLL система проецирует образ DLL-файла иа адресное пространство вызывающего процесса и присваивает единицу счетчику числа пользователей этой DLL Если поток того же процесса вызывает LoadLibrary для той же DLL еще раз, DLL больше не проецируется; система просто увеличивает счетчик числа ее пользователей — вот и все.
Чтобы выгрузить DLL из адресного пространства процесса, FreeLibrary придется теперь вызывать дважды: первый вызов уменьшит счетчик до 1, второй — до 0. Обнаружив, что счетчик числа пользователей DLL обнулен, система отключит ее. После этого попытка вызова какой-либо функции из данной DLL приведет к нарушению доступа, так как код по указанному адресу уже не отображается на адресное пространство процесса.
Система поддерживает в каждом процессе свой счетчик DLL, т. e. если поток процесса А вызывает приведенную ниже функцию, а затем тот же вызов делает поток в процессе В, то MyLib.dll проецируется на адресное пространство обоих процессов, а счетчики числа пользователей DLL в каждом из них приравниваются 1.
HINSTANCE hinstDll = LoadLibrary("MyLib.dll");
Если же поток процесса В вызовет далее:
FreeLibrary(hinst011);
счетчик числа пользователей DLL в процессе В обнулится, что приведет к отключению DLL oт адресного пространства процесса В. Но проекция DLL на адресное пространство процесса А нс затрагивается, и счетчик числа пользователей DLL в нем остается прежним.
Чтобы определить, спроецирована ли DLL на адресное пространство процесса, поток может вызывать функцию GеtМоdu1еНапd1е:
HINSTANCE GetModuleHandle(PCTSTR pszModuleName);
Например, следующий код загружает MyLib.dll, только если она еще не спроецирована на адресное пространство процесса
HINSTANCE hinstDll = GetHoduleHandle("MyLib");
// подразумевается расширение .dll if (hinstDll == NULL)
{
hinstDll = LoadLibrary("MyLib");
// подразумевается расширение .dll
}
Если у Вас есть значение HINSTANCE для DLL, можно определить полное (вместе с путем) имя DLL или EXE с помощью GetModuleFileName
DWORD GetModuleFileName( HINSTANCE hinstModule, PTSTR pszPathName, DWORD cchPath);
Первый параметр этой функции — значение типа HINSTANCE нужной DLL (или EXE). Второй параметр, pszPathName, задает адрес буфера, в который она запишет полное имя файла Третий, и последний, параметр (cchPath) определяет размер буфера в символах.
Явная загрузка DLL
В любой момент поток может спроецировать DLL на адресное пространство процес ca, вызвав одну из двух функций:
HINSTANCE LoadLibrary{PCTSTR pszDLLPathName);
HINSTANCE LoadLibraryEx( PCTSTR pszDLLPathName, HANDLE hFile, DWORD dwFlags);
Обе функции ищут образ DLL-файла (в каталогах, список которых приведен в предыдущей главе) и пытаются спроецировать его на адресное пространство вызывающего процесса. Значение типа HINSTANCE, возвращаемое этими функциями, со-
общает адрес виртуальной памяти, но которому спроецирован образ файла. Если спроецировать DLL на адресное пространство процесса не удалось, функции возвращают NULL Дополнительную информацию об ошибке можно получить вызовом GetLastError
Очевидно, Вы обратили внимание на два дополнительных параметра функции LoadLibraryEx, hFile и dwFlags Первый зарезервирован для использования в будущих версиях и должен быть NULL Bo втором можно передать либо 0, либо комбинацию флагов DONT_RESOLVE_DLL_REFERENCES, LOAD_LIBRARY_AS_DATAFILE и LOAD_WITH_ ALTERED_SEARCH_PATH, о которых мы сейчас и поговорим.
СОЗДАНИЕ DLL
1 ) Заголовочный файл с экспортируемыми прототипами структурами и идентификаторами (символьными именами) 2) Исходные файлы С/С++ в которых реализованы экспортируемые функции и определены переменные 3) Компилятор создает OBJ-файл из каждого исходного файла С/С++ 4) Компоновщик собирает DLL из OBJ модулей 5) Если DLL экспортирует хотя бы одну переменную или функцию компоновщик создает и LIB-файл {при явном связывании этот файл не используется) | СОЗДАНИЕ ЕХЕ
6) Заголовочный файл с импортируемыми прототипами, структурами и идентификаторами 7) Исходные файлы С/С++ в которых нет ссылок на импортируемые функции и переменные 8) Компилятор создает OBJ файл из каждого исходного файла С/С++ 9) Компоновщик собирает ЕХЕ-модуль из OBJ-модулей (LIB файл DLL не нужен, так как нет прямых ссылок на экспортируемые идентификаторы, раздел импорта в ЕХЕ-модуле отсутствует) |
Рис. 20-1. Так DLL создается и явно связывается с приложением
DONT_RESOLVE__DLL_REFERENCES
Этот флаг укапывает системе спроецировать DLL на адресное пространство вызывающего процесса. Проецируя DLL, система обычно вызывает из нее специальную функцию DllMain (о ней — чуть позже) и с ее помощью инициализирует библиотеку. Так вот, данный флаг заставляет систему проецировать DLL, не обращаясь к DllMain.
Кроме того, DLL может импортировать функции из других DLL При загрузке библиотеки система проверяет, использует ли она другие DLL; если да, то загружает и их При установке флага DONT_RESOLVE_DLL_REFERENCES дополнительные DLL автоматически не загружаются.
LOAD_LIBRARY_AS_DATAFILE
Этот флаг очень похож на предыдущий. DLL проецируется на адресное пространство процесса так, будто это файл данных. При этом система не тратит дополнительное время на подготовку к выполнению какого-либо кода из данного файла Например, когда DLL проецируется на адресное пространство, система считывает информацию из DLL-файла и на ее основе определяет, какие атрибуты защиты страниц следует присвоить разным частям файла. Если флаг LOAD_LIBRARY_AS_DATAFILE нс указан, атрибуты защиты устанавливаются такими, чтобы код из данного файла можно было выполнять.
Этот флаг может понадобиться по нескольким причинам Во-первых, его стоит указать, если DLL содержит только ресурсы и никаких функций. Тогда DLL проецируется на адресное пространство процесса, после чего при вызове функций, загружающих ресурсы, можно использовать значение HINSTANCE, возвращенное функцией LoadLibraryEx. Во-вторых, он пригодится, если Вам нужны ресурсы, содержащиеся в каком-нибудь ЕХЕ-файле. Обычно загрузка такого файла приводит к запуску нового процесса, но этого не произойдет, если его загрузить вызовом LoadLibraryEx в адресное пространство Вашего процесса. Получив значение HINSTANCE для спроецированного ЕХЕ-файла, Вы фактически получаете доступ к его ресурсам. Так как в ЕХЕ-файле нет DllMain, при вызове LoadLibraryEx для загрузки ЕХЕ-файла нужно указать флаг LOAD_LIBRARY_AS_DATAFILE.
LOAD_WITH_ALTERED_SEARCH_PATH
Этот флаг изменяет алгоритм, используемый LoadLibraryEx при поиске DLL-файла. Обычно поиск осуществляется так, как я рассказывал в главе 19 Однако, если данный флаг установлен, функция ищет файл, просматривая каталоги в таком порядке
Каталог, заданный в napaмeтре pszDLLPathName. Текущий каталог процесса. Системный каталог Windows. Основной каталог Windows. Каталоги, перечисленные в переменной окружения PATH
Явная загрузка DLL и связывание идентификаторов
Чтобы поток мог вызвать функцию из DLL-модуля, последний надо спроецировать на адресное пространство процесса, которому принадлежит этот поток Делается это двумя способами. Первый состоит в том, что код Вашего приложения просто ссылается на идентификаторы, содержащиеся в DLL, и гем самым заставляет загрузчик неявно загружать (и связывать) нужную DLL при запуске приложения
Второй способ — явная загрузка и связывание требуемой DLL в период выполнения приложения Иняче говоря, его поток явно загружает DLL в адресное пространство процесса, получает виртуальный адрес необходимой DLL-функции и вызывает ее по этому адресу. Изящество такого подхода в том, что все происходит в уже выполняемом приложении
На рис 20-1 показано, как приложение явно загружает DLL и связывается с ней
Явное подключение экспортируемого идентификатора
Поток получает адрес экспортируемого идентификатора из явно загруженной DLL вызовом GetProcAddress:
FARPROC GetProcAddress( HINSTANCE hinstDll, PCSTR pszSymbolName);
Параметр hinstDll — описатель, возвращенный LoadLibrary(Ex) или GetModuleHandle и относящийся к DLL, которая содержит нужный идентификатор. Параметр pszSymbolName разрешается указывать в двух формах. Во-первых, как адрес строки с нулевым символом в конце, содержащей имя интересующей Вас функции:
FARPROC pfn = GetProcAddress(hinstDll, "SomeFuncInDll");
Заметьте, тип параметра pszSymbolName — PCSTR, а не PCTSTR. Это значит, что функция GetProcAddress принимает только ANSI-строки — ей нельзя передать Unicodeстроку А причина в том, что идентификаторы функций и переменных в разделе экспорта DLL всегда хранятся как ANSI-строки.
Вторая форма пapaмeтpa pszSymbolName позволяет указывать порядковый номер нужной функции.
FARPROC pfn = GetProcAddress(hinstDll, MAKEINTRESOURCE(2));
Здесь подразумевается, что Вам известен порядковый номер (2) искомого идентификатора, присвоенный ему автором данной DLL И вновь повторю, что Microsoft
настоятельно не рекомендует пользоваться порядковыми номерами; поэтому Вы редко встретите второй вариант вызова GetProсAddress.
При любом способе Вы получаете адрес содержащегося в DLL идентификатора. Если идентификатор не найден, GetProcAddress возвращает NULL.
Учтите, что первый способ медленнее, так как системе приходится проводить поиск и сравнение строк. При втором способе, если Вы передаете порядковый номер, не присвоенный ни одной из экспортируемых функций, GetProcAddress может вернуть значение, отличное от NULL В итоге Ваша программа, ничео не подозревая, получит неправильный адрес. Попытка вызова функции по этому адресу почти наверняка приведет к нарушению доступа Я и сам — когда только начинал программировать под Windows и не очень четко понимал эти вещи — несколько раз попадал в эту ловушку. Так что будьте внимательны. (Вот Вам, кстати, и еще одна причина, почему от использования порядковых номеров следует отказаться в пользу символьных имен — идентификаторов.)
Как система упорядочивает вызовы DIIMain
Система упорядочивает вызовы функции DllMain. Чтобы понять, что я имею в виду, рассмотрим следующий сценарий Процесс А имеет два потока: А и В. На его адресное пространство проецируется DLL-модуль SomeDLL.dll. Оба потока собираются вызвать CreateThread, чтобы создать еще два потока: С и D.
Когда поток А вызывает для создания потока С функцию CreateThread, система обращается к DllMain из SomeDLL.dll со значением DLL_THREAD_АТТАСН. Пока поток С исполняет код 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);
// доступ к новому потоку больше не нужен
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_PROCFSS_ATTACH, свою работу еще не закончил. Проблема кроется в вызове WaitForSingleObject. Она приостанавливает выполнение текущего потока до тех пор, пока не завершится новый. Однако у нового потока нет ни единою шанса не только на завершение, но и на выполнение хоть какого-нибудь кода — он приостановлен в ожидании того, когда текущий поток выйдет из DllMain Вот Вам и взаимная блокировка — выполнение обоих потоков задержано навеки!
Впервые начав размышлять над этой проблемой, я обнаружил функцию DisableThreadLibraryCalls:
BOOl DisableThreadLibraryCalls(HINSTANCE hinstDll);
Вызывая ее, Вы сообщаете системе, что уведомления DLL_THREAD_ATTACH и DLL_ THREAD_DETACH не должны посылаться DllMain той библиотеки, которая указана в вызове Мне показалось логичным, что взаимной блокировки не будет, если система не стаиет посылать DLL уведомления. Но, проверив свое решение (см. ниже), я убедился, что это не выход.
BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad)
{
HANDLE hThread; DWORD dwThreadId;
switch (fdwReason)
{
case DLL_PROCESS_ATTACH.
// DLL проецируется на адресное пространство процесса
// предотвращаем вызов DllMain при создании
// или завершении потока
DisableThreadLibraryCalls(hinstDll);
// создаем поток для выполнения какой-то работы
hThread = CreateThread(NULL, 0, SomeFunction, NULL, 0, &dwThreadId);
// задерживаем наш поток до завершения нового потока
WaitForSingleObject(hThread, INFINITE);
// доступ к новому потоку больше не нужен
CloseHandle(hThread);
break;
саsе DLL_THREAD_ATTACH:
// создается сщс один поток
break;
case DLL_THREAD_DETACH:
// поток завершается корректно
break;
case DLL_PROCESS_DETACH:
// DLL выгружается из адресного пространства процесса
break;
}
return TRUE;
}
Потом я понял, в чем лело Создавая процесс, система создает и объект-мьютекс. У каждого процесса свой объект-мьютекс — он не разделяется между несколькими процессами. Его назначение — синхронизация всех потоков процесса при вызове ими функций DllMain из DLL, спроецированных на адресное пространство данного процесса.
Когда вызывается CreateThread, стстема создает сначала объект ядра "поток" и стек потока, затем обращается к WaitForSingleObject, передавая ей описатель объекта-мьютекса данного процесса. Как только поток захватит этот мьютекс, система заставит его вызвать DllMain из каждой DLL со значением DLL_THREAD_ATTACH. И лишь тогда система вызовет ReleaseMutex, чтобы освободить объект-мьютекс Вот из-за того, что система работает именно так, дополнительный вызов DisableThreadLibraryCalls и не предотвращает взаимной блокировки потоков. Единственное, что я смог придумать, — переделать эту часть исходного кода так, чтобы ни одна DllMain не вызывала WaitForSingleObject
Модификация базовых адресов модулей
У каждого EXE и DLL-модуля есть предпочтительный базовый адрес (preferred base address) — идеальный адрес, по которому он должен проецироваться на адресное пространство процесса. Для ЕХЕ-модуля компоновщик выбирает в качестве такого адреса значение 0x00400000, а для DLL-модуля — 0x10000000. Выяснить этот адрес позволяет утилита DumpBin с ключом /Headers. Вот какую информацию сообщает DumpBin о самой себе:
С \>DUMPBIN /headers dumpbin.exe
Microsoft (R} COFF Binary File Dumper Version 6 00.8168 Copyright (C) Microsoft Corp 1992-1998. All rights reserved
Dump of file dumpbin.exe
PE signature found
File Type: EXECUTABLE_IMAGE
File HEADER VALUES
14C machine (i386)
3 number of sections
3588004A time date stamp Wed Jun 17 10'43-38 1998 0 file pointer to symbol table 0 number of symbols E0 size of optional header 10F characteristics
Relocations stripped
Executable
Line numbers stripped
Symbols stripped
32 bit word machine
OPTIONAL HEADER VALUES
108 magic #
6.00 linker version
1000 size of code
2000 size of initialized data
0 size of uninitialized data
1320 RVA of entry point
1000 base of code
2000 base of data
400000 image base <-- предпочтительный базовый адрес модуля
1000 section alignment
1000 file alignment 4.00 operating system verbion 0.00 image version 4.00 subsystem version
0 Win32 version 4000 size of image 1000 size of headers 127E2 checksum
3 subsystem (Windows CUI)
0 DLL characteristics
100000 size of stack reserve 1000 size of stack commit
При запуске исполняемого модуля загрузчик операционной системы создает виртуальное адресное пространство нового процесса и проецирует этот модуль по адресу 0x00400000, а DLL-модуль — по адресу 0x10000000. Почему так важен предпочтительный базовый адрес? Взгляните на следующий фрагмент кода.
int g_x;
void Func()
{
g_x = 5; // нас интересует эта строка
}
После обработки функции Func компилятором и компоновщиком полученный машинный код будет выглядеть приблизительно так:
MOV [0x00414540], b
Иначе говоря, компилятор и компоновщик "жестко зашили" в машинный код адpеc переменной g_x: в адресном пространстве процесса (0x00414540). Но, конечно, этот адрес корректен, только ссли исполняемый модуль будет загружен по базовому адресу 0x00400000
А что получится, если тот же исходный код будет помещен в DLL? Тогда машинный код будет иметь такой вид
MOV [0x10014b40], 5
Заметьте, что и на этот paз виртуальный адрес переменной g_x "жестко зашит" в машинный код. И опять жс этот адрес будет правилен только при том условии, что DLL загрузится по своему базовому адресу.
О'кэй, а теперь представьте, что Вы создали приложение с двумя DLL. По умолчанию компоновщик установит для ЕХЕ-модуля предпочтительный базовый адрес 0x00400000, а для обеих DLL — 0x10000000. Если Вы затем попытаетесь запустить исполняемый файл, загрузчик создаст виртуальное адресное пространство и спроецирует ЕХЕ-модуль по адресу 0x00400000 Далее первая DLL будет спроецирована по адресу 0x10000000, но загрузить вторую DLL по предпочтительному базовому адресу не удастся — ee придется проецировать по какому-то другому адресу.
Переадресация (relocation) в EXE- или DLL-модуле операция просто ужасающая, и Вы должны сделать все, чтобы избежать ее. Почему? Допустим, загрузчик переместил вторую DLL по адресу 0x20000000. Тогда код, который присваивает переменной
g_x значение 5, должен измениться на:
MOV [0x20014540], 5
Но в образе файла код остался прежним:
MOV [0x10014540], 5
Если будет выполнен именно этот кол, он перезапишет какое-то 4-байтовое значение в первой DLL значением 5 Но, по идее, такого не должно случиться. Загрузчик исправит этот код. Дсло в том, что, создавая модуль, компоновщик встраивает в конечный файл раздел переадресации (relocation section) co списком байтовых смещений. Эти смещения идентифицируют адреса памяти, используемые инструкциями машинного кода. Если загрузчику удастся спроецировать модуль по его предпочтительному базовому адресу, раздел переадресации не понадобится Именно этого мы и хотим.
С другой стороны, если модуль не удастся спроецировать по базовому адресу, загрузчик обратится к разделу переадресации и последовательно обработает все его записи. Для каждой записи загрузчик обращается к странице памяти, где содержится машинная команда, которую надо модифицировать, получает используемый ею на данный момент адрес и добавляет к нему разницу между предпочтительным базовым адресом модуля и сго фактическим адресом.
В предыдущем примере вторая DLL была спроецирована по адресу 0x20000000, тогда как ее предпочтительный базовый адрес — 0x10000000 Получаем разницу (0х 10000000), добавляем ее к адресу в машинной команде и получаем.
MOV [0x20014540], 5
Теперь и вторая DLL корректно ссылается на переменную g_x. Невозможность загрузить модуль по предпочтительному базовому адресу создает две крупные проблемы
Загрузчику приходится обрабатывать все записи раздела переадресации и модифицировать уйму кода в модуле. Это сильнейшим образом сказывается на быстродействии и может резко увеличить время инициализации приложения. Из-за того что загрузчик модифицирует в оперативной памяти страницы с кодом модуля, системный механизм копирования при записи создает их копии в страничном файле.
Вторая проблема особенно неприятна, поскольку теперь страницы с кодом модуля больше нельзя выгружать из памяти и перезагружать из его файла на диске Вместо этого страницы будут постоянно сбрасываться в страничный файл и подгружаться из него. Это тоже отрицательно скажется па производительности Но и это еще не все. Поскольку все страницы с кодом модуля размещаются в страничном файле, в системе сокращается объем общей памяти, доступной другим процессам, а это ограничивает размер электронных таблиц, документов текстовых процессоров, чертежей CAD, растровых изображений и т. д.
Кстати, Вы можете создать EXE- или DLL-модуль без раздела переадресации, указав при сборке ключ /FIXED компоновщика. Тогда у модуля будет меньший размер, но загрузить сго по другому базовому адресу, кроме предпочтительного, уже не удастся. Если загрузчику понадобится модифицировать адреса в модуле, в котором пет раздела переадресации, он уничтожит весь процесс, и пользователь увидит сообщение «Abnormal Process Termination» («аварийное завершение процесса")
Для DLL, содержащей только ресурсы, это тоже проблема. Хотя в ней нет машинного кода, отсутствие раздела переадресации не позволит загрузить ее по базовому
адресу, отличному от предпочтительного Просто нелепо. Но, к счастью, компонов щик может встроить в заголовок модуля информацию о том, что в модуле нет раздела переадресации, так как он вообще не нужен. А загрузчик Windows 2000, обнаружив эту информацию, может пагрупить DLL, которая содержит только ресурсы, без дополнительной нагрузки на страничный файл.
Для создания файла с немодифицируемыми адресами предназначен ключ /SUBSYSTEM:WINDOWS, 5 0 или /SUBSYSTEM:CONSOLE, 5 0; ключ /FIXED при этом не нужен. Если компоновщик определяет, что модификация адресов в модуле не понадобится, он опускает раздел переадресации и сбрасывает в заголовке специальный флаг IMAGEFILERELOCS_STRIPPED Тогда Windows 2000 увидит, что данный модуль можно загружать по базовому адресу, отличному от предпочтительного, и что ему не требуется модификация адресов. Но все, о чем я только что рассказал, поддерживается лишь в Windows 2000 (вот почему в ключе /SUBSYSTEM указывается значение 50)
Теперь Вы понимаете, насколько важен предпочтительный базовый адрес. Загружая несколько модулей в одно адресное пространство, для каждого из них приходится выбирать свои базовые адреса. Диалоговое окно Project Settings в среде Microsoft Visual Studio значительно упрощает решение этой задачи. Вам нужно лишь открыть вкладку Link, в списке Category указать Output, а в поле Base Address ввести предпочтительный адрес. Например, на следующей иллюстрации для DLL установлен базовый адрес 0x20000000
Кстати, всегда загружайте DLL, начиная со старших адресов; это позволяет уменьшить фрагментацию адресного пространства.
NOTE:
Предпочтительные базовые адреса должны быть кратны гранулярности выделения памяти (64 Кб на всех современных платформах). В будущем эта цифра может измениться Подробнее о гранулярности выделения памяти см. главу 13
О'кэй, все это просто замечательно, но что делать, если понадобится загрузить кучу модулей в одно адресное пространство? Было бы неплохо «одним махом» задать правильные базовые адреса для всех модулей. К счастью, такой способ есть
В Visual Studio есть утилита Rebase.exe. Запустив ее без ключей в командной строке, Вы получите информацию о том, как ею пользоваться. Она описана в документации Platform SDK, и я не буду ее здесь детально рассматривать Добавлю лишь, что в ней нет ничего сверхъестественного: она просто вызывает функцию ReBaselmage для каждого указанного файла. Вот что представляет собой эта функция:
BOOL ReBaseImage(
PSIR CurrentImageName; // полное имя обрабатываемого файла
PSTR SymbolPath; // символьный путь к файлу (необходим для корректности отладочной информации)
BOOL fRebase; // TRUE = выполнить реальную модификацию адреса;
// FALSE - имитировать такую модификацию
BOOL fRebasoSysFileOk; // FALSE = не модифицировать адреса системных файлов
BOOL fGoingDown; // TRUE = модифицировать адрес модуля,
// продвигаясь в сторону уменьшения адресов
ULONG CheckImageSize; // ограничение на размер получаемого в итоге модуля
ULONG* pOldImageSize; // исходный размер модуля
ULONG* pOldImageBase; // исходный базовый адрес модуля
ULONG* pNewIinageSize; // ноеый размер модуля
ULONG* pNfiwImageRase; // новый базовый адрес модуля
ULONG TirneStamp); // новая временная мегка модуля
Когдя Вы запускаете утилиту Rebase, указывая ей несколько файлов, она выполняет следующие операции.
Моделирует создание адресного пространства процесса Открывает все модули, которые загружались бы в это адресное пространство, и получаст предпочтительный базовый адрес и размер каждого модуля. Моделирует переадресацию модулей в адресном пространстве, добиваясь того, чтобы модули не перекрывались. В каждом модуле анализирует раздел переадресации и соответственно изменяет код в фяйле модуля на диске. Записывает новый базовый адрес в заголовок файла.
Rebase — отличная утилита, и я настоятельно рекомендую Вам пользоваться ею. Вы должны запускать ее ближе к концу цикла сборки, когда уже созданы все модули приложения. Кроме того, применяя утилиту Rebase, можно проигнорировать настройку базового адреса в диалоговом окне Pro)cct Settings. Она автоматически изменит базовый адрес 0x10000000 для DLL, задаваемый компоновщиком по умолчанию
Но ни при каких обстоятельствах не модифицируйте базовые адреса системных модулей. Их адреса уже оптимизированы Microsoft, так что при загрузке в одно адресное пространство системные модули не перекрываются
Я, кстати, добавил специальный инструмент в свою программу ProcessInfoexe (см. главу 4) Он показывает список всех модулей, находящихся в адресном пространстве процесса. В колонке BaseAddr сообщается виртуальный адрес, по которому загружен модуль. Справа от BaseAddr расположена колонка ImagAddr Обычно оиа пуста, указывая, что соответствующий модуль загружен по его предпочтительному базовому адресу. Так и должно быть для всех модулей Однако, ссли в этой колонке присутствует адрес в скобках, значит, модуль загружен не по предпочтительному базовому адресу, и в колонке ImagAddr показывается базовый адрес, взятый из заголовка его файла на диске.
Ниже приведена информация о процессе Acrord32.exe, предоставленная моей программой ProcessInfo Обратите внимдние, что часть модулей загружена по предпочтительным базовым адресам, а часть — нет. Для последних сообщается один и тот же базовый адрес, 0x10000000; значит, автор этих DLL не подумал о проблемах модификации базовых адресов — пусть ему будет стыдно.
Отложенная загрузка DLL
Microsoft Visual С++ 60 поддерживает отложенную загрузку DLL — новую, просто фантастическую функциональность, которая значительно упрощает работу с библиотеками. DLL отложенной загрузки (delay-load DLL) — это неявно связываемая DLL, которая нс загружается до тех пор, пока Ваш код не обратится к какому-нибудь экспортируемому из нее идентификатору. Такие DLL могут быть полезны в следующих ситуациях.
Если Ваше приложение использует несколько DLL, его инициализация может занимать длительное время, потому что загрузчику приходится проецировать их па адресное пространство процесса. Один из способов снять остроту этой проблемы — распределить загрузку DLL в ходе выполнения приложения. DLL отложенной загрузки позволяют легко решить эту задачу. Если приложение использует какую-то новую функцию и Вы пытаетесь запустить его в более старой версии операционной системы, в которой нет такой функции, загрузчик сообщает об ошибке и не дает запустить приложение. Вам нужно как-то обойти этот механизм и уже в период выполнения, выяснив, что приложение работает в старой версии системы, не вызывать новую функцию. Например, Baшa программа в Windows 2000 должна использовать функции PSAPI, я в Windows 98 — ToolHclp-функции (вроде Process32Next) При инициализации программа должна вызвать GetVersionEx, чтобы определить версию текущей операционной системы, и после этого обращаться к соответствующим функциям. Попытка запуска этой программы в Windows 98 приведет к тому, что загрузчик сообщит об ошибке, поскольку в этой системе нет модуля PSAPI.dll. Так вот, и эта проблема легко решается за счет DLL отложенной загрузки.
Я довольно долго экспериментировал с DLL отложенной загрузки в Visual C++ 6.0 и должен скязать, что Microsoft прекрасно справилась со своей задачей. DLL отложенной загрузки открывают массу дополнительных возможностей и корректно работают как в Windows 98, так и в Windows 2000.
Давайте начнем с простого: попробуем воспользоваться механизмом поддержки DLL отложенной загрузки. Для этого создайте, как обычно, свою DLL. Точно так же создайте и ЕХЕ-модуль, по потом Вы должны поменять пару ключей компоновщика и повторить сборку исполняемого файла. Вот эти ключи:
/Lib:DelayImp.lib /DelayLoad:MyDll.dll
Первый ключ заставляет компоновщик внедрить в ЕХЕ-модуль специальную функцию, _delayLoadHelper, а второй — выполнить следующие операции:
удалить MyDll.dll из раздела импорта исполняемого модуля, чтобы при инициализации процесса загрузчик операционной системы не пытался неявно связывать эту библиотеку с ЕХЕ-модулем; встроить в ЕХЕ-файл новый раздел отложенного импорта (.didat) со списком функций, импортируемых из MyDll.dll; привести вызовы функций из DLL отложенной загрузки к вызовам _delayLoadHelper.
При выполнении приложения вызов функции из DLL отложенной загрузки (далее для краткости — DLL-функции) фактически переадресуется к _delayLoadHelper. Последняя, просмотрев раздел отложенного импорта, знает, что нужно вызывать LoadLibrary, а затем GetProcAddress. Получив адрес DLL-функции, delayLoadHelper делает так, чтобы в дальнейшем эта DLL-функция вызывалась напрямую. Обратите внимание, что каждая функция в DLL настраивается индивидуально при первом ее вызове Ключ /DelayLoad компоновщика указывается для каждой DLL, загрузку которой требуется отложить.
Вот собственно, и все. Как видите, ничего сложного здесь нет. Однако следует учесть некоторые тонкости. Загружая Ваш ЕХЕ-файл, загрузчик операционной системы обычно пытается подключить требуемые DLL и при неудяче сообщает об ошибке. Но при инициализации процесса наличие DLL отложенной загрузки не проверяется. И если функция _delayLoadHelper уже в период выполнения не найдет нужную DLL, она возбудит программное исключение. Вы можете перехватить его, используя SEH, и как-то обработать. Если же Вы этого не сделаете, Ваш процесс будет закрыт. (О структурной обработке исключений см. главы 23, 24 и 25.)
Еще одна проблема может возникнуть, когда _delayLoadHelper, найдя Вашу DLL, не обнаружит в ней вызываемую функцию (например, загрузчик нашел старую версию DLL). В этом случае _delayLoadHelper так же возбудит программное исключение, и все пойдет по уже описанной схеме В программе-примере, которая представлена в следующем разделе, я покажу, как написать SEH-код, обрабатывающий подобные ошибки. В ней же Вы увидите и массу другого кода, не имеющего никакого отношения к SEH и обработке ошибок Он использует дополнительные возможности (о них — чуть позже), предоставляемые механизмом поддержки DLL отложенной загрузки. Если эта более «продвинутая» функциональность Вас не интересует, просто удалите дополнительный код.
Разработчики Visual C++ определили два кода программных исключений: VcppException(ERROR_SEVERTY_ERROR, ERROR_MOD_NOT_FOUND) и VcppException(ERROR_SEVERTIY_ERKOR, ERROR_PROC NOT_FOUND) Они уведомляют соответственно об отсутствии DLL и DLL-фупкции. Моя функция фильтра исключений DelayLoadDllExceptionFilter реагирует на оба кода. При возникновении любого другого исключения она, как и положено корректно написанному фильтру, возвращает EXCEPTION_CONTINUE_SEARCH. (Программа не должна «глотать» исключения, которые не умеет обрабатывать.) Однако, если генерируется один из приведенных выше кодов, функция __delayLoadHelper предоставляет указатель на структуру DelayLoadInfo, содержащую некоторую дополнительную информацию. Она определена в заголовочном файле DelayImp.h, поставляемом с Visual C++.
typedef struct DelayloadInfo
{
DWORD cb; // размер структуры
PCImgDelayDescr pidd; // "сырые" данные (все, что пока не обработано)
FARPROC * ppfn; // указатель на адрес функции, которую надо загрузить
LPCSTR szDll; // имя DLL
DelayLoadProc dlp; // имя или порядковый номер процедуры
HMODULE hmodCur; // nInstance загруженной библислеки
FARPROC pfnCur; // функция, которая будет вызвана на самом деле
DWORD dwLastError;// код ошибки
} DelayLoadInfo, * PDelayLoadInfo;
Экземпляр этой структуры данных создается и инициализируется функцией _delayLoadHelper, а ее элементы заполняются по мере выполнения задачи, связанной с динамической загрузкой DLL. Внутри Вашего SEH-фильтра элемет szDll указывает на имя загружаемой DLL, а элемент dlp — на имя нужной DLL-функции. Поскольку искать функцию можно как по порядковому номеру, так и по имени, dlp представляет собой следующее
typedef struct DelayLoadProc
{
BOOL fImportByName;
union
{
LPCSTR bzProcName;
DWORD dwOrdinal;
};
} DealyLoadProc;
Если DLL загружается, но требуемой функции в ней нет, Вы можете проверить злемент hmodCur, в котором содержится адрес проекции этой DLL, и элемент dwLastError, в который помещается код ошибки, вызвавшей исключение. Однако для фильтра исключения код ошибки, видимо, не понадобится, поскольку код исключения и так информирует о том, что произошло. Элемент pfnCur содержит адрес DLL-функции, и фильтр исключения устанавливает его в NULL, так как само исключение говорит о том, что _delayLoadHelper нс смогла найти этот адрес
Что касается остальных элементов, то сЬ служит для определения версии системы, pidd указывает на раздел, встроенный в модуль и содержащий список DLL отложенной загрузки, а ppfh — это адрес, по которому вызывается функция, если она найдена в DLL. Последние два параметра используются внутри _delayLoadHelper и рассчитаны на очень «продвинутое» применение — крайне маловероятно, что они Вам когда-нибудь понадобятся.
Итак, самое главное о том, как использовать DLL отложенной загрузки, я рассказал Но это лишь видимая часть айсберга — их возможности гораздо шире. В частности, Вы можете еще и выгружать эти DLL Допустим, что для распечатки документа Вашему приложению нужна специальная DLL. Такая DLL — подходящий кандидат на
отложенную загрузку, поскольку она требуется только на время печати документа. Когда пользователь выбирает команду Print, приложение обращается к соответствующей функции Вашей DLL, и та автоматически загружается. Все отлично, но, напечатав документ, пользователь вряд ли станет сразу же печатать что-то еще, а значит, Вы можете выгрузить свою DI.I, и освободить системные ресурсы. Потом, когда пользователь решит напечатать другой документ, DLL вновь будет загружена в адресное пространство Вашего процесса.
Чтобы DLL отложенной загрузки можно было выгружать, Вы должны сделать две вещи. Во-первых, при сборке исполняемого файла задать ключ /Delay:unload компоновщика. А во-вторых, немного изменить исходный код и поместить в точке выгрузки DLL вызов функции _FUnloadDelayLoadedDLL:
BOOL __FUnloadDelayLoadedDLL(PCSTR szDll);
Ключ /Delay:unload заставляет компоновщик создать в файле дополнительный раздел В нем хранится информация, необходимая для сброса ужe вызывавшихся DLLфункций, чтобы к пим снова можно было обратиться чсрсз _delayLoadHelper. Вызывая _FUnloadDelayLoadedDLL, Вы передаете имя выгружаемой DLL После зтого она просматривает раздел выгрузки (unload section) и сбрасывает адреса всех DLL-функций. И, наконец, __FUnloadDelayLoadedDLL вызывает FreeLibrary, чтобы выгрузить эту DLL
Обратите внимание на несколько важных моментов Во-первых, ни при каких условиях не вызывайте сами FreeLibrary для выгрузки DLL, иначе сброса адреса DLLфункции не произойдет, и впоследствии любое обращение к ней приведет к нарушению доступа. Во-вторых, при вызове _FUnloadDelayLoadedDLL в имени DLL нельзя указывать путь, а регистры всех букв должны бьть точно такими же, как и при передаче компоновщику в ключе /DelayLoad, в ином случае вызов __FUnloadDelayLoadedDLL закончится неудачно. В-третьих, если Вы вообще не собираетесь выгружать DLL отложенной загрузки, не задавайте ключ /Delay:unload — тогда Вы уменьшите размер своего исполняемого файла И, наконец, если Вы вызовете __FUnloadDelayLoadedDLL из модуля, собранного без ключа /Delay:unload, ничего страшного не случится: __FUnloadDelayLoadedDll, проигнорирует1 вьиов и просто вернет FALSE.
Другая особенность DLL отложенной загрузки в том, что вызываемые Вами функции по умолчанию связываются с адресами памяти, по которым они, как считает система, будут находиться в адресном пространстве процесса (О связывании мы поговорим чуть позжс ) Поскольку связываемые разделы DLL отложенной загрузки увеличивают размер исполняемого файла, Вы можете запретить их создание, указав ключ /Delay:nobind компоновщика. Однако связывание, как правило, предпочтительно, позтому при сборке большинства приложений этот ключ использовать не следует.
И последняя особенность DLL отложенной загрузки. Она, кстати, наглядно демонстрирует характерное для Microsoft внимание к деталям Функция _delayLoadHelper может вызывать предоставленные Вами функции-ловушки (hook functions), и они будут получать уведомления о том, как идет выполнение _delayLoadHelper, а также уведомления об ошибках. Кроме того, они позволяют изменять порядок загрузки DLL и формирования виртуального адреса DLL-функций.
Чтобы получать уведомления или изменить поведение _delayLoadHelper, нужно внести два изменения в свой исходный код Во-первых, Вы должны написать функцию-ловушку по образу и подобию DliHook, код которой показан на рис. 20-6 Моя функция DliHook не влияет на характер работы _delayLoadHelper. Если Вы хотите изменить поведение _delayLoadHelper, начните с DliHook и модифицируйте ее код так, как Вам требуется. Потом передайте ее адрес функции _delayLoadHelper.
В статически подключаемой библиотеке DelayImp. lib определены двс глобальные переменные типа PfriDliHouk: __pfnDliNotifyHook и __pfnDliFailureHook:
typedef FARPROC (WINAPI *pfnDliHook)( unsigned dliNotify, PDelayLoadInfo pdli);
Как видите, это тип данных, соответствующий функции, и он совпадает с прототипом моей DliHook. В DelayImp.lib эти две переменные инициализируются значением NULL, которое сообщает __delayLoadHelper, что никаких функций-ловушек вызывать не требуется. Чтобы Ваша функция-ловушка все же вызывалась, Вы должны присвоить ее адрес одной из этих переменных. В своей программе я пpocтo добавил на глобальном уровне две строки:
PfnDliHook __pfnDliNotifyHook = DliHook;
PfnDliHook __pfnDliFailureHook = DliHook;
Так что __delayLoadHelper фактически работает с двумя функциями обратного вызова: одна вызывается для уведомлений, другая — для сообщений об ошибках. Поскольку их прототипы идентичны, а первый параметр, dliNotify, сообщает о причине вызова функции, я всегда упрощаю себе жизнь, создавая одну функцию и настраивая на нее обе переменные.
Механизм отложенной загрузки DLL, введенный в Visual C++ 6.0, — вещь весьма интересная, и я знаю многих разработчиков, которые давно мечтали о нсм. Он будет полезен в очень большом числе приложений (особенно от Microsoft).
Переадресация вызовов функций
Запись о переадресации вызова функции (function forwarder) — это строка в разделе экспорта DLL, которая перенаправляет вызов к другой функции, находящейся в другой DLL Например, запустив утилиту DumpBin из Visual С++ для Kcrncl32dll в Windows 2000, Вы среди прочей информации увидите и следующее
С \winnt\system32>DumpBin -Exports Kernel32.dll (часть вывода опущена)
360 167 HeapAlloc (forwarded to NTDLL RtlAllocateHeap)
361 168 HeapCompact (000128D9)
362 1Ь9 HeapCreate (000126EF)
363 16A HeapCreateTagsW (0001279E)
364 16B HpapDpstroy (0001?750)
365 16C HeapExtend (00012773)
366 16D HeapFree (forwarded to NTDLL RtlFreeHeap)
367 16E HeapLock (000128ED)
368 16F HeapQueryTagW (000127B8)
369 170 HeapReAlloc (forwarded to NTDLL RtlReAllocateHeap)
370 171 HeapSize (forwarded to NTDLL RtlSizeHeap) (остальное тоже опущено)
Здесь есть четыре переадресованные функции Всякий раз, когда Ваше приложение вызывает HeapAlloc, HeapFree, HeapReAlloc или HeapSize, его ЕХЕ-модуль динамически связывается с Kernel32.dll При запуске ЕХЕ-модуля загрузчик загружает Kernel32dll и, обнаружив, что переадресуемые функции на самом деле находятся в NTDLLdll, загружаег и эту DLL Обращаясь к HeapAlloc, программа фактически вызы васт функцию Rltоса1еНеар из NTULL.dll А функции HeapAlloc вообще нет1
При вызове НеарАllос (см ниже) функция GetProcAddress просмотрит раздел экспорта Kernel32dll и, выяснив, чю НеарАllос — переадресуемая функция, рекурсивно вызовет сама себя для поиска RtlAllocateHeap в разделе экспорта NTDLL.dll.
GetProcAddress(GetModuleHandle("Kernel32"), "НеарАllос" );
Вы тоже можете применять переадресацию вызовов функций в своих DLL. Самый простой способ — воспользоваться директивой pragma:
// переадресация к функции из DllWork
#pragma comment(linker, "/export:SomeFunc=DllWork.SomeOtherFunc")
Эта директива сообщает компоновщику, что DLL должна экспортировать функцию SomeFunc, которая на самом деле реализована как функция SomeOtherFunc в модуле DlIWork dll Такая запись нужна для каждой переадресуемой функции
Перенаправление DLL
WINDOWS 98
Windows 98 не поддерживает перенаправление DLL.
Когда разрабатывались первые версии Windows, оперативная намять и дисковое пространство были крайне дефицитным ресурсом, так что Windows была рассчитана на предельно экономное их использование — с максимальным разделением между потребителями. В связи с этим Microsoft рекомендовала размещать все модули, используемые многими приложениями (например, библиотеку С/С++ и DLL, относящиеся к MFC) в системном каталоге Windows, где их можно было легко найти.
Однако со временем это вылилось в серьезную проблему: программы установки приложений то и дело перезаписывали новые системные файлы старыми или не полностью совместимыми. Из-за этого уже установленные приложения переставали работать. Но сегодня жесткие диски стали очень емкими и недорогими, оперативная память тоже значительно подешевела. Поэтому Microsoft сменила свою позицию на прямо противоположную; теперь она настоятельно рекомендует размещать все фаЙлы приложения в своем каталоге и ничего не трогать в системном каталоге Windows. Тогда Вашс приложение не нарушит работу других программ, и наоборот.
С той же целью Microsoft ввела в Windows 2000 поддержку перенаправления DLL (DLL redirection). Она заставляет загрузчик операционной системы загружав модули сначала из каталога Вашего приложения и, только если их там нет, искать в других каталогах
Чтобы загрузчик всегда проверял сначала каталог приложения, нужно всего лишь поместить туда специальный файл Его содержимое не имеет значения и игнорируется — вяжно только его имя: оно должно быть в виде AppName.local. Так, если исполняемый файл Вашего приложения — SupcrAppexe, присвойте перенаправляющему файлу имя SuperApp cxc local
Функция LoadLibrary(Ex) проверяет наличие этого файла и, ссли он есть, загружает молуль из каталога приложения; в ином случае LoadLibrary(Ex) работает так же, как и раньше.
Перенаправление DLL исключительно полезно для работы с зарегистрированными СОМ-объектами. Оно позволяет приложению размещать DLL с СОМ-объектами в своем каталоге, и другие программы, регистрирующие те же объекты, не будут мешать его нормальной работе.
Программа-пример DelayLoadApp
Эта программа, «20 DelayLoadApp.exe» (см. листинг на рис. 20-6), показывает, как использовать все преимущества DLL отложенной загрузки. Для демонстрации нам понадобится небольшой DLL-файл; он находится в каталоге 20-DclayLoadLib на компактдиске, прилагаемом к книге.
Так как программа загружает модуль «20 DelayLoadLib» c задержкой, загрузчик не проецирует его на адресное пространство процесса при запуске. Периодически вызывая функцию IsModuleLoaded, программа выводит окно, которое информирует, загружен ли модуль в адресное пространство процесса. При первом запуске модуль «20 DelayLoadd.lib» не загружается, о чем и сообщается в окне (рис. 20-4).
Рис. 20-4. DelayLoadApp сообщает, что модуль «20 DelayLoadLib» не загружен
Далсс программа вызывает функцию, импортируемую из Dl.L, и это заставляет __delayLoadHelper автоматически загрузить нужную DLL. Когда функция всрнст управление, программа выведет окно, показанное на рис. 20-5.
Рис. 20-5. DelayLoadApp сообщает, что модуль "20 DelayLoadLib» загружен
Когда пользователь закроет это окно, будет вызвана другая функция из той же DLL. В этом случае DLL не перезагружается в адресное пространство, но перед вызовом новой функции придется определять ее адрес.
Далее вызывается __FUnloadDelayLoadedDLL, и модуль «20 DelayLoadLib» выгружается из памяти. После очередного вьиова IsModuleLoaded на экране появляется окно, показанное на рис. 20-4. Наконец, вновь вызывается импортируемая функция, что приводит к повторной загрузке модуля «20 DelayLoadLib», a IsModuleLoaded открывает окно, как на рис. 20-5.
Если все нормально, то программа будет работать, как я только что рассказал. Однако, если перед запуском программы Вы удалите модуль «20 DelayLoadLib» или если в этом модуле не окажется одной из импортируемых функций, будет возбуждено исключение. Из моего кода видно, как корректно выйти из такой ситуации.
Наконец, эта программа демонстрирует, как настроить функцию-ловушку из DLL отложенной загрузки. Моя схематическая функция DliHook не делает ничего интересного. Тем не менее она перехватывает различные уведомления и показывает их Вам.
DelayLoadApp DelayLoadLibСвязывание модулей
Модификация базовых адресов действительно очень важна и позволяет существенно повысить производительность всей системы Но Вы можете сделать еще больше. Допустим, Вы должным образом модифицировали базовые адреса всех модулей своего приложения. Вспомните из главы 19, как загрузчик определяет адреса импортируемых идентификаторов он записывает виртуальные адреса идентификаторов в раздел импорта ЕХЕ-модуля. Это позволяет, ссылаясь на импортируемые идентификаторы, адресоваться к нужным участкам в памяти
Давайте поразмыслим Сохраняя виртуальные адреса импортируемых идентификаторов в разделе импорта ЕХЕ-модуля, загрузчик записывает их на те страницы памяти, где содержится этот раздел Здесь включается в paбoтy механизм копирования при записи, и их копии попадают в страничный файл. И у нас опять та же проблема, что и при модификации базовых адресов: отдельные части проекции модуля периодически сбрасываются в страничный файл и вновь подгружаются из него. Кроме того, загрузчику приходится преобразовывать адреса всех импортируемых идентификаторов (для каждого модуля), на что может понадобиться немалое время.
Для ускорения инициализации и сокращения объема памяти, занимаемого Вашим приложением, можно применить связывание модулей (module binding) Суть этой операции в том, что в раздел импорта модуля помещаются виртуальные адреса всех импортируемых идентификаторов. Естественно, она имеет смысл, только если проводится до загрузки модуля
В Visual Studio есть еще одна утилита, Bind.exe. Информацию о том, как ею пользоваться, Вы получите, запустив Bind.exe без ключей в командной строке. Она описана в документации Platform SDK, и я не буду ее здесь детально рассматривать Добавлю лишь, что в ней, как и в утилите Rebase, тоже нет ничего сверхъестественного: она просто вызывает функцию BindlmageEx для каждого указанного файла. Вот что представляет собой эта функция.
BOOL BindImageEx(
DWORD dwFlags, // управляющие флаги
PSTR pszImageName, // полное имя обрабатываемого файла
PSTR pszDllPath, // пугь для поиска образов файлов
PSTR pszSymbolPath, // путь для поиска О1ладочной информации
PIMAGEHLP_STATUS_ROUTINE StatusRoutine); // функция обратного вызова
Последний параметр, StatusRoutine, — адрес функции обратного вызова, к которой периодически обращается BindImageEx, позволяя отслеживать процесс связывания Прототип функции обратного вызова должен выглядеть так:
BOOL WINAPI StatusRoutine(
IMAGtHLP_STATUS_RLASON Reason, // причина неудачи
PSTR pszImageName, // полное имя обрабатываемою файла
PSTR pszDllName. // полное имя DLL
ULONG_PTR VA, // вычисленный виртуальный адрес
ULONG_PTR Parameter); // дополнительные сведения (зависят от значения Reason)
Когда Вы запускаете утилиту Bind, указывая ей нужный файл, она выполняет следующие операции.
Открывает раздел импорта указанного файла. Открывает каждую DLL, указанную в разделе импорта, и просматривает ее заголовок, чтобы определить предпочтительный базовый адрес. Отыскивает все импортируемые идентификаторы в разделе экспорта DLL Получает RVA (относительный виртуальный адрес) идентификатора, суммирует его с предпочтительным базовым адресом модуля и записывает полученное значение в раздел импорта обрабатываемого файла. Вносит в раздел импорта модуля некоторую дополнительную информацию, включая имена всех DLL, с которыми связывается файл, и их временные метки.
В главе 19 мы исследовали раздел импорта CaIc,exe с помощью утилиты DumpBln. В конце выведенного ею текста можно заметить информацию о связывании, добавленную при операции по п 5 Вот эти строки
Header contains the following bound import information: Bound to SHELL32 dll [36E449EO] Mon Мяг 08 14:06:24 1999 Bound to MSVCRT dll [36BB8379] Fri Feb Ob 15:49:13 1999 Bound to ADVAPI32.dll [36E449E1] Mon Mar 08 14:06:25 1999 Bound to KERNEL32 dll [36DDAD55] Wed Mar 03 13:44:53 1999 Bound to GDI32 dll [36E449EO] Mon Mar 08 14:06:24 1999 Bound to USER32 dll [36E449EO] Mon Mar 08 14:06:24 1999
Здесь видно, с какими модулями связан файл Calc.exe, а номер в квадратных скобках идентифицирует время создания каждого DLL-модуля Это 32-разрядное значение расшифровывается и отображается за квадратными скобками в более привычном нам виде
Утилита Bind использует два важных правила.
При инициализации процесса все необходимые DLL действительно загружаются по своим предпочтительным базовым адресам. Вы можете соблюсти это правило, применив утилиту Rebase. Адреса идентификаторов в разделе экспорта остаются неизменными со времени последнего связывания. Загрузчик проверяет это, сравнивая временную метку каждой DLL со значением, сохраненным при операции по п. 5.
Конечно, если загрузчик обнаружит, что нарушено хотя бы одно из правил, он решит, что Bind не справилась со своей задачей, и самостоятельно модифицирует раздел импорта исполняемого модуля (по обычной процедуре). Но если загрузчик увидит, что модуль связан, нужные DLL загружены по предпочтительным базовым
адресам и временные метки корректны, он фактически ничего делать не будет, и приложение сможет немедленно начать свою работу'
Кроме того, приложение не потребует лишнего места в страничном файле. И очень жаль, что многие коммерческие приложения поставляются без должной модификации базовых адресов и связывания.
О'кэй, теперь Вы знаете, что все модули приложения нужно связывать. Но вот вопрос когда? Если Вы свяжете модули в своей системе, Вы привяжете их к системным DLL, установленным на Вашем компьютере, а у пользователя могут быть установлены другие версии DLL. Поскольку Вам заранее не известно, в какой операционной системе (Windows 98, Windows NT или Windows 2000) будет запускаться Ваше приложение и какие сервисные пакеты в ней установлены, связывание нужно проводить в процессе установки приложения.
Естественно, если пользователь применяет конфигурацию с альтернативной загрузкой Windows 98 и Windows 2000, то для одной из операционных систем модули будут связаны неправильно. Тот же эффект даст и обновление операционной системы установкой в ней сервисного пакета. Эту проблему ни Вам, ни тем более пользователю решить не удастся. Microsoft следовало бы поставлять с операционной системой утилиту, которая автоматически проводила бы повторное связывание всех модулей после обновления системы. Но, увы, такой утилиты нет.
Уведомление DLL_PROCESS_ATTACH
Система вызывает DllMain с этим значением параметра fdwReason сразу после того, как DLL спроецирована на адресное пространство процесса. А это происходит, только когда образ DLL-файла проецируется в первый раз. Если затем поток вызовет LoadLibrary(Ex) для уже спроецированной DLL, система просто увеличит счетчик числа пользователей этой DLL; так что DllMain вызывается со значением DLL_PROCESS_ATTACH лишь раз.
Обрабатывая DLL_PROCESS_ATTACH, библиотека должна выполнить в процессе инициализацию, необходимую ее функциям. Например, в DLL могут быть функции, которым нужна своя куча (создаваемая в адресном пространстве процесса). В этом случае DllMain могла бы создать такую кучу, вызвав HeapCreate при обработке уведомления DLL_PROCESS_ATTACH, а описатель созданной кучи сохранить в глобальной переменной, доступной функциям DLL
При обработке уведомления DLL_PROCESSATTACH значение, возвращаемое функцией DllMain, указывает, корректно ли прошла инициализация DLL. Например, если вызов HeapCreate закончился благополучно, следует вернуть TRUE. А если кучу создать не удалось — FALSE. Для любых других значений fdwReason — DLL_PROCESS_DETACH, DLL_THREAD_ATTACH или DLL_THREAD_DETACH — значение, возвращаемое DllMain, системой игнорируется.
Конечно, где-то в системе должен быть поток, отвечающий за выполнение кода DllMain. При создании нового процесса система выделяет для него адресное пространство, куда проецируется ЕХЕ-файл и всс необходимые ему DLL-модули Далее создается первичный поток процесса, используемый системой для вызова DllMain из каждой DLL со значением DLL_PROCESS_ATTACH. Когда все спроецированные DLL ответят на это уведомление, система заставит первичный поток процесса выполнить стартовый код из библиотеки С/С++, а потом — входную функцию ЕХЕ-файла (main, wmain, WinMain или wWinMain). Если DllMain хотя бы одной из DLL вернет FALSE, сообщая об ошибке при инициализации, система завершит процесс, удалив из его адресного пространства образы всех файлов, после этого пользователь увидит окно с сообщением о том, что процесс запустить не удалось. Ниже показаны соответствующие окна для Windows 2000 и Windows 98,
Теперь посмотрим, что происходит при явной загрузке DLL Когда поток вызывает LoadLibrary(Ex), система отыскивает указанную DLL и проецирует ее на адресное иросчранство процесса Затем вызывает DllMain со значением DLL_PROCESS_ATTACH, используя поток, вызвавший LoadLibrary(Ex) Как только DllMain обработает унедомление, произойдет возврат из LoadLibrary(Ex), и поток продолжит работу в обычном режиме. Если DllMain вернет FALSE (неудачная инициализация), система автоматически отключит образ файла DLL от адресного пространства процесса, а вызов LoadI.ibrary(Ex) даст NULL.
Уведомление DLL_PROCESS_DETACH
При отключении DLL от адресного пространства процесса вызывается ее функция DllMain со значением DLLPROCESS_DETACH в параметре fdwReason. Обрабатывая это значение, DLL должна провести очистку в данном процессе Например, вызвать HeapDestroy, чтобы разрушить кучу, созданную ею при обработке уведомления DLL__PROCESS_ATTACH. Oбpaтите внимание: если функция DllMain вернула FALSE, получив уведомление DLL_PROCESS_ATTACH, то ее нельзя вызывать с уведомлением DLL_PROCESS_DETACH. Если DLL отключается из-за завершения процесса, то за выполнение кода DllMain отвечает поток, вызвавший ExitProcess (обычно эчо первичный поток приложения). Когда Ваша входная функция возвращает управление стартовому коду из библиотеки С/С++, тот явно вызывает ExitProcess и завершает процесс
Если DLL отключается в результате вызова FreeLibrary или FreeLibraryAndExitThread, код DllMain выполняется потоком, вызвавшим одну из этих функций. D случае обращения к FreeLibrary управление не возвращается, пока DllMain не закончит обработку уведомления DLL_PROCESS_DETACH.
Учтите также, что DLL может помешать завершению процесса, если, например, ее DllMain входит в бесконечный цикл, получив уведомление DLL_PROCESS_DETACH Операционная система уничтожает процесс только после того, как все DLL-модули обработают уведомление DLL_PROCESS_DETACH
Рис. 20-2. Операции, выполняемые системой при вызове потоком функции LoadLibrary
Если процесс завершается в результате вызова TerminateProcess, система не вызывает DllMain со значением DLL_PROCESS_DETACH. А значит, ни одна DLL, спроецированная на адресное пространство процесса, не получит шанса на очистку до завершения процесса. Последствия могут быть плачевны ~ вплоть до потери данных. Вызывайте TerminateProcess только в самом крайнем случае!
На рис. 20-2 показаны операции, выполняемые при вызове LoadLibrary, а на рис. 20-3 - при вызове FreeLibrary.
Рис. 20-3. Операции, выполняемые системой при вызове потоком функции FreeLibrary
Уведомление DLL_THREAD_ATTACH
Когда в процессе создается новый поток, система просматривает все DLL, спроецированные в данный момент на адресное пространство этого процесса, и в каждой из таких DLL вызывает DllMain со значением DLL_THREAD_ATTACH. Тем самым она уведомляет DLL-модули о необходимости инициализации, связанной с данным потоком. Только что созданный поток отвечает за выполнение кода в функциях DllMain всех
DLL Работа его собственной (стартовой) функции начинается лишь после того, как все DLL-модули oбpaбoтaют уведомление DLL_THREAD_ATTACH.
Если в момент проецирования DLL на адресное пространство процесса в нем выполняется несколько потоков, система не вызывает DllMain со значением DLL_ THREAD_ATTACH ни для одного из существующих потоков. Вызов DllMain c этим значением осуществляется, только если DLL проецируется на адресное пространство процесса в момент создания потока.
Обратите также внимание, что система не вызывает функции DllMain co значением 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 заканчивают обработку этого уведомления.
NOTE:
Если поток завершается из-за того, что другой поток вызвал для него TerminateThread, система не вызывает DllMain со значением DLL_THREAD_DETACH Следовательно, ни одна DLL, спроецированная на адресное пространство процесса, не получит шанса на выполнение очистки до завершения потока, что может привести к потере данных Поэтому TetminateThread, как и TerminateProcess, можно использовать лишь в самом крайнем случае!
Если при отключении DLL еще выполняются какие-то потоки, то для них DllMain не вызывается со значением DLL_THREAD_DETACH Вы можете проверить это при обработке DLL_PROCESS_DETACH и провести необходимую очистку.
Ввиду упомянутых выше правил не исключена такая ситуация: поток вызывает LoadIibrary для загрузки DLL, в результате чего система вызывает из этой библиотеки DllMain со значением DLL_PROCESS_ATTACH. (В этом случае уведомление DLL_ THREAD_ATTACH не посылается.) Затем поток, загрузивший DLL, завершается, что приводит к новому вызову DllMain — на этот раз со значением DLL_THREAD_DETACH Библиотека уведомляется о завершении потока, хотя она не получала DLL_ THREAD_ATTACH, уведомляющего о его подключении. Поэтому будьте крайне осторожны при выполнении любой очистки, связанной с конкретным потоком К счастью, большинство программ пишется так, что LoadLibrary и FreeLibrary вызываются одним потоком.