Создание эффективных WIN32-приложений с учетом специфики 64-разрядной версии Windows

Отложенная загрузка 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).


    Содержание раздела