DLL hell
DLL hell — термин из жаргона, обозначающий ряд проблем, возникающих при работе с динамически подключаемыми библиотеками в более ранних версиях операционных систем Microsoft Windows[1], особенно в устаревших 16-битных редакциях, где все процессы работают в едином адресном пространстве. Проявления «DLL hell» могут быть разными — в частности, нарушается корректная работа программ, вплоть до полной невозможности их запуска. «DLL hell» — частный случай более общей проблемы, известной как адское дерево зависимостей, характерной именно для экосистемы Windows.
Проблемы
DLL представляют собой реализацию разделяемых библиотек от компании Microsoft. Разделяемые библиотеки позволяют оформить общий код в виде DLL, которую затем могут использовать любые прикладные программы без необходимости загружать несколько копий кода в память. К примеру, текстовый редактор с графическим интерфейсом, который используется многими приложениями, помещается в отдельную DLL, и все программы могут пользоваться им, не расходуя лишнюю память. Для сравнения, статическая библиотека требует копирования кода непосредственно в состав приложения, что приводит к увеличению размера каждого исполняемого файла на объём всех используемых им библиотек. Это особенно заметно для современных крупных программ.
Проблема возникает тогда, когда версия DLL на компьютере отличается от той, c которой компилировалась программа. Структуры совместимости по умолчанию в DLL не заложено, и даже незначительные изменения могут привести к тому, что внутреннее устройство библиотеки будет сильно отличаться от прежних версий. В результате попытка использовать такую DLL вызывает сбои или аварийное завершение приложения. В случае со статическими библиотеками эта проблема отсутствует, поскольку нужная версия уже включена в само приложение, и наличие более новых версий библиотеки в системе не влияет на его работу.
Важной причиной несовместимости версий является структура файла DLL. Внутри содержится каталог доступных процедур (методов, функций и т. д.) и типов данных для передачи и возврата значений. Даже небольшие правки могут перестроить каталог, и тогда вызовы функций по порядковому номеру приведут к обращению к некорректным участкам кода и, как следствие, сбоям.
Наиболее часто встречающиеся проблемы с DLL наблюдаются при многократной установке и удалении разных программ. К типовым трудностям относятся конфликты версий библиотек, затруднения при поиске нужных DLL, а также избыточность — наличие множества лишних копий одних и тех же библиотек.
Решения для этих проблем были известны ещё в момент создания системы DLL, и реализованы в замене на платформу .NET, где вместо DLL используются сборки.
Определённая версия библиотеки может быть совместима с одними программами и несовместима с другими. В системе Windows эта проблема проявлялась особенно остро из-за широкого применения динамической загрузки C++-библиотек и объектов OLE. C++-классы экспортируют множество методов, и даже незначительное изменение, например, добавление виртуального метода, делает класс несовместимым с программами, собранными с предыдущей версией. В OLE для предотвращения подобных конфликтов действуют строгие правила: необходимо сохранять стабильность интерфейсов и не использовать общий диспетчер памяти, однако изменение семантики класса всё равно может повлиять на совместимость. Исправление ошибки для одного приложения может незаметно привести к удалению нужной функции для другого. До появления Windows 2000 все COM-объекты регистрировались глобально, и на систему мог быть установлен только один объект с данным идентификатором класса. Если приложение создавало экземпляр такого класса, оно получало последнюю зарегистрированную реализацию, что приводило к поломке других программ после установки новых версий общих компонентов.
Распространённая проблема — перезапись ранее работавшей DLL новой программой, которая устанавливает несовместимую или устаревшую версию. Примерами таких DLL в эпоху Windows 3.1 являются ctl3d.dll и ctl3dv2.dll — библиотеки Microsoft, распространявшиеся сторонними разработчиками, которые включали в свои пакеты не самые новые версии, а те, с которыми работали при разработке[2]. Причины такой ситуации:
- Корпорация Microsoft распространяла системные DLL как общие компоненты, например, в C:\WINDOWS и C:\WINDOWS\SYSTEM, чтобы экономно расходовать память и дисковое пространство. Вслед за ней так же поступали сторонние разработчики[3].
- Установщики приложений обычно выполнялись с повышенными привилегиями и могли записывать DLL в системные папки и изменять реестр для регистрации новых объектов COM. Некорректная работа установщика могла привести к откату версии библиотеки на устаревших системах, если не было механизмов защиты Windows File Protection, Windows Resource Protection. Начиная с Windows Vista, только учётная запись «доверенного установщика» имеет право менять системные библиотеки.
- Разработчики программ имели право включать сервисные обновления Windows в состав своих установщиков, а многие DLL Microsoft распространялись как отдельно подключаемые компоненты.
- До появления Windows Installer, установочные программы были зачастую самописными, и разработчики могли не учесть нюансы проверки версий и перезаписывали DLL без должного контроля[4].
- Некоторые среды разработки не добавляли данные о версии в скомпилированные библиотеки, поэтому проверка производилась только по дате файла или отсутствовала вовсе.
- Иногда сама операционная система могла заменить новые DLL на устаревшие, например, после установки чёрно-белого принтера в Windows 2000, который перезаписывал библиотеки цветного принтера[5].
В технологии COM и других частях Windows до внедрения так называемых «side-by-side» сборок (без необходимости глобальной регистрации в реестре)[6] для определения, какую DLL загрузить, использовались записи в реестре. При регистрации новой версии модуля загружалась именно она, даже если другое приложение ожидало другую версию. Такая ситуация возникала при конфликтных установках, в результате чего последнее установленное приложение «побеждало».
В 16-битных версиях Windows и при использовании Windows on Windows загружалась только одна копия каждой DLL, а все приложения обращались к единственному экземпляру в памяти, пока он не выгружался. В 32- и 64-битных Windows совместное использование между процессами происходит только при загрузке одного и того же файла из одной папки. Код, а не стек, разделяется через механизм отображения памяти. Если несовместимая DLL была загружена ранее другим приложением из иной директории, попытка запуска другой копии может привести к сбоям, и ошибка возникает только при определённом порядке запуска программ.
В противовес проблеме перезаписи DLL: если обновления библиотеки не сказываются на всех приложениях, использующих её, становится намного сложнее её обслуживать — устранять ошибки и выпускать исправления. Это особенно актуально для безопасности: приходится тестировать совместимость новой версии с каждым приложением.
Причины
К причинам «DLL hell» относятся:
- Ограничения объёма памяти и отсутствие индивидуального адресного пространства в 16-битных Windows,
- Отсутствие стандартов по версионированию, именованию и размещению DLL,
- Недостаточная строгость процедур установки и удаления программ (пакетный менеджмент отсутствовал),
- Нет централизованной поддержки и контроля ABI DLL — возможно существование несовместимых DLL с одинаковыми именами и номерами версий,
- Недостаточно развитые инструменты администрирования, затрудняющие диагностику конфликтов DLL,
- Разработчики, нарушающие обратную совместимость при обновлении модулей,
- Внеплановые обновления компонентов Windows, выпущенные Microsoft,
- Отсутствие поддержки одновременного использования разных версий одной библиотеки в ранних версиях Windows,
- Опора на текущую директорию или переменную среды
%PATH%для поиска DLL, что различается на разных компьютерах, - Повторное использование идентификаторов класса из примерных программ COM вместо генерации уникальных идентификаторов.
В 16-битных версиях Windows и до появления семейства Windows NT проблема «DLL hell» была особенно острой именно из-за отсутствия индивидуального адресного пространства для процессов, что не позволяло приложениям загружать свои версии разделяемых модулей. От разработчиков установщиков ожидалось, что они будут вести себя корректно и проверять версию DLL перед заменой системных библиотек. Microsoft и сторонние вендоры предоставляли стандартные установочные программы, а для получения права использования логотипа Windows требовали сертифицировать установщик. Однако рост распространения Интернета способствовал расширению круговложения неофициальных программ, что ухудшало ситуацию.
Поиск DLL Windows осуществляет в ряде директорий, и если имя не полностью определено, вредоносные программы могут воспользоваться этим. Один из способов — предзагрузка DLL либо атака через внедрение библиотеки, когда файл требуется DLL помещается в каталог, который система обходит в поиске первым, например, текущий рабочий каталог. Запущенная программа загружает вредоносную DLL, которая получает привилегии приложения[7].
Иной способ — relative path DLL hijacking: программа и DLL размещаются вместе в каталоге, который ищется первым по умолчанию; данный метод признан наиболее распространённым[8]. DLL sideloading подразумевает поставку одновременно честного приложения и «примкнувшей» библиотеки; обнаружить атаку сложно, так как выглядит как запуск доверенного ПО[9].
Также возможны атаки типа phantom DLL hijacking — создание DLL с именем отсутствующей на системе библиотеки; или модификация реестра для изменения порядка поиска DLL[7].
DLL-хайджекинг применяется в арсенале государственных группировок, например, Lazarus Group и Tropic Trooper[9].
Решения
За годы существования проблемы различные разновидности «DLL hell» получили частичные или полные решения.
Один из простых способов — статическая компоновка всех библиотек в приложение, когда используется конкретная версия библиотеки, встроенная непосредственно в исполняемый файл[4]. Подобная практика распространена в C/C++: например, чтобы избежать проблем с MFC42.DLL, приложение компилируется со статической сборкой библиотек. Правда, при этом утрачиваются достоинства DLL, связанные с экономией памяти и возможностью централизованного обновления компонентов.
С проблемой перезаписи DLL система стала бороться с внедрением механизма Windows File Protection в Windows 2000 — он защищает критические системные библиотеки от перезаписи сторонними приложениями за исключением использования специально разрешённых API. Несмотря на это, возможна ситуация, когда даже исправления от Microsoft оказываются несовместимыми с существующими приложениями, хотя современные версии Windows за счёт механизмов side-by-side сборок существенно уменьшают этот риск.
Инсталляторы сторонних приложений теперь не могут заменить системные библиотеки, если только с их установкой не связано легитимное обновление либо если вручную не отключается служба защиты файлов. В Windows Vista и новее только служба TrustedInstaller может вносить изменения в защищённые файлы; служебная программа System File Checker также позволяет восстановить их.
Решение конфликта — размещение разных версий нужных библиотек в отдельных папках каждого приложения. Такой подход работает до тех пор, пока не используется разделяемая память DLL — это возможно для 32- и 64-битных приложений. В 16-битных средах одновременный запуск приложений несовместим с работой «нестыкующихся» DLL, а до Windows 98 SE/2000 ещё и из-за единого реестра объектов OLE.
В Windows 98 SE и Windows 2000 появился механизм «side-by-side assembly» — отдельная загрузка копий DLL для каждого приложения и, соответственно, возможность параллельного запуска программ с несовместимыми библиотеками. Такой подход позволяет сохранить преимущества разделяемых библиотек, однако не подходит для DLL с общей памятью[10]. Побочный эффект — устаревшие экземпляры DLL могут остаться в системе при автоматическом обновлении.
В отдельных случаях решение могут давать портируемые приложения, где каждая программа содержит собственные копии всех нужных DLL. Эффективность метода основана на том, что пути к нужным библиотекам полностью не задаются, поэтому операционная система ищет DLL сначала в каталоге самого приложения[11]. Однако этот способ подвержен эксплойтам[12], а также требует регулярного обновления всех DLL отдельно.
Виртуализация приложений также позволяет запустить программы в изолированной среде «песочнице», не устанавливая DLL в систему.
Ряд иных решений (иногда совместно используемых):
- В комплекте Microsoft Visual Studio поставляются установщики, выполняющие сверку версий DLL и поддерживающие готовые пакеты для интеграции компонентов Windows (.MSI).
- Восстановление системы Windows позволяет откатить неудачную установку приложения, включая коррекцию реестра.
- Каталог WinSxS, где могут сосуществовать несколько версий одной библиотеки.
- Запуск 16-битных приложений в отдельном адресном пространстве под управлением 32-битной Windows.
- Использование версий Windows с поддержкой Windows File Protection, начиная с Windows Me и Windows 2000, далее — Windows XP и Windows Server 2003. В Windows Vista/Server 2008 появился новый механизм защиты — Windows Resource Protection.
- Для COM впервые в Windows XP реализована регистрация объектов на уровне приложения «registration-free COM» — COM-библиотеки регистрируются только в папке приложения, а не глобально, что позволяет совмещать разные версии DLL на одной системе. Однако такая регистрация применяется только к Dll COM-серверам и не подходит для системных компонентов: MDAC, MSXML, DirectX, Internet Explorer.
- Интеграция в состав Windows развёрнутой системы управления пакетами, способной отслеживать зависимости DLL Windows Installer.
- Централизованная база авторитетных библиотек и системы разрешения конфликтов DLL.
- Индивидуальная поставка модифицированных или уникальных DLL только в каталог приложения, либо статическая компоновка в случае необходимости принципиальных отличий.
- На современных системах, где ограничения памяти малозначимы, DLL оправданы не всегда. При необходимости использования библиотеки только конкретной программой её можно скомпилировать статически, либо выделить в отдельный модуль.
- В системах Windows Vista и новее установка системных файлов осуществляется только через службу TrustedInstaller, в результате чего обычные пользователи, включая SYSTEM, не могут изменять критические бинарные файлы; а начиная с Windows 7 — и защищённые части реестра.
Примечания
Литература
- Anderson, Rick The End of DLL Hell. microsoft.com (11 января 2000). Дата обращения: 7 июля 2010. Архивировано 5 июня 2001 года.
- Pfeiffer, Tim Windows DLLs: Threat or Menace? Dr. Dobb's Journal (1 июня 1998). Дата обращения: 7 июля 2010. Архивировано 7 августа 2010 года.
- Desitter, Arnaud Using static and shared libraries across platforms; Row 9: Library Path. ArnaudRecipes (15 июня 2007). Дата обращения: 7 июля 2010. Архивировано 1 июня 2008 года.
Ссылки
- Как избавиться от DLL hell на Microsoft TechNet
- Упрощение установки и решение DLL hell с помощью .NET Framework на MSDN
- Избегая DLL Hell: внедрение метаданных приложений в .NET от Мэтта Пиетрека
- Dr. Dobb's о DLL hell (подробности о LoadLibraryEx)
- Обсуждение на Joel on Software Архивировано 30 октября 2018 года.
- Статья о DLL hell


