Рекомендации по библиотеке динамических ссылок

**Обновлено:**

  • 17 мая 2006 г.

Важные API

Создание библиотек DLL представляет ряд проблем для разработчиков. Библиотеки DLL не имеют системного управления версиями. Если в системе существует несколько версий библиотеки DLL, простота перезаписи в сочетании с отсутствием схемы управления версиями создает зависимости и конфликты API. Сложность в среде разработки, реализации загрузчика и зависимостей DLL создала хрупкость в порядке загрузки и поведении приложения. Наконец, многие приложения полагаются на библиотеки DLL и имеют сложные наборы зависимостей, которые должны учитываться для правильной работы приложений. В этом документе содержатся рекомендации для разработчиков БИБЛИОТЕК DLL, помогающие создавать более надежные, переносимые и расширяемые библиотеки DLL.

Неправильная синхронизация в DllMain может привести к взаимоблокировке приложения или доступу к данным или коду в неинициализированной библиотеке DLL. Вызов некоторых функций из DllMain вызывает такие проблемы.

what happens when a library is loaded

Общие рекомендации

DllMain вызывается во время блокировки загрузчика. Поэтому значительные ограничения накладываются на функции, которые могут вызываться в DllMain. Таким образом, DllMain предназначен для выполнения минимальных задач инициализации с помощью небольшого подмножества API Microsoft® Windows®. Вы не можете вызвать любую функцию в DllMain , которая напрямую или косвенно пытается получить блокировку загрузчика. В противном случае вы введете возможность взаимоблокировки или сбоя приложения. Ошибка в реализации DllMain может поставить под угрозу весь процесс и все его потоки.

Идеальной библиотекой DllMain будет просто пустая заглушка. Однако, учитывая сложность многих приложений, это, как правило, слишком ограничительно. Хорошее правило для DllMain заключается в том, чтобы отложить как можно больше инициализации. Отложенная инициализация повышает надежность приложения, так как эта инициализация не выполняется во время блокировки загрузчика. Кроме того, отложенная инициализация позволяет безопасно использовать гораздо больше API Windows.

Некоторые задачи инициализации нельзя отложить. Например, библиотека DLL, которая зависит от файла конфигурации, не должна загружаться, если файл неправильно сформирован или содержит мусор. Для этого типа инициализации библиотека DLL должна пытаться выполнить действие и быстро завершить работу, а не тратить ресурсы, выполнив другие действия.

Вы никогда не должны выполнять следующие задачи из библиотеки DllMain:

  • Вызов LoadLibrary или LoadLibraryEx (напрямую или косвенно). Это может вызвать взаимоблокировку или сбой.
  • Вызовите GetStringTypeA, GetStringTypeEx или GetStringTypeW (прямо или косвенно). Это может вызвать взаимоблокировку или сбой.
  • Синхронизация с другими потоками. Это может привести к взаимоблокировки.
  • Получите объект синхронизации, принадлежащий коду, который ожидает получения блокировки загрузчика. Это может привести к взаимоблокировки.
  • Инициализация потоков COM с помощью CoInitializeEx. В определенных условиях эта функция может вызывать LoadLibraryEx.
  • Вызовите функции реестра.
  • Вызов CreateProcess. Создание процесса может загрузить другую библиотеку DLL.
  • Вызов ExitThread. Выход из потока во время отсоединения библиотеки DLL может привести к повторному получению блокировки загрузчика, что приводит к взаимоблокировке или сбою.
  • Вызов CreateThread. Создание потока может работать, если вы не синхронизируете с другими потоками, но это рискованно.
  • Вызов shGetFolderPathW. Вызов api оболочки или известных папок может привести к синхронизации потоков и, следовательно, может привести к взаимоблокировкам.
  • Создайте именованный канал или другой именованный объект (только Для Windows 2000). В Windows 2000 именованные объекты предоставляются библиотекой DLL служб терминалов. Если эта библиотека DLL не инициализирована, вызовы библиотеки DLL могут привести к сбою процесса.
  • Используйте функцию управления памятью из динамического времени выполнения C (CRT). Если библиотека DLL CRT не инициализирована, вызовы этих функций могут привести к сбою процесса.
  • Вызов функций в User32.dll или Gdi32.dll. Некоторые функции загружают другую библиотеку DLL, которая не может быть инициализирована.
  • Используйте управляемый код.

Следующие задачи безопасны для выполнения в DllMain:

  • Инициализировать статические структуры и элементы данных во время компиляции.
  • Создание и инициализация объектов синхронизации.
  • Выделение памяти и инициализация динамических структур данных (избегая перечисленных выше функций).)
  • Настройка локального хранилища потока (TLS).
  • Открытие, чтение и запись в файлы.
  • Вызов функций в Kernel32.dll (за исключением функций, перечисленных выше).
  • Задайте для глобальных указателей значение NULL, отключив инициализацию динамических элементов. В Microsoft Windows Vista™ можно использовать функции однократной инициализации, чтобы обеспечить выполнение блока кода только один раз в многопоточной среде.

Взаимоблокировки, вызванные инверсией порядка блокировки

При реализации кода, использующего несколько объектов синхронизации, таких как блокировки, важно учитывать порядок блокировки. Если необходимо получить несколько блокировок одновременно, необходимо определить явный приоритет, который называется иерархией блокировки или порядком блокировки. Например, если блокировка A приобретается перед блокировкой B где-то в коде, и блокировка B приобретается перед блокировкой C в другом месте кода, то порядок блокировки — A, B, C и этот порядок должен выполняться по всему коду. Инверсия порядка блокировки возникает, если порядок блокировки не выполняется, например, если блокировка B приобретается перед блокировкой A. Инверсия порядка блокировки может привести к взаимоблокировкам, которые трудно отлаживать. Чтобы избежать таких проблем, все потоки должны получать блокировки в одном порядке.

Важно отметить, что загрузчик вызывает DllMain с уже полученной блокировкой загрузчика, поэтому блокировка загрузчика должна иметь наивысший приоритет в иерархии блокировки. Кроме того, обратите внимание, что код должен получить блокировки, необходимые для правильной синхронизации; Он не должен получать каждую блокировку, определенную в иерархии. Например, если для раздела кода требуется только блокировка A и C для правильной синхронизации, код должен получить блокировку A, прежде чем получить блокировку C; Коду также не требуется получить блокировку B. Кроме того, код DLL не может явно получить блокировку загрузчика. Если код должен вызвать API, например GetModuleFileName, который может косвенно приобрести блокировку загрузчика, а код также должен получить частную блокировку, то код должен вызвать GetModuleFileName, прежде чем получить блокировку P, таким образом гарантируя соблюдение порядка загрузки.

Рис. 2— пример, демонстрирующий инверсию порядка блокировки. Рассмотрим библиотеку DLL, основной поток которой содержит DllMain. Загрузчик библиотеки получает блокировку загрузчика L, а затем вызывает библиотеку DllMain. Основной поток создает объекты синхронизации A, B и G для сериализации доступа к структурам данных, а затем пытается получить блокировку G. Рабочий поток, который уже успешно приобрел блокировку G, вызывает функцию, например GetModuleHandle, которая пытается получить блокировку загрузчика L. Таким образом, рабочий поток блокируется на L, а основной поток блокируется на G, что приводит к взаимоблокировке.

deadlock caused by lock order inversion

Чтобы предотвратить взаимоблокировки, вызванные инверсией порядка блокировки, все потоки должны пытаться получить объекты синхронизации в определенном порядке загрузки в любое время.

Рекомендации по синхронизации

Рассмотрим библиотеку DLL, которая создает рабочие потоки в рамках инициализации. При очистке библиотеки DLL необходимо синхронизировать все рабочие потоки, чтобы убедиться, что структуры данных находятся в согласованном состоянии, а затем завершают рабочие потоки. Сегодня нет простого способа полностью решить проблему чистой синхронизации и завершения работы библиотек DLL в многопоточной среде. В этом разделе описаны текущие рекомендации по синхронизации потоков во время завершения работы библиотеки DLL.

Синхронизация потоков в DllMain во время выхода процесса

  • К тому времени, когда DllMain вызывается при выходе процесса, все потоки процесса были принудительно очищены и есть вероятность того, что адресное пространство несогласовано. Синхронизация не требуется в этом случае. Другими словами, идеальный обработчик DLL_PROCESS_DETACH пуст.
  • Windows Vista гарантирует, что основные структуры данных (переменные среды, текущий каталог, куча процессов и т. д.) находятся в согласованном состоянии. Однако другие структуры данных могут быть повреждены, поэтому очистка памяти не является безопасной.
  • Постоянное состояние, которое необходимо сохранить, должно быть удалено в постоянное хранилище.

Синхронизация потоков в DllMain для DLL_THREAD_DETACH во время выгрузки библиотеки DLL

  • При выгрузке библиотеки DLL адресное пространство не будет выброшено. Таким образом, библиотека DLL должна выполнить чистое завершение работы. Это включает синхронизацию потоков, открытые дескрипторы, постоянное состояние и выделенные ресурсы.
  • Синхронизация потоков сложна, так как ожидание выхода потоков в DllMain может привести к взаимоблокировке. Например, библиотека DLL A содержит блокировку загрузчика. Он сигнализирует о выходе потока T и ожидает выхода потока. Поток T завершает работу и загрузчик пытается получить блокировку загрузчика, чтобы вызвать библиотеку DLL A DllMain с DLL_THREAD_DETACH. Это вызывает взаимоблокировку. Чтобы свести к минимуму риск взаимоблокировки, выполните следующие действия.
    • DLL A получает сообщение DLL_THREAD_DETACH в библиотеке DllMain и задает событие для потока T, сигналив о выходе из него.
    • Thread T завершает текущую задачу, приносит себя в согласованное состояние, сигнализирует DLL A и ожидает бесконечно. Обратите внимание, что подпрограммы согласованности проверка должны следовать тем же ограничениям, что и DllMain, чтобы избежать взаимоблокировки.
    • БИБЛИОТЕКА DLL завершает T, зная, что она находится в согласованном состоянии.

Если библиотека DLL выгружается после создания всех его потоков, но перед началом выполнения потоки могут завершиться сбоем. Если библиотека DLL создала потоки в библиотеке DLLMain в рамках его инициализации, некоторые потоки, возможно, не завершили инициализацию и их DLL_THREAD_ATTACH сообщение по-прежнему ожидает доставки в библиотеку DLL. В этом случае, если библиотека DLL выгружается, начнется завершение потоков. Однако некоторые потоки могут быть заблокированы за блокировкой загрузчика. Их DLL_THREAD_ATTACH сообщения обрабатываются после отмены сопоставления библиотеки DLL, что приводит к сбою процесса.

Рекомендации

Ниже приведены рекомендации.

  • Используйте средство проверки приложений для перехвата наиболее распространенных ошибок в DllMain.
  • Если используется частная блокировка внутри DllMain, определите иерархию блокировки и последовательно используйте ее. Блокировка загрузчика должна находиться в нижней части этой иерархии.
  • Убедитесь, что вызовы не зависят от другой библиотеки DLL, которая, возможно, еще не была загружена полностью.
  • Выполняйте простые инициализации статически во время компиляции, а не в DllMain.
  • Отложите все вызовы в DllMain , которые могут ждать до конца.
  • Отложить задачи инициализации, которые могут ждать до конца. Некоторые условия ошибки должны быть обнаружены рано, чтобы приложение могли обрабатывать ошибки корректно. Однако между этим ранним обнаружением и потерей надежности, которые могут привести к ее возникновению, существуют компромиссы. Отсрочка инициализации часто лучше всего подходит.