Запуск библиотеки dll как приложения
Для начала нужно разобраться, что же такое DLL.
DLL — это сокращение фразы Dynamic-Link Library — динамически подключаемая библиотека.
Первоначальная идея введения библиотек заключалась в более рациональном использовании ресурсов ПК, таких как ОЗУ и НЖМД. Предполагалось, что различные приложения будут использовать одну библиотеку, тем самым ликвидируя проблему сохранения одинаковых участков кода в различных местах и многократную загрузку одинаковых участков в ОЗУ.
В дальнейшем, было решено увеличить эффективность разработки приложений за счёт модульности. Обновление самих DLL должно было позволить расширять и усовершенствовать систему, не затрагивая при этом всех приложений. Однако, данная идея столкнулась с проблемой одновременного требования приложениями разных, не полностью совместимых версий библиотек, что приводило к сбоям. Данная проблема была названа "DLL Hell".
По сути, DLL - это относительно независимый участок кода, имеющий свою оболочку.
Оболочка DLL имеет секции импорта/экспорта.
Секция экспорта перечисляет те идентификаторы объектов (классы, функции, переменные), доступ к которым предоставляет данная DLL. В большинстве случаев именно эта секция вызывает особый интерес у разработчиков. Тем не менее, DLL может вовсе не содержать секцию экспорта. Кроме функций, к которым можно получить доступ извне (exported), существуют также внутренние функции (internal), которые спрятаны от "посторонних глаз" и могут использоваться лишь кодом самой DLL.
Секция импорта предназначена для связи данной DLL с другими.
Большинство библиотек импортируют функции из системных DLL (kernel32.dll, user32.dll и др.). Иногда в стандартный список нужно добавить другие библиотеки, например ws2_32.dll для работы с Socket'ами.
DLL не может выполняться сама по себе, поэтому требует хост-процесс (главный процесс, в котором предполагается использование DLL).
Перед тем, как библиотеку можно будет использовать, её необходимо загрузить в область памяти хост-процесса. В exe-файле компилятором будет сгенерирована инструкция вызова заданной функции (call).
Т.к. DLL расположена в адресном пространстве процесса, то вызов функции из библиотеки будет иметь вид:
При выполнении команды call, процессор передает управление на код с адреса 010199, и выполняет его до команды ret, а затем возвращает управление команде следующей за call. Код который вызывается командой call называется процедурой.
Файл, являющийся динамически загружаемой библиотекой не всегда носит расширение *.dll.
Есть также: *.cpl (библиотеки, используемые апплетом панели управления) и *.ocx.
Библиотеки удобно использовать для разделения приложения на части, выполняющие разные роли.
Например, приложение, обеспечивающее работу с базами данных может быть разделено на часть, отвечающую за выборку данных из базы, находящейся на сервере, и, часть, которая отвечает за визуальное управление приложением. Это явный пример той модульности, о которой я упоминал выше. Вы в праве изменять принципы обмена данными с сервером БД, не затрагивая при этом работу с визуальной частью.
- 1.3. Необходимость внедрения DLL. Нужно ли это?
Настало время ответить на вопрос: Когда же нужно использовать DLL?
Если вы разрабатываете небольшой проект или тестовое приложение, то внедрение DLL будет для вас пустой тратой времени и сил. Тратой времени: не только времени на создание библиотеки, но ещё и времени, необходимого для загрузки DLL в память хост-процесса. До того, как вы будете использовать объекты из DLL, вам рано или поздно прийдётся выгрузить содержимое в память. А это требует некоторого времени.
Однако, если вашу DLL будет использовать более, чем одна копия приложения (или несколько различных приложений) - наступит явный выигрыш.
Для эксперимента запустите Microsoft Office. Первый запуск долгий. Это обусловлено тем, что все необходимые модули загрузаются в оперативную память. Теперь полностью закройте Office и снова откройте его! Окно появится почти мгновенно. Это обусловлено тем, что модули хранились в ОЗУ (система не тронет их, пока ей не понадобится память для других целей).
При создании больших проектов лучше сразу пытаться представить себе части, которые могут быть уникальными и требующими частого усовершенствования.
Но не старайтесь придать вашему проекту чрезвычайной "уникальности" за счёт помещения каждой функции в отдельную библиотеку! Это можно делать, только зачем тратить время для загрузки каждого модуля?
- 2.1. Создаём первую DLL своими руками.
Пришло время постепенно перейти от теории к практике.
Открываем IDE (для написании данной статьи я использовал RAD Studio 2010).
Переходим к File -> New -> Other -> Dynamic-Link Library.
Перед нами возникает диалог:
Source Type - язык, на котором ведётся разработка.
Use VCL - использование библиотеки визуальных компонентов.
Multi Threaded - опция, указывающая на то, будет ли использоваться многопоточность в данной DLL (VCL уже подразумевает в себе многопоточность).
VC++ Style DLL - опция, указывающая на то, будет ли DLL совместима с компиляторами Microsoft.
Если в совместимости нет нужды и опция не выбрана, то DLL будет иметь точку входу с таким прототипом:
Оставим диалог без изменений и нажмём "ОК".
Перед нами появится шаблон минимальной DLL. Сохраним его.
Как вы помните, сама по себе DLL работать не может, ей нужен клиент. Поэтому, для удобства сразу создадим новый проект VCL Forms Application.
Для этого переходим в Project Manager, вызываем контекстное меню у нашей Project Group и переходим к Add New Project -> VCL Forms Application.
Для удобста я назвал проекты TestDLL и TestVCL соответственно (и сохранил их в одном каталоге - это избавит меня от копирования DLL или указания абсолютного пути):
Без изменений запускаем TestVCL, сохраняем и переключаемся к проекту TestDLL (дабл-клик на проекте в Project Manager).
Переходим к Run -> Parameters и в поле Host Application указываем путь к нашему проекту TestVCL.
К шаблону DLL добавляем функцию, которая будет вычислять сумму и выводить результат на экран:
Сохраняем. Запускаем. DLL мы подготовили. Теперь необходимо узнать, как же подключить DLL к проекту. Сделать это можно тремя способами. Рассмотрим их подробнее.
При неявной загрузке DLL загружается (проецируется на адресное пространство вызывающего процесса) при его создании. Если при загрузке возникает ошибка - процесс останавливается и разрушается.
Для выполнения неявной загрузки приложению требуются:
- Заголовочный файл (*.h) с прототипами функций, описаниями классов и типов, которые используются в приложении.
- Библиотечный файл (*.lib), в котором описывается список экспортируемых из DLL функций (переменных), и их смещения, необходимые для правильной настройки вызовов функций.
В проекте TestVCL подключим наш заголовочный файл:
Далее, объявим прототип:
Теперь осталось только вызвать функцию там, где это необходимо.
Чтобы убедится в том, что всё работает, прописываем в конструкторе формы:
Запускаем и смотрим на результат. Думаю, "3 + 2 = 5" всех устраивает.
Для того, чтобы выполнить явную загрузку программист должен попыхтеть, управляя DLL через функции WinAPI.
Наиболее часто рассматриваемые WinAPI функции:
DisableThreadLibraryCalls, FreeLibrary, FreeLibraryAndExitThread, GetModuleFileName, GetModuleHandle, GetProcAddress, LoadLibrary .
При этом, основными функциями являются:
LoadLibrary[Ex] - позволяют загрузить DLL в адресное пространство хост-процесса.
FreeLibrary - функция, используемая для явной выгрузки DLL.
GetProcAddress - функция, позволяющая получить виртуальный адрес экспортируемой из DLL функции(или переменной) для ее последующего вызова.
Общая методика выглядит так:
1. Загрузить DLL с помощью LoadLibrary.
2. Получить указатели на необходимые объекты с помощью GetProcAddress.
3. Выгрузить DLL после завершения всех действий.
Теперь возникает вопрос, как же проверить теорию на практике?
Всё, что нужно, это добавить TestDLL.lib к проекту (также, как и при неявной загрузке).
А дальше, для проверки снова пишем в конструкторе формы:
И на экране снова красуется победная надпись "3 + 2 = 5"
Остался один неосвещенный вопрос. Почему же название функции "ShowSum" мы ищем в библиотеке с нижним подчёркиванием?
Виновато во всём декорирование имён.
Прототип функции Test(int); мог быть преобразован компилятором, например в ?Test@@YAKH@Z.
Естественно, такое декорирование нам вообще не по душе. Избавиться от него можно объявляя все экспортируемые функции с модификатором extern "C" - тогда компилятор не будет искажать имя функции.
Однако, как мы видим, нижние подчёркивание всё же добавилось.
Это один из нюансов среды C++ Builder. Однако, можно отучить его добавлять нижнее подчёркивание таким образом:
Project -> Options -> C++ Compiler -> Output -> Generate underscores on symbol names - перевести в состояние false.
Для чего же нужна отложенная загрузка?
Представьте себе ситуацию: вы написали приложение, использующее стандартные системные библиотеки вашей новой операционной системы, скажем, для проверки орфографии.
Даёте это приложение пользователю, который использует ОС более старой версии, чем у вас и в этой ОС нет функций для проверки орфографии. А пользователю это не сильно и надо. Приложение будет работать, пока не обратится к необходимой функции. То есть, фактически, DLL не нужна до обращения к определённой функции. Исключение отсутствия можно обработать и выдать пользователю предупреждение с просьбой обновить библиотеки своей ОС (и т.п.).
Использование отложенной загрузки DLL в C++ Builder мало отличается от неявной загрузки.
В проект добавляется заголовочный (*.h) файл с описаниями и библиотечный файл (*.lib).
Далее, переходим в Project -> Options -> C++ Linker -> Advanced -> Delay Load DLLs и вписываем название нашей библиотеки (TestDLL.dll).
Когда библиотека теряет свою необходимость её нужно явно выгрузить с помощью __FUnloadDelayLoadedDLL. В качестве параметра передаём имя DLL (с расширением + параметр регистрозависим).
Если вы используете многопоточные приложения - убедитесь, что все потоки завершили работу с DLL.
Примечание: Нельзя использовать отложенную загрузку для библиотек, имеющих секцию импорта (т.е. использующих другие библиотеки), а также Kernel32.dll и RTLDLL.dll (т.к. функции поддержки отложенной загрузки как раз и находятся в последней).
3. Заключение.
Данная статья даёт вам представление о возможных вариантах использования DLL в ваших проектах.
При внимательном ознакомлении с методами загрузки можно выбрать наиболее оптимальный вариант.
Подведя итоги можно выявить плюсы и минусы описанных методов:
Явная загрузка:
+ контроль и управление процессом жизни DLL.
- управлением DLL занимается программист посредством WinAPI.
Неявная загрузка:
+ все заботы берет на себя компилятор и сборщик.
- ресурсы заняты всё время жизни приложения.
Отложенная загрузка:
+ все заботы берет на себя компилятор и сборщик.
+ возможность использования приложения с не полностью совместимыми DLL.
- необходимость усиленного контроля за многопоточными приложениями.
Для чего вообще нужно внедрять свои DLL-ки в чужие процессы и устанавливать там хуки? Для того, чтобы понять какие функции будет вызывать это приложение, с какими параметрами и что эти функции вернут. Таким образом мы можем понять внутреннюю логику работы этого приложения, узнать к каким файлам оно пытается получить доступ, какие данные пересылает по сети, мы можем добавить в него логирование, профилирование, отладить баг, получить из приложения некоторые данные или наоборот — добавить в его интерфейс что-нибудь нужное нам. Хуки использует известная утилита Spy++ для работы с окнами приложений, DirectX-отладчики вроде RenderDoc, некоторые утилиты от SysInternals, программы типа ApiMonitor и т.д.
Некоторое время назад я писал вводные статьи об использовании хуков на примерах библиотек Microsoft Detours и madCodeHook (если вы совсем не знакомы с хуками — это то, с чего можно начать). Описанных там техник достаточно для работы с обычными приложениями, но время не стоит на месте и вот сегодня у нас уже есть Metro Modern-приложения, входящие в комплект Windows 8 и Windows 10, а также распространяющиеся через Microsoft Store программы сторонних производителей. К примеру, новый браузер Spartan — это Modern-приложение. Внедрение DLL в эти процессы имеет свои особенности и на первый взгляд может даже показаться невозможным, но это не так. В этой статье я расскажу, как мы можем залезть внутрь Modern-приложения и установить в нём хуки в своих коварных целях.
32-битные и 64-битные приложения
Операционная система Windows бывает 32-разрядная и 64-разрядная. Соответственно, Modern-приложения тоже могут быть 32-битные либо 64-битные. При загрузке приложения в магазин Microsoft разработчик собирает обе версии (плюс еще, возможно, ARM), а Windows уже сам решает, какую загрузить пользователю. Отсюда следует очевидный вывод, что DLL-ка, которую мы будем внедрять в Modern-приложение, тоже должна быть в двух вариантах. Менее очевидный вывод в том, что приложение, которое будет «забрасывать» нашу DLL-ку в чужой процесс, тоже должно быть соответствующей битности. Нам ведь нужно запустить внутри чужого процесса поток, который загрузит нашу библиотеку и вызовет из неё какую-то функцию. Делается это через вызов CreateRemoteThread(Ex), а вот уже она требует одинаковой битности вызывающего и вызываемого процессов (а иначе как передавать адреса?).
- InjectedLibrary32.dll
- InjectedLibrary64.dll
- Injector32.exe
- Injector64.exe
Доступ к внедряемой DLL
Как вы, возможно, знаете, Modern-приложения живут в своих песочницах, откуда они имеют доступ только к своей папке и некоторым другим доступным для них (ограниченным) ресурсам ОС. К примеру, Modern-приложение не может вот просто так взять и открыть любой файл с диска. Для нас это означает то, что попытавшись заставить чужое приложение подгрузить нашу библиотеку в своё адресное пространство — мы получим ошибку доступа. Это можно легко увидеть, воспользовавшись, к примеру, утилитой ProcMon.
Что же делать? Разрешить доступ к файлу внедряемой библиотеки для Modern-приложений. Для этого есть системная утилита icacls. Вот эта команда открывает доступ к файлу на диске для всех Modern-приложений:
Теперь ошибки доступа у нас не будет.
Линковка DLL-ки
Но не всё так просто. DLL-ка может иметь зависимости. И даже если вам кажется, что их нет — возможно, вы забываете о зависимости от VC++ Runtime Library. По умолчанию созданная в Visual Studio С++ библиотека предполагает динамическую линковку с рантайм-библиотекой С++. Это означает, что когда какое-то приложение захочет загрузить вашу библиотеку — функция LoadLibrary() первым делом увидит, что вам необходима, к примеру, библиотека msvcp90r.dll и попытается её найти, что вобщем-то, получится если речь идёт об обычном (не Modern) приложении и наличии соответствующего VC++ Redistribution Package. Что касается Modern-приложений, то порядок поиска DLL-ок совсем другой. Если коротко: будучи вызванной из Modern-приложения, LoadLibrary() не найдёт библиотеки VC++ Redistribution Package, даже если они до этого были успешно установлены.
- Копировать VC++ Runtime Library в папку Modern-приложения
- Копировать VC++ Runtime Library в %SystemRoot%\system32
- Прописать библиотеки в специальный раздел в реестре (HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs)
- Использовать статическую линковку (когда код VC++ Runtime Library включается в тело нашей DLL-ки)
Первые два способа крайне некрасивы (а без аккаунта администратора и невозможны), третий — сработает, но влияет на глобальную конфигурацию ОС, что тоже может быть чревато последствиями. Так что самый простой вариант — статическая линковка (ключи /MTd и /MT на вкладке Code Generation).
Цифровая подпись
Решив все вопросы с доступом к файлу DLL-ки может показаться, что у нас всё получилось — ProcMon не показывает никаких ошибок доступа. Но инъекция всё равно не работает — DLL-ка не появляется в списке загруженных модулей процесса. Что же пошло не так? На этот вопрос нам даёт ответ просмотр Windows Event Viewer.
Операционная система не хочет загружать нашу DLL-ку, поскольку она не подписана. В этом месте мне стало тревожно, поскольку я уж было подумал, что все модули, загружаемые в Modern-приложение, должны быть подписаны цифровой подписью одного издателя (разработчика). Но, к счастью, это не так. Windows требует от DLL-ки просто наличия валидной цифровой подписи, но не требует её соответствия подписи автора приложения. Т.е. мы можем взять подписанную DLL-ку от Хрома и забросить её в Spartan. Что же делать нам? Получить сертификат и подписывать нашу DLL-ку.
Да, это серьёзно усложняет работу. Но ведь в принципе, если мы разрабатываем нормальный продукт, а не какие-нибудь вирусы\трояны\малвари\кейлоггеры\локеры — у нас не будет проблем с получением сертификата и его использованием для подписи DLL-ки. Вообще, я считаю эту защиту одной из самых серьёзных шагов Microsoft по защите пользовательских данных и изоляции Modern-приложений в песочнице. Фактически от желающего пробросить мостик к приложению требуют предъявить документы и идентифицироваться, а это остановит 99% скрипт-киддисов.
Права доступа на канал связи
Ок, теперь наша DLL-ка внедрена в чужой процесс, она может вешать хуки на разные системные функции, делать что-то полезное, но… Сама она живёт в той же песочнице, что и родительский процесс, то есть ни передать данные «наружу», ни получать команды «извне» она не может, ведь для этого нужно создать канал связи, на который она по умолчанию не имеет прав. Как решать эту задачу я уже отдельно рассказывал вот в этой статье, а здесь просто упоминаю в качестве пункта, о котором важно не забыть.
Использование DLL в программе на Visual C++
Многие знают, что существует два основных способа подключить DLL к программе - явный и неявный.
При неявном подключении (implicit linking) линкеру передается библиотека импорта (обычно имеет расширение lib), содержащая список переменных и функций DLL, которые могут использовать приложения. Обнаружив, что программа обращается хотя бы к одной из них, линкер добавляет в целевой exe-файл таблицу импорта . Таблица импорта содержит список всех DLL, которые использует программа, с указанием конкретных переменных и функций, к которым она обращается. Позже, когда exe-файл будет запущен, загрузчик проецирует все DLL, перечисленные в таблице импорта, на адресное пространство процесса; в случае неудачи весь процесс немедленно завершается.
При явном подключении (explicit linking) приложение вызывает функцию LoadLibrary, чтобы загрузить DLL, затем использует функцию GetProcAddress, чтобы получить указатели на требуемые функции (или переменные), а по окончании работы с ними вызывает FreeLibrary, чтобы выгрузить библиотеку и освободить занимаемые ею ресурсы.
Каждый из способов имеет свои достоинства и недостатки. В случае неявного подключения все библиотеки, используемые приложением, загружаются в момент его запуска и остаются в памяти до его завершения (даже если другие запущенные приложения их не используют). Это может привести к нерациональному расходу памяти, а также заметно увеличить время загрузки приложения, если оно использует очень много различных библиотек. Кроме того, если хотя бы одна из неявно подключаемых библиотек отсутствует, работа приложения будет немедленно завершена. Явный метод лишен этих недостатков, но делает программирование более неудобным, поскольку требуется следить за своевременными вызовами LoadLibrary и соответствующими им вызовами FreeLibrary, а также получать адрес каждой функции через вызов GetProcAddress.
Теперь рассмотрим, как каждый из перечисленных методов используется на практике. Для этого будем считать, что у нас есть библиотека MyDll.dll, которая экспортирует переменную Var, функцию Function и класс Class. Их объявления содержатся в заголовочном файле MyDll.h, который выглядит следующим образом:
Кроме того, будем считать, что библиотека импорта содержится в файле MyDll.lib.
Неявное подключение
Это наиболее простой метод подключения DLL к нашей программе. Все, что нам нужно - это передать линкеру имя библиотеки импорта, чтобы он использовал ее в процессе сборки. Сделать это можно различными способами.
Теперь можно использовать в программе любые переменные, функции и классы, содержащиеся в DLL, как если бы они находились в статической библиотеке. Например:
Явное подключение
Загрузка DLL
Как уже говорилось ранее, при явном подключении DLL программист должен сам позаботиться о загрузке библиотеки перед ее использованием. Для этого используется функция LoadLibrary, которая получает имя библиотеки и возвращает ее дескриптор. Дескриптор необходимо сохранить в переменной, так как он будет использоваться всеми остальными функциями, предназначенными для работы с DLL.
В нашем примере загрузка DLL выглядит так.
Вызов функций
После того как библиотека загружена, адрес любой из содержащихся в ней функций можно получить с помощью GetProcAddress, которой необходимо передать дескриптор библиотеки и имя функции. Затем функцию из DLL можно вызывать, как обычно. Например:
Обратите внимание на приведение указателя к ссылке на тип FARPROC. FARPROC - это указатель на функцию, которая не принимает параметров и возвращает int. Именно такой указатель возвращает функция GetProcAddress. Приведение типа необходимо, чтобы умиротворить компилятор, который строго следит за соответствием типов параметров оператора присваивания. Альтернативный подход заключается в использовании оператора typedef с последующим приведением значения, возвращаемого GetProcAddress, к указателю на функцию с нужным прототипом.
Доступ к переменным
Хотя это не всегда очевидно из документации, получить указатель на переменную из DLL можно, используя все ту же функцию GetProcAddress. В нашем примере это выглядит так.
Использование классов
Сразу замечу, что в общем случае не рекомендуется размещать классы в библиотеках, подключаемых явно. Приемлемым можно считать только подход, который исповедует COM, при котором объекты класса создаются и разрушаются внутри DLL (для этого используются экспортируемые глобальные функции), а сам класс содержит исключительно виртуальные методы.
Однако предположим, что у нас нет доступа к исходным кодам библиотеки, содержащей класс, а использование других типов подключения DLL по каким-то причинам невозможно. Классом удастся воспользоваться и в этом случае, но для достижения цели придется проделать дополнительную работу.
Сначала задумаемся, почему объекты класса из явно подключаемой библиотеки нельзя использовать, как обычно. Дело в том, что при создании объекта класса компилятор генерирует вызов его конструктора. Но линкер не может разрешить этот вызов, поскольку адрес конструктора будет известен только в процессе выполнения программы. В результате сборка программы закончится неудачно. Такая же проблема возникает при вызове невиртуальных методов класса. С другой стороны, вызов виртуальных методов возможен, так как он осуществляется через таблицу виртуальных функций (vtable). Так, следующий фрагмент откомпилируется и слинкуется нормально (хотя, конечно, вызовет ошибку в процессе выполнения):
Приведенные выше рассуждения подсказывают решение проблемы. Коль скоро неявный вызов конструктра невозможен, мы можем вызвать его вручную, предварительно получив его адрес и выделив память под объект. Затем можно вызывать невиртуальные методы, получая их адреса с помощью GetProcAddress. Виртуальные методы можно вызывать, как обычно. Кроме того, необходимо не забыть вручную вызвать деструктор объекта, прежде чем выделенная для него память будет освобождена.
Продемонстрирую все сказанное на примере. Сначала мы выделяем память для объекта и вызываем для него конструктор. Память можно выделить как на стеке, так и в куче (с помощью оператора new). Рассмотрим оба варианта.
Обратите внимание на использование операторов .* и ->* для вызова функции-члена класса по указателю на нее. Этими операторами мы будем пользоваться и дальше.
ПРИМЕЧАНИЕ
Как правило, имена функций, экспортируемых из DLL, искажаются линкером. Поэтому вместо понятного имени, такого как "Constructor", получается совершенно нечитабельное имя вида "??0Class@@QAE@XZ". В рассматриваемом примере я назначил переменным и функциям нормальные имена при помощи def-файла следующего содержания:
Невиртуальные методы класса вызываются так же, как и конструктор, например:
Виртуальные методы вызываются непосредственно (как это делается для обычных классов). Хотя DLL и экспортирует их, явно получать их адреса с помощью GetProcAddress не требуется. Отсюда следует вывод: если все методы класса являются виртуальными, использование объектов класса из явно подключаемой библиотеки практически ничем не отличается от использования объектов любого другого класса. Разница только в том, что конструктор и деструктор для таких объектов придется вызывать вручную.
В нашем примере виртуальная функция вызывается так.
После того, как работа с объектом завершена, его нужно уничтожить, вызвав для него деструктор. Если объект был создан на стеке, деструктор необходимо вызвать до его выхода из области видимости, иначе возможны неприятные последствия (например, утечки памяти). Если объект был распределен при помощи new, его необходимо уничтожить перед вызовом delete. В нашем примере это выглядит так.
До сих пор я ничего не говорил о статических переменных и функциях класса. Дело в том, что они практически ни чем не отличаются от обычных функций и переменных. Поэтому к ним можно обращаться, используя уже известные нам методы. Например:
Выгрузка библиотеки
После того, как работа с библиотекой закончена, ее можно выгрузить, чтобы она не занимала системные ресурсы. Для этого используется функция FreeLibrary, которой следует передать дескриптор освобождаемой библиотеки. В нашем примере это выглядит так.
Отложенная загрузка
Использование отложенной загрузки
Вот и все. Теперь можно использовать функции и классы DLL прозрачно, как и в случае с неявным подключением. Единственная проблема возникает с переменными: их невозможно использовать напрямую. Дело в том, что при обращении к одной из функций в DLL мы на самом деле вызываем функцию __delayLoadHelper, которая и выполняет загрузку DLL (если она еще не загружена), затем получает адрес функции с помощью GetProcAddress и перенаправляет все последующие вызовы функции по этому адресу. Но при обращении к переменной вызова функции не происходит, а значит использовать __delayLoadHelper не удается.
Проблема решается путем явного использования GetProcAddress при работе с переменными. Если DLL еще не загружена, ее придется загрузить явно с помощью LoadLibrary. Но если мы уже обращались к ее функциям и точно знаем, что она находится в памяти, мы можем получить ее дескриптор с помощью функции GetModuleHandle, которой необходимо передать имя DLL. В нашем примере это выглядит так.
Выгрузка библиотеки
Итак, мы установили, что при использовании отложенной загрузки DLL грузится в память, когда происходит обращение к одной из ее функций. Но в последствии нам может потребоваться выгрузить ее, чтобы не занимать зря системные ресурсы. Специально для этого предназначена функция __FUnloadDelayLoadedDLL, объявленная в файле Delayimp.h. Если вы планируете использовать ее, вам нужно задать еще один ключ линкера - /DELAY:UNLOAD. Например:
Имя, которое вы передаете функции __FUnloadDelayLoadedDLL, должно в точности соответствовать имени, заданному в ключе /DELAYLOAD. Если, к примеру, передать ей "MYLIB.DLL" или "mylib.dll", библиотека останется в памяти.
ПРЕДУПРЕЖДЕНИЕ
Не используйте FreeLibrary, чтобы выгрузить DLL с отложенной загрузкой.
Обработка исключений
Как я уже говорил, в случае ошибки функция __delayLoadHelper возбуждает исключение. Если нужная DLL не обнаружена, возбуждается исключение с кодом VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND). Если в DLL не обнаружена требуемая функция, исключение будет иметь код VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND).
ПРИМЕЧАНИЕ
VcppException - это макрос, который используется для формирования кода ошибки в подсистеме Visual C++. Первый параметр задает "степень серьезности" ошибки, а второй - код исключения.
И то, и другое исключение можно обработать, используя механизм структурной обработки исключений. Для этого нужно написать фильтр , реагирующий на приведенные коды исключения, а также обработчик исключения. В простейшем случае они могут выглядеть так.
Написанный вами фильтр исключений может также получить дополнительную информацию с помощью функции GetExceptionInformation. Эта функция возвращает указатель на структуру EXCEPTION_POINTERS. В ней содержится поле ExceptionRecord - указатель на структуру EXCEPTION_RECORD. А структура EXCEPTION_RECORD в свою очередь содержит поле ExceptionInformation[0], в которое __delayLoadHelper помещает указатель на структуру DelayLoadInfo, содержащую дополнительную информацию. Эта структура объявлена следующим образом (файл Delayimp.h).
В частности, вы можете извлечь из нее имя DLL (поле szDll), а также имя или порядковый номер функции, вызов которой привел к исключению (поле dlp).
Функции-ловушки
Функции-ловушки должны иметь следующий прототип:
Первый параметр функции содержит код уведомления или ошибки, второй - указатель на уже знакомую нам структуру DelayLoadInfo. Все возможные коды уведомления описаны в файле Delayimp.h при помощи следующего перечисления:
В качестве примера приведу текст функции-ловушки, которая подменяет вызов функции SomeFunc на вызов функции YetAnotherFunc.
Часто библиотеки DLL помещаются в файлы с различными расширениями, такими как .EXE, .DRV или .DLL.
Преимущества DLL
Ниже приведены несколько преимуществ наличия файлов DLL.
Использует меньше ресурсов
DLL файлы не загружаются в оперативную память вместе с основной программой; они не занимают места, если не требуется. Когда требуется файл DLL, он загружается и запускается. Например, пока пользователь Microsoft Word редактирует документ, файл DLL принтера не требуется в оперативной памяти. Если пользователь решает распечатать документ, то приложение Word вызывает загрузку и запуск DLL-файла принтера.
Способствует модульной архитектуре
DLL помогает продвигать разработку модульных программ. Это помогает вам разрабатывать большие программы, которые требуют многоязычных версий, или программу, которая требует модульной архитектуры. Примером модульной программы является бухгалтерская программа, имеющая много модулей, которые могут динамически загружаться во время выполнения.
Помогите легко развернуть и установить
Когда функция в DLL нуждается в обновлении или исправлении, развертывание и установка DLL не требует повторного связывания программы с DLL. Кроме того, если несколько программ используют одну и ту же DLL, все они получают выгоду от обновления или исправления. Эта проблема может возникать чаще, когда вы используете сторонние библиотеки DLL, которые регулярно обновляются или исправляются.
Приложения и библиотеки DLL могут автоматически ссылаться на другие библиотеки DLL, если связь с DLL указана в разделе ИМПОРТ файла определения модуля как часть компиляции. Иначе, вы можете явно загрузить их, используя функцию Windows LoadLibrary.
Важные DLL-файлы
Сначала мы обсудим проблемы и требования, которые вы должны учитывать при разработке своих собственных библиотек DLL.
Типы DLL
Когда вы загружаете DLL в приложение, два метода связывания позволяют вам вызывать экспортированные функции DLL. Два метода связывания:
- динамическое связывание во время загрузки и
- динамическое связывание во время выполнения.
Динамическое связывание во время загрузки
При динамическом связывании во время загрузки приложение выполняет явные вызовы экспортируемых функций DLL, таких как локальные функции. Чтобы использовать динамическое связывание во время загрузки, предоставьте файл заголовка (.h) и файл библиотеки импорта (.lib) при компиляции и компоновке приложения. Когда вы сделаете это, компоновщик предоставит системе информацию, необходимую для загрузки DLL, и разрешит расположение экспортированных функций DLL во время загрузки.
Динамическое связывание во время выполнения
При динамическом связывании во время выполнения приложение вызывает либо функцию LoadLibrary, либо функцию LoadLibraryEx, чтобы загрузить DLL во время выполнения. После успешной загрузки DLL вы используете функцию GetProcAddress, чтобы получить адрес экспортированной функции DLL, которую вы хотите вызвать. Когда вы используете динамическое связывание во время выполнения, вам не нужен файл библиотеки импорта.
В следующем списке описаны критерии приложения для выбора между динамическим связыванием во время загрузки и динамическим связыванием во время выполнения:
Производительность при запуске: если важна начальная производительность при запуске приложения, следует использовать динамическое связывание во время выполнения.
Простота использования : при динамическом связывании во время загрузки экспортированные функции DLL похожи на локальные функции. Это помогает вам легко вызывать эти функции.
Логика приложения . При динамическом связывании во время выполнения приложение может выполнять ветвление для загрузки различных модулей по мере необходимости. Это важно при разработке многоязычных версий.
Производительность при запуске: если важна начальная производительность при запуске приложения, следует использовать динамическое связывание во время выполнения.
Простота использования : при динамическом связывании во время загрузки экспортированные функции DLL похожи на локальные функции. Это помогает вам легко вызывать эти функции.
Логика приложения . При динамическом связывании во время выполнения приложение может выполнять ветвление для загрузки различных модулей по мере необходимости. Это важно при разработке многоязычных версий.
Точка входа в DLL
Когда вы создаете DLL, вы можете дополнительно указать функцию точки входа. Функция точки входа вызывается, когда процессы или потоки присоединяются к DLL или отсоединяются от DLL. Вы можете использовать функцию точки входа для инициализации или уничтожения структур данных в соответствии с требованиями DLL.
Кроме того, если приложение является многопоточным, вы можете использовать локальное хранилище потоков (TLS) для выделения памяти, которая является частной для каждого потока в функции точки входа. Следующий код является примером функции точки входа DLL.
Когда функция точки входа возвращает значение FALSE, приложение не запускается, если вы используете динамическое связывание во время загрузки. Если вы используете динамическое связывание во время выполнения, не будет загружаться только отдельная DLL.
Функция точки входа должна выполнять только простые задачи инициализации и не должна вызывать какие-либо другие функции загрузки или завершения DLL. Например, в функции точки входа не следует прямо или косвенно вызывать функцию LoadLibrary или функцию LoadLibraryEx . Кроме того, вы не должны вызывать функцию FreeLibrary, когда процесс завершается.
ВНИМАНИЕ : В многопоточных приложениях убедитесь, что доступ к глобальным данным DLL синхронизирован (потокобезопасен), чтобы избежать возможного повреждения данных. Для этого используйте TLS, чтобы предоставить уникальные данные для каждого потока.
Экспорт функций DLL
Чтобы экспортировать функции DLL, вы можете добавить ключевое слово функции в экспортированные функции DLL или создать файл определения модуля (.def), в котором перечислены экспортированные функции DLL.
Чтобы использовать ключевое слово функции, вы должны объявить каждую функцию, которую вы хотите экспортировать, со следующим ключевым словом:
Чтобы использовать экспортированные функции DLL в приложении, вы должны объявить каждую функцию, которую вы хотите импортировать, с помощью следующего ключевого слова:
Как правило, вы использовали бы один заголовочный файл, имеющий оператор определения и оператор ifdef, чтобы разделить оператор экспорта и оператор импорта.
// SampleDLL.def // LIBRARY "sampleDLL" EXPORTS HelloWorldНаписать пример DLL
В Microsoft Visual C ++ 6.0 вы можете создать DLL, выбрав либо тип проекта Win32 Dynamic-Link Library, либо тип проекта MFC AppWizard (dll) .
Следующий код представляет собой пример библиотеки DLL, созданной в Visual C ++ с использованием типа проекта Win32 Dynamic-Link Library.
Вызов примера библиотеки DLL
Следующий код является примером проекта приложения Win32, который вызывает экспортированную функцию DLL в библиотеке SampleDLL.
ПРИМЕЧАНИЕ . При динамическом связывании во время загрузки необходимо связать библиотеку импорта SampleDLL.lib, которая создается при сборке проекта SampleDLL.
При динамическом связывании во время выполнения вы используете код, подобный следующему коду, для вызова экспортированной функции DLL SampleDLL.dll.
Когда вы компилируете и связываете приложение SampleDLL, операционная система Windows ищет библиотеку SampleDLL в следующих местах в следующем порядке:
Системная папка Windows (функция GetSystemDirectory возвращает путь к системной папке Windows).
Папка Windows (функция GetWindowsDirectory возвращает путь к папке Windows).
Системная папка Windows (функция GetSystemDirectory возвращает путь к системной папке Windows).
Папка Windows (функция GetWindowsDirectory возвращает путь к папке Windows).
Это может быть использовано для включения и выключения службы.
Доступно несколько инструментов, которые помогут вам решить проблемы с DLL. Некоторые из них обсуждаются ниже.
Зависимость Уокер
Средство Dependency Walker ( disabled.exe ) может рекурсивно сканировать все зависимые библиотеки DLL, которые используются программой. Когда вы открываете программу в Dependency Walker, Dependency Walker выполняет следующие проверки:
- Проверяет наличие недостающих DLL.
- Проверяет недопустимые программные файлы или библиотеки DLL.
- Проверяет, совпадают ли функции импорта и функции экспорта.
- Проверяет круговые ошибки зависимости.
- Проверяет недопустимые модули, поскольку модули предназначены для другой операционной системы.
DLL Универсальный Решатель Проблем
Инструмент универсального решения проблем DLL (DUPS) используется для аудита, сравнения, документирования и отображения информации DLL. В следующем списке описаны утилиты, которые составляют инструмент DUPS:
Помните следующие советы при написании DLL:
Используйте правильное соглашение о вызовах (C или stdcall).
Помните о правильном порядке аргументов, передаваемых в функцию.
НИКОГДА не изменяйте размеры массивов и не объединяйте строки, используя аргументы, передаваемые непосредственно в функцию. Помните, что передаваемые вами параметры являются данными LabVIEW. Изменение размеров массива или строки может привести к сбою при перезаписи других данных, хранящихся в памяти LabVIEW. Вы МОЖЕТЕ изменить размер массивов или объединить строки, если вы передадите дескриптор массива LabVIEW или дескриптор строки LabVIEW и используете компилятор Visual C ++ или Symantec для компиляции вашей DLL.
При передаче строк в функцию выберите правильный тип строки для передачи. C или Паскаль или LabVIEW Строка Ручка.
Длина строк Паскаля ограничена 255 символами.
Если вы работаете с массивами или строками данных, ВСЕГДА передавайте буфер или массив, достаточно большой, чтобы хранить любые результаты, помещенные в буфер функцией, если вы не передаете их как дескрипторы LabVIEW, и в этом случае вы можете изменить их размер с помощью CIN. работает под компилятором Visual C ++ или Symantec.
Перечислите функции DLL в разделе EXPORTS файла определения модуля, если вы используете _stdcall.
Перечислите функции DLL, которые другие приложения вызывают в разделе EXPORTS файла определения модуля, или включите ключевое слово _declspec (dllexport) в объявление функции.
Если вы используете компилятор C ++, экспортируйте функции с помощью оператора extern .C. <> В заголовочном файле, чтобы предотвратить искажение имени.
Если вы пишете свою собственную DLL, вы не должны перекомпилировать DLL, пока DLL загружается в память другим приложением. Перед перекомпиляцией DLL убедитесь, что все приложения, использующие эту конкретную DLL, выгружены из памяти. Это гарантирует, что сама DLL не загружается в память. Возможно, вам не удастся восстановить правильно, если вы забудете об этом и ваш компилятор не предупредит вас.
Протестируйте свои DLL с другой программой, чтобы убедиться, что функция (и DLL) работают правильно. Тестирование с помощью отладчика вашего компилятора или простой C-программы, в которой вы можете вызывать функцию в DLL, поможет вам определить, являются ли возможные трудности присущими DLL или LabVIEW.
Используйте правильное соглашение о вызовах (C или stdcall).
Помните о правильном порядке аргументов, передаваемых в функцию.
НИКОГДА не изменяйте размеры массивов и не объединяйте строки, используя аргументы, передаваемые непосредственно в функцию. Помните, что передаваемые вами параметры являются данными LabVIEW. Изменение размеров массива или строки может привести к сбою при перезаписи других данных, хранящихся в памяти LabVIEW. Вы МОЖЕТЕ изменить размер массивов или объединить строки, если вы передадите дескриптор массива LabVIEW или дескриптор строки LabVIEW и используете компилятор Visual C ++ или Symantec для компиляции вашей DLL.
При передаче строк в функцию выберите правильный тип строки для передачи. C или Паскаль или LabVIEW Строка Ручка.
Длина строк Паскаля ограничена 255 символами.
Если вы работаете с массивами или строками данных, ВСЕГДА передавайте буфер или массив, достаточно большой, чтобы хранить любые результаты, помещенные в буфер функцией, если вы не передаете их как дескрипторы LabVIEW, и в этом случае вы можете изменить их размер с помощью CIN. работает под компилятором Visual C ++ или Symantec.
Перечислите функции DLL в разделе EXPORTS файла определения модуля, если вы используете _stdcall.
Перечислите функции DLL, которые другие приложения вызывают в разделе EXPORTS файла определения модуля, или включите ключевое слово _declspec (dllexport) в объявление функции.
Если вы используете компилятор C ++, экспортируйте функции с помощью оператора extern .C. <> В заголовочном файле, чтобы предотвратить искажение имени.
Если вы пишете свою собственную DLL, вы не должны перекомпилировать DLL, пока DLL загружается в память другим приложением. Перед перекомпиляцией DLL убедитесь, что все приложения, использующие эту конкретную DLL, выгружены из памяти. Это гарантирует, что сама DLL не загружается в память. Возможно, вам не удастся восстановить правильно, если вы забудете об этом и ваш компилятор не предупредит вас.
Протестируйте свои DLL с другой программой, чтобы убедиться, что функция (и DLL) работают правильно. Тестирование с помощью отладчика вашего компилятора или простой C-программы, в которой вы можете вызывать функцию в DLL, поможет вам определить, являются ли возможные трудности присущими DLL или LabVIEW.
Мы видели, как написать DLL и как создать программу «Hello World». Этот пример, должно быть, дал вам представление об основной концепции создания DLL.
Здесь мы дадим описание создания DLL с использованием Delphi, Borland C ++ и снова VC ++.
Читайте также: