Введение
Реализация Microsoft концепции разделяемой библиотеки в Windows и OS/2. Реализация в OS/2 и Windows.
the OS/2 and Windows implementation
Библиотека динамической связи (DLL) — это разделяемая библиотека в операционной системе Microsoft Windows или OS/2. DLL может содержать исполняемый код (функции), данные и ресурсы в любом сочетании.
Расширения файлов
Файл DLL часто имеет расширение dll, но может иметь любое расширение. Разработчики могут выбирать расширение файла, описывающее содержимое файла, например, ocx для элементов управления ActiveX и drv для устаревшего драйвера устройства. DLL, содержащая только ресурсы, называется ресурсной DLL. Примеры включают библиотеку значков, иногда с расширением icl, и библиотеку шрифтов с расширениями fon и fot.
Формат файла
Формат файла DLL такой же, как у исполняемого файла (a. k. a. 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" (DLL). API для рисования, графический интерфейс устройства (GDI), был реализован в DLL с именем GDI.EXE, а пользовательский интерфейс – в USER.EXE. Эти дополнительные слои поверх DOS должны были использоваться всеми запущенными программами Windows, не только для того, чтобы Windows могла работать на машине с менее чем мегабайтом оперативной памяти, но и для обеспечения взаимодействия программ друг с другом. Код в GDI должен был преобразовывать команды рисования в операции над конкретными устройствами. На дисплее он должен был манипулировать пикселями в буфере кадров. При рисовании на принтере вызовы API должны были преобразовываться в запросы к принтеру. Хотя можно было бы обеспечить аппаратную поддержку ограниченного набора устройств (например, дисплея Color Graphics Adapter, языка команд принтера HP LaserJet), Microsoft выбрала другой подход. GDI работала путем загрузки различных фрагментов кода, называемых "драйверами устройств", для работы с различными выходными устройствами. Та же архитектурная концепция, которая позволяла GDI загружать различные драйверы устройств, также позволяла оболочке Windows загружать различные программы Windows, и этим программам вызывать вызовы API из общих библиотек USER и GDI. Эта концепция называлась "динамической связью". В обычной неразделяемой статической библиотеке разделы кода просто добавляются к вызывающей программе при создании ее исполняемого файла на этапе "связывания"; если две программы вызывают одну и ту же подпрограмму, она включается в обе программы во время этапа связывания. При динамической связи общий код помещается в один отдельный файл. Программы, вызывающие этот файл, подключаются к нему во время выполнения, а операционная система (или, в случае ранних версий Windows, расширение ОС) выполняет связывание. Для этих ранних версий Windows (1.0–3.11) DLL были основой всего графического интерфейса. Таким образом, драйверы дисплея были просто DLL с расширением DRV, которые предоставляли пользовательские реализации того же API рисования через унифицированный интерфейс драйвера устройства (DDI), а API рисования (GDI) и GUI (USER) были просто вызовами функций, экспортируемыми системными DLL GDI и USER с расширением EXE. Эта концепция построения операционной системы из коллекции динамически загружаемых библиотек является основополагающей для Windows и сохраняется до сих пор. DLL обеспечивают стандартные преимущества общих библиотек, такие как модульность. Модульность позволяет вносить изменения в код и данные в единой самодостаточной DLL, используемой несколькими приложениями, без внесения изменений в сами приложения. Еще одним преимуществом модульности является использование общих интерфейсов для плагинов. Можно разработать единый интерфейс, который позволяет беспрепятственно интегрировать старые и новые модули во время выполнения в существующие приложения без их модификации. Эта концепция динамической расширяемости доведена до крайности в компонентной объектной модели, лежащей в основе 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 выполняется в адресном пространстве вызывающего процесса и с теми же правами доступа, что означает небольшие накладные расходы при использовании, но также и отсутствие защиты для вызывающей программы в случае наличия ошибок в 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. В Cygwin/MSYS и MinGW импортные библиотеки обычно имеют суффикс dll.a, объединяющий суффикс DLL Windows и суффикс ar Unix. Формат файла схож, но символы, используемые для обозначения импорта, отличаются (head foo dll vs IMPORT DESCRIPTOR foo). Хотя инструментарий GNU Binutils может генерировать импортные библиотеки и выполнять с ними компоновку, компоновка непосредственно с DLL выполняется быстрее. Экспериментальный инструмент genlib в MinGW позволяет генерировать импортные библиотеки с символами в стиле MSVC.
Разрешение символов и их обязательность
Каждая функция, экспортируемая DLL, идентифицируется числовым порядковым номером и, опционально, именем. Аналогично, функции могут быть импортированы из DLL либо по порядковому номеру, либо по имени. Порядковый номер представляет собой позицию указателя адреса функции в таблице адресов экспорта DLL (Export Address Table). Обычно внутренние функции экспортируются только по порядковому номеру. Для большинства функций Windows API сохраняются только имена в разных версиях Windows; порядковые номера могут изменяться. Таким образом, надежно импортировать функции Windows API по их порядковым номерам нельзя. Импорт функций по порядковому номеру обеспечивает лишь незначительное повышение производительности по сравнению с импортом по имени: экспортные таблицы DLL упорядочены по имени, поэтому для поиска функции можно использовать двоичный поиск. Затем индекс найденного имени используется для поиска порядкового номера в таблице порядковых номеров экспорта (Export Ordinal Table). В 16-битных версиях Windows таблица имен не была отсортирована, поэтому накладные расходы на поиск имени были гораздо более заметными. Также можно привязать исполняемый файл к конкретной версии DLL, то есть разрешить адреса импортированных функций во время компиляции. Для привязанного импорта компоновщик сохраняет метку времени и контрольную сумму DLL, к которой привязан импорт. Во время выполнения Windows проверяет, используется ли та же версия библиотеки, и, если да, Windows пропускает обработку импорта. В противном случае, если библиотека отличается от той, к которой была произведена привязка, Windows обрабатывает импорт обычным образом. Привязанные исполняемые файлы загружаются немного быстрее, если они запускаются в той же среде, для которой они были скомпилированы, и с тем же временем, если они запускаются в другой среде, поэтому привязка импорта не имеет недостатков. Например, все стандартные приложения Windows привязаны к системным DLL-файлам соответствующих версий Windows. Удобный момент для привязки импорта приложения к целевой среде – во время установки приложения. Это сохраняет библиотеки "привязанными" до следующего обновления ОС. Однако это изменяет контрольную сумму исполняемого файла, поэтому это нельзя сделать с подписанными программами или программами, управляемыми инструментом управления конфигурацией, который использует контрольные суммы (например, MD5) для управления версиями файлов. Поскольку более новые версии Windows отказались от использования фиксированных адресов для каждой загруженной библиотеки (в целях безопасности), возможность и ценность привязки исполняемого файла уменьшаются.
Явное соединение времени выполнения
Файлы DLL могут быть явно загружены во время выполнения, процесс, который Microsoft называет просто динамической связью во время выполнения, с помощью API-функции LoadLibrary (или LoadLibraryEx). API-функция GetProcAddress используется для поиска экспортируемых символов по имени, а FreeLibrary – для выгрузки DLL. Эти функции аналогичны dlopen, dlsym и dlclose в стандартном POSIX API. Процедура явной динамической загрузки во время выполнения одинакова для любого языка, поддерживающего указатели на функции, поскольку она зависит от Windows API, а не от конструкций языка.
Задержка в загрузке
Обычно приложение, связанное с библиотекой импорта DLL, не запустится, если DLL не будет найдена, поскольку Windows не запустит приложение, если не сможет найти все DLL, которые ему могут потребоваться. Однако приложение может быть связано с библиотекой импорта для реализации отложенной загрузки динамической библиотеки. В этом случае операционная система не будет пытаться найти или загрузить DLL при запуске приложения; вместо этого, компоновщик включит в приложение заглушку, которая будет пытаться найти и загрузить DLL с помощью функций LoadLibrary и GetProcAddress при вызове одной из её функций. Если DLL не удастся найти или загрузить, или если вызываемая функция не существует, приложение сгенерирует исключение, которое можно перехватить и обработать соответствующим образом. Если приложение не обработает исключение, операционная система перехватит его и завершит программу с сообщением об ошибке. Механизм отложенной загрузки также предоставляет механизмы уведомлений, позволяя приложению выполнять дополнительную обработку или обработку ошибок при загрузке DLL и/или вызове любой функции DLL.
Дельфийские
В исходном файле вместо понятия "программа" используется "библиотека". В конце файла перечисляются функции, предназначенные для экспорта, в разделе `exports`. Delphi не требует LIB-файлов для импорта функций из DLL; для подключения к DLL в объявлении функции используется ключевое слово `external`, указывающее имя DLL, за которым следует имя символа (если оно отличается от имени функции) или индекс для идентификации функции по индексу.
Microsoft Visual Basic (всего лишь один)
В Visual Basic (VB) поддерживается только динамическая связь; однако, помимо использования API-функций LoadLibrary и GetProcAddress, разрешены объявления импортируемых функций. При импорте функций DLL через объявления, VB выдаст ошибку времени выполнения, если файл DLL не найден. Разработчик может перехватить эту ошибку и обработать её соответствующим образом. При создании DLL в VB, среда разработки позволяет создавать только ActiveX DLL, однако существуют способы, позволяющие пользователю явно указать компоновщику включить DEF-файл, который определяет порядковый номер и имя каждой экспортируемой функции. Это позволяет пользователю создавать стандартную DLL для Windows с использованием Visual Basic (версии 6 и ниже), на которую можно ссылаться с помощью оператора "Declare".
C и C++
Microsoft Visual C++ (MSVC) предоставляет несколько расширений стандарта C++, позволяющих указывать функции как импортируемые или экспортируемые непосредственно в коде C++. Эти расширения были приняты другими компиляторами C и C++ для Windows, включая версии GCC для Windows. Для этого используется атрибут `declspec` перед объявлением функции. Следует отметить, что при обращении к функциям C из C++, они также должны быть объявлены как `extern "C"` в коде C++, чтобы сообщить компилятору об использовании C-связи. Помимо указания импортируемых или экспортируемых функций с помощью атрибутов `declspec`, их можно перечислить в разделах IMPORT или EXPORTS файла DEF, используемого в проекте. Файл DEF обрабатывается компоновщиком, а не компилятором, и поэтому не является специфичным для C++. Компиляция DLL приводит к созданию как DLL, так и LIB файлов. LIB файл (импортная библиотека) используется для компоновки с DLL во время компиляции; он не требуется для компоновки во время выполнения. Если DLL не является COM-сервером (Component Object Model), DLL файл должен быть помещен в один из каталогов, указанных в переменной среды PATH, в системный каталог по умолчанию или в тот же каталог, что и программа, использующая его. DLL COM-серверов регистрируются с помощью regsvr32.exe, который помещает расположение DLL и его глобально уникальный идентификатор (GUID) в реестр. Программы затем могут использовать DLL, находя его GUID в реестре для определения его местоположения или создавая экземпляр COM-объекта косвенно, используя его идентификатор класса и идентификатор интерфейса.
Использование явной ссылки на время выполнения
Следующие примеры демонстрируют, как использовать возможности загрузки и связывания во время выполнения с помощью привязок к Windows API, специфичных для языка. Обратите внимание, что все четыре образца уязвимы для атак предварительной загрузки DLL, поскольку example.dll может быть разрешен в место, не предусмотренное автором (если явно не исключено, каталог приложения предшествует расположениям системных библиотек, и при отсутствии ключей HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode или HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\CWDIllegalInDLLSearch текущий рабочий каталог просматривается перед каталогами системных библиотек), и, следовательно, к вредоносной версии библиотеки. См. документацию Microsoft по безопасной загрузке библиотек: следует использовать в для удаления как каталога приложения, так и текущего рабочего каталога из пути поиска DLL, или использовать в для удаления текущего рабочего каталога из пути поиска DLL.
Модель объекта компонента
Компонентная объектная модель (COM) определяет бинарный стандарт для размещения реализации объектов в DLL- и EXE-файлах. Она предоставляет механизмы для обнаружения и управления версиями этих файлов, а также языково-независимое и машиночитаемое описание интерфейсов. Размещение COM-объектов в DLL более эффективно и позволяет им совместно использовать ресурсы с клиентским процессом. Это позволяет COM-объектам создавать мощные серверные части для простых графических интерфейсов, таких как Visual Basic и ASP. Их также можно программировать с использованием скриптовых языков.
Угон DLL
Из-за уязвимости, широко известной как перехват DLL, подмена DLL, предварительная загрузка DLL или внедрение вредоносного кода, многие программы загружают и выполняют вредоносную DLL, находящуюся в той же папке, что и открываемый ими файл данных. Эта уязвимость была обнаружена Георгием Гунинским в 2000 году. В августе 2010 года она получила широкую известность после повторного обнаружения ACROS Security, в результате чего было выявлено, что сотни программ уязвимы. Программы, запускаемые из небезопасных мест, то есть из папок, доступных для записи пользователем, таких как папка "Загрузки" или временная папка, почти всегда подвержены этой уязвимости.