Динамически загружаемые библиотеки: структура, архитектура и применение Текст научной статьи по специальности «Компьютерные и информационные науки»
Аннотация научной статьи по компьютерным и информационным наукам, автор научной работы — Леденев Александр Вячеславович, Семенов Игорь Алексеевич, Сторожевых Владимир Анатольевич
DLL - это сокращение от Dynamic Link Library (динамически загружаемая библиотека). Исполняемый код в DLL не предполагает автономного использования. Перед тем как можно будет приступить к использованию, необходимо загрузить DLL в область памяти вызывающего процесса (т.е. DLL не может выполняться сама по себе - ей обязательно нужен клиент). Это явление носит название «проецирование DLL на адресное пространство процесса». И это не удивительно, если вспомнить тот факт, что процессор работает не только с регистрами, но и с адресами памяти. Поэтому каждому объекту DLL требуется свое место «под солнцем», чтобы иметь возможность быть выполненным при вызове. В конечном коде exe-файла, который генерирует компилятор, не будет инструкций процессора, соответст вующих коду данной функции. Вместо этого будет сгенерирована инструкция вызова соответствующей функции (call).
Похожие темы научных работ по компьютерным и информационным наукам , автор научной работы — Леденев Александр Вячеславович, Семенов Игорь Алексеевич, Сторожевых Владимир Анатольевич
Текст научной работы на тему «Динамически загружаемые библиотеки: структура, архитектура и применение»
А.В. Леденев, И.А. Семенов, В.А. Сторожевых
Динамически загружаемые библиотеки: структура, архитектура и применение
DLL — это сокращение от Dynamic Link Library (динамически загружаемая библиотека). С формальной точки зрения DLL — особым образом оформленный относительно независимый блок исполняемого кода. DLL используются множеством приложений. Все приложения для ОС Windows так или иначе используют динамические библиотеки.
Данный материал посвящен особенностям реализации DLL в различных средах и для различных целей.
Особый способ оформления предполагает наличие в DLL так называемых секций импорта и экспорта. Секция экспорта указывает те идентификаторы объектов (функций, классов, переменных), доступ к которым предоставляет данная DLL. В этом случае мы говорим об экспортировании идентификаторов из DLL. В общем случае именно секция экспорта представляет особый интерес для разработчиков. Хотя ничто не мешает реализовать DLL, которая не имеет данной секции, но тем не менее выполняет полезную работу.
Относительная независимость связана с наличием/отсутствием секции импорта у DLL (т. е. секции, в которой описываются внешние зависимости данной DLL от других). Подавляющее большинство DLL (за исключением, быть может, DLL-ресурсов) импортирует функции из системных DLL (kernel32.dll, user32.dll, gdi32.dll и проч.). В большинстве случаев при создании проекта в его опциях автоматически проставляется стандартный набор таких библиотек. Иногда в этот список необходимо добавить DLL, требующиеся для конкретных задач (например, в случае использования библиотеки сокетов требуется дополнительно подключить библиотеку ws2_32.dll).
Исполняемый код в DLL не предполагает автономного использования. Перед тем как можно будет приступить к использованию, необходимо загрузить DLL в область памяти вызывающего процесса (т.е. DLL не может выполняться сама по себе — ей обязательно нужен клиент). Это явление носит название «проецирование DLL на адресное пространство процесса». И это не удивительно, если вспомнить тот факт, что процессор работает не только с регистрами, но и с адресами памяти. Поэтому каждому объекту DLL требуется свое место «под солнцем», чтобы иметь возможность быть выполненным при вызове. В конечном коде exe-файла, который генерирует компилятор, не будет инструкций процессора, соответствующих коду данной функции. Вместо этого будет сгенерирована инструкция вызова соответствующей функции (call). Так как DLL отображена на адресное пространство процесса, то код DLL будет легко доступен по call-вызову.
Итак, формально, DLL — особым образом оформленный программный компонент, доступ к исполняемому коду которого приложение получает в момент старта (DLL неявной загрузки) или в момент использования (DLL явной и отложенной загрузки).
Что же касается физического представления на диске, то разница между dll- и exe-файлами небольшая. Как динамически лин-
куемые библиотеки, так и исполняемые модули приложений в Windows имеют формат Portable Executable (PE-файл), однако нельзя «запустить» DLL-библиотеку на выполнение, как обычное приложение. «Узнать» DLL-файл можно по его сигнатуре (заголовку): признаком библиотеки является установленный флаг IMAGE_FILE_DLL (13-й бит) в поле Characteristics заголовка IMAGE_FILE_HEADER (эти константы описаны в файле winnt.h; подробная информация о формате exe- и dll-файлов — см. MSDN «Microsoft Portable Executable and Common Object File Format Specification»). Кроме того, в заголовках файлов динамически линкуемых библиотек указан нулевой размер стека — это связано с тем, что функции DLL используют стек вызывающего приложения. DLL-файл может содержать как инструкции (команды процессора), так и данные (разделяемые ресурсы).
В общем случае файл, являющийся динамически загружаемой библиотекой, не обязан иметь расширение .dll. Например, известные файлы *.cpl — это не что иное, как DLL, используемые апплетом панели управления; *.ocx — DLL, содержащие внутрипроцессные (inproc) COM-объекты.
Логическое (философское) представление DLL не имеет никаких ограничений. Удобно представлять себе DLL в виде сервера, который предлагает дополнительную функциональность приложению. Приложения, которые используют данную функциональность, являются клиентами DLL. Рисунок 1 показывает процесс взаимодействия приложения с DLL. После проецирования DLL на адресное пространство вызывающего процесса DLL становится частью этого процесса. Поэтому возможен абсолютно безболезненный вызов функций, экспортируемых DLL.
Рис. 1. Взаимодействие приложения с DLL
Зачем нужны DLL?
Свою историю DLL ведут с середины 60-х годов прошлого столетия, однако по-настоящему широкое распространение динамически линкуемые библиотеки получили после появления операционной системы Windows1.
По крайней мере, доподлинно известно, что операционные системы Windows 3.1/3.11 уже содержали программы, использующие DLL. В связи с тем, что в те времена емкости оперативной памяти и жесткого диска были значительно меньше, чем сейчас, использование DLL предоставляло ряд преимуществ.
• Экономия дискового пространства за счет многократного использования кода (reusing). Если приложения используют один и тот же код, нет необходимости поставлять его в коде каждого приложения. Достаточно разработать DLL.
• Экономия физической памяти (RAM) за счет загрузки в нее единственного экземпляра DLL. Именно тогда появились счетчики ссылок пользователей DLL — при каждом вызове функции ОС проверяет наличие загруженного в память экземпляра библиотеки. В случае положительного ответа счетчик ссылок пользователей данной DLL увеличивается на единицу. Если же эк-
1 Крис Касперски («Техника сетевых атак», 2001) пишет: «Именно в MULTICS (1965-1969 гг.) появилась возможность динамического связывания модулей в ходе выполнения программы, более известная современному читателю по этим пресловутым DLL в Windows. Такой прием логически завершил эволюцию совершенствования оверлеев, обеспечив единый, унифицированный интерфейс для всех программ, позволяя сэкономить значительную часть оперативной памяти и процессорных ресурсов. »
земпляр данной DLL в памяти не обнаружен, то операционная система загружает файл в память и присваивает счетчику значение «1». Механизм разделения кода носит название memory mapping (отображение в память). При выгрузке DLL из памяти уменьшается значение счетчика числа пользователей, в случае равенства его нулю DLL немедленно выгружается.
• Изолирование и модификация кода DLL независимо от остального кода программы. Например, код визуализации изолируется от математической части. При изменении математического аппарата (например, при разработке нового, более быстрого алгоритма) перекомпиляция кода клиентского приложения (отвечающего за визуализацию результатов) не требуется. Этот фактор может играть значительную роль в том случае, если число клиентов достаточно велико.
Самое удивительное (время не стоит на месте!), что ранее политика Microsoft позволяла (и даже приветствовала) размещение DLL в системных директориях (таких как Windows/System, Windows/System32). Это порождало периодические проблемы конфликта версий при замене DLL (см. раздел «DLL Hell»). В связи с этим (а также с ростом емкости запоминающих устройств и соответствующим снижением цены на них) на данный момент политика Microsoft изменена на прямо противоположную — Microsoft настоятельно рекомендует хранить все используемые DLL в рабочем каталоге программы и лишь в случае острой необходимости пользоваться системными директориями.
Основные направления использования DLL:
• всевозможные модули расширения функциональности приложений — так называемые plug-in (далее приводится пример с MatLab, Far и проч.);
• разделение объектов абстракции (функций, классов и проч.) между приложениями;
• независимость модификации кода — § DLL может быть в любой момент переписа- | на с сохранением экспортируемых интер- ^ фейсов; <§
• реализация определенных действий, < которые можно совершить только при по- ад-мощи DLL; g
• хранилище ресурсов с возможностью J независимого изменения этих ресурсов. ^
Кстати, DLL широко используются в тех- -ё нологии COM (а до этого — в OLE 1.0) — § в качестве основы при построении так на- ^ зываемых inproc-серверов (внутрипроцесс- < ных серверов) используются DLL. Это позволяет упростить взаимодействие с приложением благодаря загрузке используемых ActiveX объектов в адресное пространство клиента. В этом случае накладные расходы, связанные с преодолением границ адресных пространств при передаче данных (параметров функций и т.д.) — так называемый marshalling, сводятся к нулю.
Все те абстракции, с которыми приходится работать в повседневной программистской жизни, могут быть внедрены в DLL — классы, объекты, таймеры, потоки, функции и проч. Однако не всегда удобно и правильно работать с этими объектами вне DLL. Связано это с тем, что не всегда логическое представление того или иного объекта может быть однозначно представлено (переведено) в физическое. Использование DLL не налагает ограничений на используемый язык (точнее, почти не налагает). Более того, как правило, DLL разрабатывается на другом языке программирования, нежели тот, который используется при ее загрузке. Приведем пример: разрабатывался математический проект, код которого реализовывался на M-языке среды MatLab (Matrix Laboratory). M-язык по своей природе является интерпретируемым языком программирования. После этого полученные алгоритмы были реализованы при помощи языка C++ и скомпилированы в DLL, которая также использовалась средой Mat-Lab.
Сравнение производительности исполнения кода (*.DLL vs *.M)
Задача — построение выпрямляющего функционала. Количество точек — 864 001
Функционал «Энергия» 0,44 35,15
Функционал «Длина» 0,22 34,05
Функционал «Регрессия» 31,25 148,52
Таким образом, аналогичная реализация в скомпилированном варианте дает выигрыш по скорости исполнения от пяти до ста раз! И это не предел. Разумеется, подобная эффективность может быть очень хорошо использована при наличии соответствующих возможностей. Необходимо отметить, что DLL — мощный и удобный механизм и пользоваться им надо с умом.
| один exe-файл и никаких DLL
[I или компактный exe-файл
^ Небезызвестный Бьерн Страуструп в
К своей книге «Язык программирования C++»
<5, дает после каждой главы ряд полезных со-
«о ветов, один из которых звучит так: «Поль-
^ зуйтесь здравым смыслом. » Этотже совет
ji можно предложить и при использовании
^ DLL в программе.
¡5 Разумеется, не стоит каждую отдельную
| функцию размещать в DLL и затем экспортировать (хотя это и возможно). Со време-
<ц нем такой проект превратится в трудно-
Ц управляемого монстра, инициализация ко-
Ц торого будет занимать порядочное время.
& Правда, экспортирование только одной функции из DLL может быть продиктовано
<S следующей логикой: серверное приложе-
| ние сканирует определенную директорию
Sg на предмет нахождения в ней файлов с оп-
^ ределенным расширением. В случае нахождения такого файла сервер полагает, что
это — DLL, которая экспортирует функцию с определенным именем (скажем, CplApp-let или mexFunction), и, соответственно, он сможет ее загрузить. Таким простым образом возможно динамическое расширение функциональности приложения — именно такой подход проповедуется в случае Панели управления (Control Panel) и системы MatLab ( Matrix Laboratory ).
Не стоит также бояться использовать те плюсы, которые может принести использование DLL в программе.
Логически отделенная библиотека, часто используемая в совместных проектах, — верный кандидат на размещение в DLL. Это позволит:
• поставлять различным клиентам приложения с различной функциональностью;
• модифицировать (оптимизировать, изменять) код DLL без повторной перекомпиляции клиентских приложений, использующих DLL. А это значит, что при модификации или исправлении ошибки достаточно изменить DLL (сама Microsoft часто именно так и делает: постоянно появляются пакеты обновлений Service Pack);
• разделить разработку большого проекта на отдельные, независимые группы;
• обеспечить легкость отладки и тестирования. В этом случае возможности, связанные с использованием нисходящих технологий проектирования, существенно повышаются;
• использовать различные языки при написании DLL и клиентского приложения. Например, клиент может быть написан при помощи m-языка (тип интерпретируемого скрипта, используемый в системе MatLab), а DLL — при помощи языка C++. Это позволит провести дополнительную оптимизацию кода без потери его качества.
Разумеется, каждая DLL требует определенного процесса инициализации и, соответственно, затрат на него. В случае наличия большого количества DLL процесс инициализации может занимать порядочное
Среда подготовки исполняемого модуля. Время исполнения, с
Visual Matrix Studio 7.0 Laboratory
время (правда, необязательно все DLL инициализировать сразу, а также необязательно вообще инициализировать DLL — ведь иногда из DLL нужно получить не код, а находящиеся в ней ресурсы — например, в случае использования DLL как хранилища локализованных строковых параметров).
В случае помещения всего кода в exe-файл дополнительная инициализация не потребуется. Правда, при этом будет происходить постоянное дублирование кода. В случае единичного проекта это не составляет проблемы. Если же таких проектов несколько, возможности повторной перекомпиляции бывают существенно затруднены (и неэффективны). Представьте себе, что было бы, если бы при каждом изменении Microsoft заставляла бы всех клиентов перекомпилировать «ядро» Windows?
Кроме того, помните, что определенные проблемы проектирования без использования DLL (например, связанные с установкой ловушек) решить вообще нельзя.
Еще можно вспомнить так называемые ISAPI-расширения — высокоэффективные модули, используемые при написании Web-приложений. Ну и, наконец, такая система, как MatLab, позволяет использовать собственноручно написанные и скомпилированные модули в виде DLL для повышения быстродействия работы программ. Так что использование DLL может быть продиктовано еще и вопросами оптимизации.
Подытоживая сказанное выше, можно сделать следующие выводы.
Плюсы и минусы в случае использования одного большого exe-файла:
(+) относительная быстрота инициализации — каждая DLL требовала бы отдельного процесса инициализации;
(+) все в одном файле (нет внешних зависимостей) — такой файл может поставляться без внешних библиотек;
(-) постоянное дублирование кода (за счет увеличения размера ^xe-файла);
(-) полная перекомпиляция при любом изменении;
(-) все «в одной куче»: отсутствие разделения реализации логики различных (по функциональному наполнению) объектов;
(+) идеально для небольших проектов (утилиты, тестовые приложения и проч.).
Плюсы и минусы в случае разделения EXE и DLL:
(+) физическое разделение логически независимых объектов (классов, функций и проч.). Это позволяет проводить независимую разработку и последующее тестирование подобных DLL. На момент сбора объекты будут иметь достаточно предсказуемое поведение, чтобы быть сразу же использованными другими разработчиками;
(-) возможно большее время загрузки (если не применять различного рода оптимизирующих действий — отложенную загрузку, управление предпочтительными базовыми адресами и проч.);
(+) идеально для больших проектов (повторное использование кода + отсутствие его дублирования);
(+) легкость обновления и замены.
А что использовать в каждом случае — это дело конкретной задачи.
Как создать собственную DLL
Итак, мы готовы от теории перейти к практике, т. е. к написанию собственной динамически загружаемой библиотеки. Средства для создания библиотек DLL имеются практически во всех средах разработки для Windows. Мы рассмотрим процесс создания DLL с использованием наиболее популярных в настоящий момент систем программирования.
Создадим простенький пример DLL — пусть она экспортирует функцию, которая будет реализовывать сверхсложный и, что самое главное, уникальный алгоритм — получать на вход два параметра, складывать их и возвращать результат.
Что для этого необходимо сделать?
1. Создать проект типа «Win32 Dynamic-Link Library». Для этого необходимо выбрать соответствующую опцию в левом окне, затем ввести имя проекта в окне «Project name» и нажать кнопку «Ok».
Существуют также проекты типа «Win32 Static Library». При компиляции проектов данного типа создаются статические (в отличие от динамических) библиотеки — имеющие расширение *.lib. Отличие статических библиотек от динамических состоит в том, что при компиляции код, заключенный в lib-файл (а там располагается именно код), помещается целиком и полностью в бинарный файл использующего его приложения. При компиляции проекта «Win32 Dynamic-Link Library» также создается lib-файл. Но он содержит лишь необходимую информацию для связывания адресов DLL-файла и приложения.
Как, спрашивается, отличить lib-файл статической библиотеки от lib-файла динамической? Очень просто — используйте утилиту dumpbin с параметром /headers. Для примера приведем примеры вывода этой утилиты в различных случаях (см. стр. 37).
В описаниях функций lib-файла библиотеки динамической загрузки упоминается имя файла требуемой (связанной с ними) DLL — при вызове функции происходит попытка вызова этой DLL (посредством загрузки в память и последующим процессом проецирования). Функции выше (lib-файл статической библиотеки) не имеют такого параметра — их код явно подставляется в тело вызывающего приложения при вызове.
2. Далее можно либо создать пустой проект (и самостоятельно добавить в него файлы), либо создать простой проект с заданным по умолчанию шаблоном, либо создать проект, в котором изначально будет предусмотрен экспорт функций и классов. Мы пойдем по наиболее тернистому первому пути.
3. Добавьте в проект следующие файлы (см. стр. 38).
Будьте осторожны со встраиваемыми (inline) функциями в DLL. Опасность заключается в том, что код таких функций явно подставляется в код приложения-клиента. Это не вызывает проблем до тех пор, пока не произойдет изменения кода библиотеки. В этом случае придется перекомпилировать код приложения, использующего конкретную DLL. Как правило, код встраиваемых функций тривиален и подвержен очень редким изменениям. Но знать об этом, тем не менее, нужно.
4. И не забываем определить идентификатор XDLL_EXPORTS в настройках проекта — «Projects settings->C++->General-> Preprocessor definitions». В случае создания проекта DLL VC++ выполняет эту операцию автоматически. После успешной компиляции данного проекта мы можем убедиться, что в директории вывода объектных файлов появились два файла — XDll6.dll и XDll6.lib. Первый из них представляет собой непосредственно DLL. Посмотрим, что нам выдаст dumpbin относительно XDll6.dll:
Microsoft (R) COFF/PE Dumper Version 7.00.9466 Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file XDll6.dll File Type: DLL
Section contains the following exports for XDll6.dll 00000000 characteristics
3F50EECB time date stamp Sat Aug 30 22:36:59 2003 0.00 version
1 ordinal base 1 number of functions 1 number of names
ordinal hint RVA name
1 0 00001005 ?getSum@@YAHHH@Z Summary
Lib-файл статической библиотеки
SECTION HEADER #3
0 physical address ^
0 virtual address OQ
25 size of raw data §
486 file pointer to raw data (00000486 to 000004AA) *
0 file pointer to relocation table ^
4AB file pointer to line numbers
0 number of relocations 5g
3 number of line numbers c®
60501020 flags jg Code ¡5 COMDAT; sym= "int _cdecl getSum(int,int)" (?getSum@@YAHHH@Z) ^
Execute Read SECTION HEADER #5 .text name
0 physical address 0 virtual address 28 size of raw data 53F file pointer to raw data (0000053F to 00000566)
0 file pointer to relocation table 567 file pointer to line numbers 0 number of relocations 3 number of line numbers 60501020 flags Code
COMDAT; sym= "int _cdecl getSum(int,int,int)" (?getSum@@YAHHHH@Z)
16 byte align Execute Read
Lib-файл динамической библиотеки
3F54CD9E Tue Sep 02 21:04:30 2003
?getSum@@YAHHH@Z (int _cdecl getSum(int,int))
3F54CD9E Tue Sep 02 21:
?getSum@@YAHHHH@Z (int code name 1
/* Символ XDLL6_EXPORTS по умолчанию определен в проекте (см. Настройки проекта->С/С++->General-> Preprocessor Definitions). При этом все экспортируемые идентификаторы предваряются символом XDLL_API. Что это дает?
В случае определения XDLL6_EXPORTS в проекте XDLL_API определяется как экспортируемый объект; в случае же отсутствия такого определения будет получен импортируемый объект.
Таким образом, один и тот же заголовочный файл может быть использован и в DLL-проекте, и в проекте, который будет использовать данную DLL! Без каких-либо изменений. Очень удобно, не правда ли? */
#define XDLL_API _declspec(dllexport)
#define XDLL_API _declspec(dllimport)
/* Каждый экспортируемый идентификатор предваряем _declspec(dllexport).
Эта директива позволяет линкеру определить, что данный идентификатор следует экспортировать из DLL. При этом создается специальный lib-файл,который содержит все экспортируемые идентификаторы из модуля. Также экспортируемые объекты заносятся в раздел экспорта DLL — это можно проверить при помощи утилиты dumpbin.exe. */
XDLL_API int getSum(const int n1, const int n2);
XDLL_API int getSum(const int n1,const int n2)