Динамически подключаемая библиотека ( DLL ) — это общая библиотека в операционной системе Microsoft Windows или OS/2 .
DLL может содержать исполняемый код (функции), данные и ресурсы в любой комбинации.
Файл DLL часто имеет расширение файла .dll
, но может иметь любое расширение файла. Разработчики могут выбрать использование расширения файла, которое описывает содержимое файла, например, .ocx
для элементов управления ActiveX и .drv
для устаревшего (16-разрядного) драйвера устройства .
DLL, которая содержит только ресурсы, можно назвать ресурсной DLL . Примерами служат библиотека иконок , иногда имеющая расширение .icl
, и библиотека шрифтов , имеющая расширения .fon
и .fot
. [1]
Формат файла DLL такой же, как и у исполняемого файла (т. н. EXE ), но разные версии Windows используют разные форматы. 32- и 64-разрядные версии Windows используют Portable Executable (PE), а 16-разрядные версии Windows используют New Executable (NE).
Главное различие между DLL и EXE заключается в том, что DLL не может быть запущена напрямую, поскольку операционной системе требуется точка входа для начала выполнения. Windows предоставляет служебную программу (RUNDLL.EXE/RUNDLL32.EXE) для выполнения функции, предоставляемой DLL.
Поскольку они имеют одинаковый формат, EXE может использоваться как DLL. Потребляющий код может загружать EXE через тот же механизм, что и загрузка DLL.
Первые версии Microsoft Windows запускали программы вместе в одном адресном пространстве . Каждая программа должна была взаимодействовать, уступая процессор другим программам, чтобы графический пользовательский интерфейс (GUI) мог выполнять несколько задач одновременно и быть максимально отзывчивым. Все операции на уровне операционной системы предоставлялись базовой операционной системой: MS-DOS . Все службы более высокого уровня предоставлялись библиотеками Windows «Dynamic Link Library». API рисования , интерфейс графических устройств (GDI), был реализован в DLL под названием GDI.EXE
, пользовательский интерфейс в USER.EXE
. Эти дополнительные слои поверх DOS должны были быть общими для всех запущенных программ Windows, не только для того, чтобы Windows могла работать на машине с объемом оперативной памяти менее мегабайта, но и для того, чтобы программы могли взаимодействовать друг с другом. Код в GDI должен был преобразовывать команды рисования в операции на определенных устройствах. На дисплее он должен был манипулировать пикселями в буфере кадра. При рисовании на принтере вызовы API должны были преобразовываться в запросы к принтеру. Хотя можно было бы обеспечить жестко запрограммированную поддержку для ограниченного набора устройств (например, цветного графического адаптера дисплея, языка команд принтера HP LaserJet ), Microsoft выбрала другой подход. GDI работал бы, загружая различные фрагменты кода, называемые « драйверами устройств », для работы с различными устройствами вывода.
Та же архитектурная концепция, которая позволяла GDI загружать различные драйверы устройств, также позволяла оболочке Windows загружать различные программы Windows, а этим программам — вызывать вызовы API из общих библиотек USER и GDI. Эта концепция называлась «динамической компоновкой».
В обычной неразделяемой статической библиотеке разделы кода просто добавляются к вызывающей программе, когда ее исполняемый файл создается на этапе «связывания»; если две программы вызывают одну и ту же процедуру, эта процедура включается в обе программы на этапе связывания двух. При динамическом связывании общий код помещается в один отдельный файл. Программы, вызывающие этот файл, подключаются к нему во время выполнения, при этом операционная система (или, в случае ранних версий Windows, расширение ОС) выполняет связывание.
Для ранних версий Windows (от 1.0 до 3.11) библиотеки DLL были основой всего графического интерфейса пользователя. Таким образом, драйверы дисплея были просто библиотеками DLL с расширением .DRV, которые предоставляли пользовательские реализации одного и того же API рисования через унифицированный интерфейс драйвера устройства (DDI), а API рисования (GDI) и графического интерфейса пользователя (USER) были просто вызовами функций, экспортируемыми GDI и USER, системными библиотеками DLL с расширением .EXE.
Эта идея построения операционной системы из набора динамически загружаемых библиотек является основной концепцией Windows, которая сохраняется с 2015 года [обновлять]. DLL предоставляют стандартные преимущества общих библиотек , такие как модульность . Модульность позволяет вносить изменения в код и данные в одной автономной DLL, совместно используемой несколькими приложениями, без каких-либо изменений в самих приложениях.
Другим преимуществом модульности является использование общих интерфейсов для подключаемых модулей. Можно разработать единый интерфейс, который позволяет бесшовно интегрировать старые и новые модули во время выполнения в уже существующие приложения без каких-либо изменений в самом приложении. Эта концепция динамической расширяемости доведена до крайности в Component Object Model , основе ActiveX .
В Windows 1.x, 2.x и 3.x все приложения Windows совместно использовали одно и то же адресное пространство, а также одну и ту же память. DLL загружалась в это адресное пространство только один раз; с этого момента все программы, использующие библиотеку, обращались к ней. Данные библиотеки были общими для всех программ. Это могло использоваться как косвенная форма межпроцессного взаимодействия или могло случайно повредить разные программы. С появлением 32-разрядных библиотек в Windows 95 каждый процесс выполнялся в своем собственном адресном пространстве. Хотя код DLL может быть общим, данные являются частными, за исключением случаев, когда общие данные явно запрашиваются библиотекой. Тем не менее, большие части Windows 95 , Windows 98 и Windows Me были построены из 16-разрядных библиотек, что ограничивало производительность микропроцессора Pentium Pro при запуске и, в конечном счете, ограничивало стабильность и масштабируемость версий Windows на базе DOS.
Хотя технология DLL является основой архитектуры Windows, у нее есть недостатки.
DLL hell описывает плохое поведение приложения при использовании неправильной версии DLL. [2] Стратегии смягчения включают:
Исполняемый код DLL выполняется в пространстве памяти вызывающего процесса и с теми же правами доступа, что означает небольшие накладные расходы при его использовании, но также и отсутствие защиты для вызывающей программы, если в DLL есть какая-либо ошибка.
Технология DLL позволяет изменять приложение без необходимости повторной компиляции или повторной компоновки потребляющих компонентов. DLL можно заменить, чтобы при следующем запуске приложения использовалась новая версия DLL. Для корректной работы изменения DLL должны поддерживать обратную совместимость .
Даже операционная система может быть обновлена, поскольку она открыта для приложений через DLL. Системные DLL могут быть заменены, так что в следующий раз, когда приложения будут запускаться, они будут использовать новые системные DLL.
В Windows API файлы DLL организованы в разделы . Каждый раздел имеет свой собственный набор атрибутов, таких как возможность записи или только чтения, исполняемость (для кода) или неисполняемость (для данных) и т. д.
Код в DLL обычно является общим для всех процессов, использующих DLL; то есть они занимают одно место в физической памяти и не занимают место в файле подкачки . Windows не использует позиционно-независимый код для своих DLL; вместо этого код подвергается перемещению по мере загрузки, фиксируя адреса для всех своих точек входа в местах, которые свободны в пространстве памяти первого процесса, загрузившего DLL. В старых версиях Windows, в которых все запущенные процессы занимали одно общее адресное пространство, одной копии кода DLL всегда было достаточно для всех процессов. Однако в новых версиях Windows, которые используют отдельные адресные пространства для каждой программы, возможно использовать одну и ту же перемещенную копию DLL в нескольких программах только в том случае, если каждая программа имеет одни и те же свободные виртуальные адреса для размещения кода DLL. Если некоторые программы (или их комбинации уже загруженных DLL) не имеют этих свободных адресов, то необходимо будет создать дополнительную физическую копию кода DLL, используя другой набор перемещенных точек входа. Если физическая память, занимаемая разделом кода, должна быть освобождена, ее содержимое сбрасывается и позднее перезагружается непосредственно из файла DLL по мере необходимости.
В отличие от разделов кода, разделы данных DLL обычно являются частными; то есть каждый процесс, использующий DLL, имеет свою собственную копию всех данных DLL. При желании разделы данных можно сделать общими, что позволит осуществлять межпроцессное взаимодействие через эту общую область памяти. Однако, поскольку ограничения пользователя не применяются к использованию общей памяти DLL, это создает брешь в безопасности ; а именно, один процесс может повредить общие данные, что, скорее всего, приведет к тому, что все другие общие процессы будут вести себя нежелательно. Например, процесс, работающий под гостевой учетной записью, может таким образом повредить другой процесс, работающий под привилегированной учетной записью. Это важная причина избегать использования общих разделов в DLL.
Если DLL сжимается определенными упаковщиками исполняемых файлов (например, UPX ), все ее разделы кода помечаются как доступные для чтения и записи и не будут использоваться совместно. Разделы кода для чтения и записи, как и разделы частных данных, являются частными для каждого процесса. Таким образом, DLL с разделами общих данных не следует сжимать, если они предназначены для одновременного использования несколькими программами, поскольку каждый экземпляр программы должен будет нести свою собственную копию DLL, что приведет к увеличению потребления памяти.
Как и статические библиотеки, библиотеки импорта для DLL отмечены расширением .lib
файла. Например, kernel32.dll , основная динамическая библиотека для базовых функций Windows, таких как создание файлов и управление памятью, связана через kernel32.lib
. Обычный способ отличить библиотеку импорта от настоящей статической библиотеки — по размеру: библиотека импорта намного меньше, так как содержит только символы, ссылающиеся на фактическую DLL, которая должна быть обработана во время компоновки. Тем не менее, обе являются файлами формата Unix ar .
Связывание с динамическими библиотеками обычно осуществляется путем связывания с библиотекой импорта при построении или связывании для создания исполняемого файла. Созданный исполняемый файл затем содержит таблицу адресов импорта (IAT), с помощью которой ссылаются на все вызовы функций DLL (каждая ссылаемая функция DLL содержит свою собственную запись в IAT). Во время выполнения IAT заполняется соответствующими адресами, которые указывают непосредственно на функцию в отдельно загруженной DLL. [3]
В Cygwin/MSYS и MinGW библиотекам импорта традиционно присваивается суффикс .dll.a
, объединяющий как суффикс Windows DLL, так и суффикс Unix ar. Формат файла похож, но символы, используемые для обозначения импорта, отличаются ( _head_foo_dll
vs __IMPORT_DESCRIPTOR_foo
). [4] Хотя его инструментарий GNU Binutils может генерировать библиотеки импорта и ссылаться на них, быстрее ссылаться на DLL напрямую. [5] Экспериментальный инструмент в MinGW под названием genlib можно использовать для генерации библиотек импорта с символами в стиле MSVC.
Каждая функция, экспортируемая DLL, идентифицируется числовым порядковым номером и, опционально, именем. Аналогично, функции могут быть импортированы из DLL либо по порядковому номеру, либо по имени. Порядковый номер представляет собой положение указателя адреса функции в таблице адресов экспорта DLL. Обычно внутренние функции экспортируются только по порядковому номеру. Для большинства функций Windows API в разных выпусках Windows сохраняются только имена; порядковые номера могут меняться. Таким образом, невозможно надежно импортировать функции Windows API по их порядковым номерам.
Импорт функций по порядковому номеру обеспечивает лишь немного лучшую производительность, чем импорт по имени: экспортные таблицы DLL упорядочены по имени, поэтому для поиска функции можно использовать двоичный поиск . Индекс найденного имени затем используется для поиска порядкового номера в таблице Export Ordinal. В 16-разрядной Windows таблица имен не сортировалась, поэтому накладные расходы на поиск имени были гораздо более заметны.
Также возможно привязать исполняемый файл к определенной версии DLL, то есть разрешить адреса импортируемых функций во время компиляции. Для связанных импортов компоновщик сохраняет временную метку и контрольную сумму DLL, к которой привязан импорт. Во время выполнения Windows проверяет, используется ли та же версия библиотеки, и если да, Windows обходит обработку импорта. В противном случае, если библиотека отличается от той, к которой была привязана, Windows обрабатывает импорт обычным способом.
Связанные исполняемые файлы загружаются несколько быстрее, если они запущены в той же среде, для которой они были скомпилированы, и точно в то же время, если они запущены в другой среде, поэтому нет никаких недостатков в связывании импортов. Например, все стандартные приложения Windows привязаны к системным DLL-файлам соответствующего выпуска Windows. Хорошая возможность связать импорты приложения с его целевой средой — во время установки приложения. Это сохраняет библиотеки «связанными» до следующего обновления ОС. Однако это изменяет контрольную сумму исполняемого файла, поэтому это нельзя сделать с подписанными программами или программами, которые управляются инструментом управления конфигурацией, использующим контрольные суммы (например, контрольные суммы MD5 ) для управления версиями файлов. Поскольку более поздние версии Windows отошли от фиксированных адресов для каждой загруженной библиотеки (из соображений безопасности), возможность и ценность связывания исполняемого файла уменьшаются.
Файлы DLL могут быть явно загружены во время выполнения, этот процесс Microsoft называет просто динамическим связыванием во время выполнения , с помощью функции API LoadLibrary
(или LoadLibraryEx
). GetProcAddress
Функция API используется для поиска экспортированных символов по имени и FreeLibrary
– для выгрузки DLL. Эти функции аналогичны dlopen
, dlsym
, и dlclose
в стандартном API POSIX .
Процедура явного связывания во время выполнения одинакова в любом языке, поддерживающем указатели на функции , поскольку она зависит от API Windows, а не от языковых конструкций.
Обычно приложение, связанное с библиотекой импорта DLL, не запустится, если DLL не может быть найдена, поскольку Windows не запустит приложение, пока не найдет все DLL, которые могут понадобиться приложению. Однако приложение может быть связано с библиотекой импорта, чтобы разрешить отложенную загрузку динамической библиотеки. [6]
В этом случае операционная система не будет пытаться найти или загрузить DLL при запуске приложения; вместо этого в приложение включается заглушка компоновщиком, которая попытается найти и загрузить DLL через LoadLibrary
и GetProcAddress
при вызове одной из его функций. Если DLL не может быть найдена или загружена, или вызванная функция не существует, приложение сгенерирует исключение , которое может быть перехвачено и обработано соответствующим образом. Если приложение не обрабатывает исключение, оно будет перехвачено операционной системой, которая завершит программу с сообщением об ошибке.
Механизм отложенной загрузки также предоставляет перехватчики уведомлений , позволяя приложению выполнять дополнительную обработку или обработку ошибок при загрузке DLL и/или вызове любой функции DLL.
В исходном файле library
вместо ключевого слова используется program
. В конце файла в предложении перечислены экспортируемые функции exports
.
Delphi не нужны LIB
файлы для импорта функций из DLL; для присоединения к DLL external
в объявлении функции используется ключевое слово для обозначения имени DLL, за которым следует name
имя символа (если он отличается) или index
для идентификации индекса.
В Visual Basic (VB) поддерживается только связывание во время выполнения; но в дополнение к использованию функций API LoadLibrary
разрешены объявления импортированных функций .GetProcAddress
При импорте функций DLL через объявления VB сгенерирует ошибку времени выполнения, если DLL
файл не может быть найден. Разработчик может перехватить ошибку и обработать ее соответствующим образом.
При создании DLL в VB IDE позволяет создавать только ActiveX DLL, однако были созданы методы [7] , позволяющие пользователю явно указать компоновщику включить файл .DEF, который определяет порядковое положение и имя каждой экспортируемой функции. Это позволяет пользователю создавать стандартную Windows DLL с помощью Visual Basic (версии 6 или ниже), на которую можно ссылаться с помощью оператора «Declare».
Microsoft Visual C++ (MSVC) предоставляет несколько расширений для стандартного C++ , которые позволяют указывать функции как импортируемые или экспортируемые непосредственно в коде C++; они были приняты другими компиляторами Windows C и C++, включая версии GCC для Windows . Эти расширения используют атрибут __declspec
перед объявлением функции. Обратите внимание, что когда функции C доступны из C++, они также должны быть объявлены как extern "C"
в коде C++, чтобы сообщить компилятору, что следует использовать связь C. [8]
Помимо указания импортируемых или экспортируемых функций с помощью __declspec
атрибутов, они могут быть перечислены в разделе IMPORT или EXPORTS файла, DEF
используемого проектом. DEF
Файл обрабатывается компоновщиком, а не компилятором, и поэтому он не является специфичным для C++.
Компиляция DLL создаст DLL
и LIB
файлы, и . LIB
Файл (библиотека импорта) используется для компоновки с DLL во время компиляции; он не является необходимым для компоновки во время выполнения. Если DLL не является сервером Component Object Model (COM), DLL
файл должен быть помещен в один из каталогов, перечисленных в переменной среды PATH, в системный каталог по умолчанию или в тот же каталог, что и программа, использующая его. DLL сервера COM регистрируются с помощью regsvr32.exe, который помещает местоположение DLL и ее глобальный уникальный идентификатор ( GUID ) в реестр. Затем программы могут использовать DLL, просматривая ее GUID в реестре , чтобы найти ее местоположение, или косвенно создавать экземпляр объекта COM, используя его идентификатор класса и идентификатор интерфейса.
В следующих примерах показано, как использовать привязки, специфичные для языка, для импорта символов для связывания с DLL во время компиляции.
Дельфи
{$КОНСОЛЬ ТИПА ПРИЛОЖЕНИЯ}Пример программы ; // импортируем функцию, которая складывает два числа function AddNumbers ( a , b : Double ) : Double ; StdCall ; external 'Example.dll' ; // основная программа var R : Double ; begin R := AddNumbers ( 1 , 2 ) ; Writeln ( 'Результат: ' , R ) ; end .
С
Файл 'Example.lib' должен быть включен (предполагая, что Example.dll сгенерирован) в проект перед статической компоновкой. Файл 'Example.lib' автоматически генерируется компилятором при компиляции DLL. Невыполнение приведенного выше оператора приведет к ошибке компоновки, поскольку компоновщик не будет знать, где найти определение AddNumbers
. Файл DLL 'Example.dll' также может быть скопирован в место, где будет сгенерирован файл .exe, с помощью следующего кода:
#include <windows.h> #include <stdio.h> // Импортируем функцию, которая складывает два числа extern "C" __declspec ( dllimport ) double AddNumbers ( double a , double b ); int main ( int argc , char * argv []) { double result = AddNumbers ( 1 , 2 ); printf ( "Результат: %f \n " , result ); return 0 ; }
В следующих примерах показано, как использовать возможности загрузки и связывания во время выполнения с использованием языковых привязок Windows API.
Обратите внимание, что все четыре примера уязвимы для атак предварительной загрузки DLL, поскольку example.dll может быть разрешен в место, непреднамеренное автором (если явно не исключено, что каталог приложения идет до местоположений системной библиотеки, и без HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode
[9] или HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\CWDIllegalInDLLSearch
[10] текущий рабочий каталог ищется до каталогов системной библиотеки), и, таким образом, во вредоносную версию библиотеки. См. ссылку на руководство Microsoft по безопасной загрузке библиотеки: следует использовать SetDefaultDllDirectories
in kernel32
для удаления как каталога приложения, так и текущего рабочего каталога из пути поиска DLL, или использовать SetDllDirectoryW
in kernel32
для удаления текущего рабочего каталога из пути поиска DLL. [11]
Параметр Явное объявление Функция AddNumbers Lib "Example.dll" _ ( ByVal a As Double , ByVal b As Double ) As Double Sub Main () Dim Result As Double Result = AddNumbers ( 1 , 2 ) Debug . Print "Результат был: " & Result End Sub
Пример программы ; {$APPTYPE CONSOLE} использует Windows ; var AddNumbers : function ( a , b : integer ) : Double ; StdCall ; LibHandle : HMODULE ; begin LibHandle := LoadLibrary ( 'example.dll' ) ; if LibHandle <> 0 then AddNumbers := GetProcAddress ( LibHandle , 'AddNumbers' ) ; if Assigned ( AddNumbers ) then Writeln ( '1 + 2 = ' , AddNumbers ( 1 , 2 ) ) ; Readln ; end .
#include <windows.h> #include <stdio.h> // Сигнатура функции DLL typedef double ( * importFunction )( double , double ); int main ( int argc , char ** argv ) { importFunction addNumbers ; double result ; HINSTANCE hinstLib ; // Загрузить файл DLL hinstLib = LoadLibrary ( TEXT ( "Example.dll" )); if ( hinstLib == NULL ) { printf ( "ОШИБКА: невозможно загрузить DLL \n " ); return 1 ; } // Получить указатель на функцию addNumbers = ( importFunction ) GetProcAddress ( hinstLib , "AddNumbers" ); if ( addNumbers == NULL ) { printf ( "ОШИБКА: не удалось найти функцию DLL \n " ); FreeLibrary ( hinstLib ); return 1 ; } // Вызываем функцию. result = addNumbers ( 1 , 3 ); // Выгрузить файл DLL FreeLibrary ( hinstLib );// Отобразить результат printf ( "Результат: %f \n " , result ); вернуть 0 ; }
Привязка Python ctypes будет использовать API POSIX в системах POSIX.
импорт ctypesmy_dll = ctypes . компакт-диск . LoadLibrary ( "Example.dll" )# Следующая спецификация метода "restype" необходима для того, чтобы Python понял, какой тип возвращается функцией. my_dll . AddNumbers . restype = ctypes . c_doublep = my_dll.AddNumbers ( ctypes.c_double ( 1.0 ) , ctypes.c_double ( 2.0 ) ) print ( "Результат:" , p )
Модель компонентных объектов (COM) определяет двоичный стандарт для размещения реализации объектов в файлах DLL и EXE. Он предоставляет механизмы для поиска и версионирования этих файлов, а также независимое от языка и машиночитаемое описание интерфейса. Размещение объектов COM в DLL более легковесно и позволяет им совместно использовать ресурсы с клиентским процессом. Это позволяет объектам COM реализовывать мощные бэкэнды для простых фронтэндов GUI, таких как Visual Basic и ASP. Их также можно программировать с помощью языков сценариев. [12]
Из-за уязвимости, обычно известной как перехват DLL, подмена DLL, предварительная загрузка DLL или двоичная установка, многие программы загружают и выполняют вредоносную DLL, содержащуюся в той же папке, что и файл данных, открытый этими программами. [13] [14] [15] [16] Уязвимость была обнаружена Георгием Гунински в 2000 году. [17] В августе 2010 года она получила всемирную огласку после того, как ACROS Security повторно обнаружила ее, и многие сотни программ были признаны уязвимыми. [18] Программы, которые запускаются из небезопасных мест, т. е. из папок, доступных для записи пользователем, таких как каталог Downloads или Temp , почти всегда подвержены этой уязвимости. [19] [20] [21] [22] [23] [24] [25]
Библиотека импорта — это обычная UNIX-подобная библиотека .a, но она содержит только крошечную часть информации, необходимую для того, чтобы сообщить ОС, как программа взаимодействует с ("импортирует") dll. Эта информация связана с .exe.