May 4th, 2020

Рекомендации по разработке DLL

Начало тут:Collapse )
Перевод с английского статьи от 31.05.2018 г. «Dynamic-Link Library Best Practices»:
https://docs.microsoft.com/ru-ru/windows/win32/dlls/dynamic-link-library-best-practices
(На данный момент на этом сайте нет перевода этой статьи на русский, есть только версия на английском.)

Последнее обновление данной статьи: 17 мая 2006 г. [Надо понимать, что все статьи из этой серии довольно старые. В них во всех стоит дата 31.05.2018 г., но это не дата написания этих статей, а дата переноса этих статей со старого сайта (msdn.microsoft.com) библиотеки MSDN на новый сайт (docs.microsoft.com) библиотеки «Microsoft Docs».]

Важные для данной темы функции из набора функций Windows API:

Создание библиотек DLL ставит перед разработчиками множество сложных задач. Библиотеки DLL не имеют обеспечиваемого операционной системой контроля версий. Когда в системе присутствует множество версий одной и той же библиотеки DLL, легкость записи одной версии DLL поверх другой версии, сочетаясь с отсутствием механизма контроля версий, создает конфликты зависимостей [DLL hell] и интерфейсов [API]. Сложности в среде разработки, в реализации загрузчика, в зависимостях библиотек DLL друг от друга создали слабость в поведении порядка загрузки библиотек DLL и поведении приложения. Наконец, много приложений полагаются на библиотеки DLL и имеют сложные множества зависимостей, которые должны быть соблюдены, чтобы приложения функционировали нормально. Этот документ обеспечивает методическими рекомендациями разработчиков библиотек DLL; эти рекомендации помогут создать более надежные, имеющие меньший размер и лучше расширяемые библиотеки DLL.

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


Основные рекомендации по разработке

Функция DllMain вызывается после установки и во время удерживания загрузчиком блокировки [loader-lock]. Следовательно, на функции, которые могут вызываться внутри функции DllMain, накладываются серьезные ограничения. Функция DllMain как таковая предназначена для выполнения минимальных инициализационных задач посредством использования малого подмножества функций из набора функций Windows API. Вы не можете вызвать в функции DllMain любую функцию, которая прямо или косвенно попытается вызвать установку загрузчиком блокировки. Если вы, всё же, это сделаете, то тем самым введете в ваше приложение вероятность того, что ваше приложение попадет в ситуацию взаимной блокировки или неожиданно закончит работу с ошибкой. Ошибка в реализации функции DllMain может поставить под удар весь процесс целиком и все его потоки выполнения.

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

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

Вы никогда не должны совершать следующие действия изнутри функции DllMain:
  • вызывать функцию LoadLibrary или LoadLibraryEx (прямо или косвенно). Такое действие может вызвать взаимную блокировку или неожиданное окончание работы с ошибкой;
  • вызывать функцию GetStringTypeA, GetStringTypeEx или GetStringTypeW (прямо или косвенно). Такое действие может вызвать взаимную блокировку или неожиданное окончание работы с ошибкой;
  • выполнять синхронизацию с другими потоками выполнения. Такое действие может привести к взаимной блокировке;
  • получать объект синхронизации, который принадлежит коду, ожидающему получения блокировки загрузчика. Такое действие может привести к взаимной блокировке;
  • инициализировать библиотеку COM [Component Object Model] для использования вызывающими потоками выполнения посредством вызова функции CoInitializeEx. При определенных условиях эта функция может вызывать функцию LoadLibraryEx;
  • вызывать функции реестра. Эти функции реализованы в системной библиотеке Advapi32.dll. Если системная библиотека Advapi32.dll не инициализирована перед инициализацией вашей DLL, ваша DLL может получить доступ к неинициализированной памяти и вызвать неожиданное окончание работы процесса с ошибкой;
  • вызывать функцию CreateProcess. Создание процесса может повлечь загрузку другой DLL;
  • вызывать функцию ExitThread. Завершение работы потока выполнения во время отключения DLL может вызвать новую установку загрузчиком блокировки, что повлечет взаимную блокировку или неожиданное окончание работы с ошибкой;
  • вызывать функцию CreateThread. Создание потока выполнения может завершиться успешно, если вы не выполняете синхронизацию с другими потоками выполнения, но это всё равно рискованно;
  • создавать именованный канал [named pipe] или другой именованный объект [named object] (актуально только для операционной системы «Windows 2000»). В операционной системе «Windows 2000» именованные объекты предоставлялись посредством системной библиотеки DLL служб терминалов [Terminal Services DLL; начиная с операционной системы «Windows Server 2008 R2», «службы терминалов» переименованы в «службы удаленного рабочего стола», по-английски «Remote Desktop Services» (сокращенно «RDS»)]. Если эта системная DLL не инициализирована, обращения к ней могут вызвать неожиданное окончание работы процесса с ошибкой;
  • использовать функции управления памятью из динамической версии библиотеки времени исполнения языка Си [С Run-Time, сокращенно «CRT»]. Если библиотека DLL, в которой реализованы функции CRT, не инициализирована, то обращения к функциям управления памятью из этой библиотеки могут привести к неожиданному окончанию работы процесса с ошибкой;
  • вызывать функции из системной библиотеки User32.dll или Gdi32.dll. Некоторые функции из этих системных библиотек загружают другую DLL, которая может быть неинициализированной;
  • использовать управляемый код [managed code].

Следующие действия безопасно совершать внутри функции DllMain:
  • инициализировать статические структуры данных и статические поля [классов], резервирование памяти для которых происходит во время компиляции;
  • создавать и инициализировать объекты синхронизации;
  • резервировать память и инициализировать динамические структуры данных (избегая функций, перечисленных в списке выше);
  • настраивать [пример] локальное хранилище потока (TLS);
  • открывать файлы, читать из них и писать в них;
  • вызывать функции из системной библиотеки Kernel32.dll (за исключением функций, перечисленных в списке выше);
  • устанавливать глобальные указатели в значение NULL, откладывая инициализацию динамических элементов. В операционной системе «Windows Vista» (вики: с 30.11.2006 г.) вы можете использовать функции одноразовой инициализации, чтобы гарантировать, что блок кода будет выполнен только один раз в многопоточной среде.

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

Когда вы пишете код, который использует множество объектов синхронизации, таких как блокировки [locks], жизненно важно соблюдать порядок блокировок. Когда появляется необходимость создать более одной блокировки разом, вы должны определить их явную очередность, которая называется «иерархией блокировок» или «порядком блокировок». Например, если блокировка А создана перед блокировкой Б где-то в коде, и блокировка Б создана перед блокировкой В в другом месте программы, тогда порядком блокировок считается последовательность «А, Б, В» и этот порядок должен соблюдаться в любой части программы. Инверсия порядка блокировок происходит, когда порядок блокировок не соблюдается — например, если блокировка Б создается перед блокировкой А. Инверсия порядка блокировок может вызывать взаимные блокировки, которые сложно найти и исправить. Чтобы избежать таких проблем, все потоки выполнения должны создавать блокировки в одном и том же порядке.

Важно отметить, что загрузчик вызывает функцию DllMain с уже созданной загрузчиком блокировкой, поэтому эта блокировка должна иметь самый высокий приоритет в иерархии блокировок. Также отметим, что программа должна привлекать только те блокировки, которые ей требуются для правильной синхронизации; она не должна привлекать каждую отдельную блокировку из числа определенных в иерархии. Например, если секция кода требует для правильной синхронизации только блокировки А и В, то код должен использовать блокировку А перед использованием блокировки В; для программы нет необходимости в использовании блокировки Б. Кроме того, код DLL не может явно получить блокировку от загрузчика. Если код DLL должен вызвать интерфейс [API] такой, как функция GetModuleFileName, который может неявно получить блокировку от загрузчика, и также код DLL при этом должен получить свою внутреннюю блокировку [private lock], то код DLL должен вызвать функцию GetModuleFileName перед получением своей внутренней блокировки, таким образом гарантируя, что порядок блокировок будет соблюден.

На рисунке 2 (см. ниже) показан пример, который иллюстрирует инверсию порядка блокировок. Рассмотрим DLL, чей главный поток выполнения содержит обращение к функции DllMain. Загрузчик библиотеки DLL устанавливает блокировку L и затем вызывает функцию DllMain. Главный поток выполнения создает объекты синхронизации A, B и G для сериализации доступа [обычно под сериализацией подразумевается перевод некоего объекта в последовательность байтов, но под сериализацией доступа (to serialize access) подразумевается «устроить доступ к ресурсу так, чтобы в один момент времени доступ к этому ресурсу мог иметь только один поток выполнения»] к его структурам данных и затем пытается включить блокировку G. Рабочий поток выполнения, который уже успешно включил блокировку G, после этого вызывает функцию, такую как GetModuleHandle, которая пытается включить блокировку L от загрузчика. Таким образом, рабочий поток выполнения блокирован на попытке включить блокировку L, а главный поток выполнения блокирован на попытке включить блокировку G, что означает попадание в ситуацию взаимной блокировки.


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

Рекомендации по разработке, касающиеся синхронизации

Рассмотрим DLL, которая создает рабочие потоки выполнения во время ее инициализации. При очистке DLL [под очисткой (cleanup) здесь имеется в виду операция, противоположная по смыслу инициализации и выполняющаяся при выгрузке DLL] необходима синхронизация со всеми рабочими потоками, чтобы гарантировать, что структуры данных находятся в согласованном состоянии [«consistent state», то есть данные согласуются друг с другом, выполняются условия их целостности и внутренней непротиворечивости] и затем можно прекращать работу рабочих потоков. На сегодня не существует простого способа полностью решить проблему синхронизации при очистке и завершении работы библиотек DLL в многопоточной среде. Этот раздел описывает текущие рекомендации по синхронизации потоков во время завершения работы DLL.

Синхронизация потоков в функции DllMain во время завершения процесса
  • К моменту, когда функция DllMain вызывается при завершении процесса, все потоки выполнения процесса уже принудительно очищены и существует вероятность, что адресное пространство находится в несогласованном состоянии [inconsistent]. Синхронизация в этом случае не требуется. Другими словами, идеальный обработчик события DLL_PROCESS_DETACH должен быть пустым.
  • Операционная система «Windows Vista» (вики: с 30.11.2006 г.) гарантирует, что базовые для системы [core] структуры данных (переменные среды, текущий каталог, куча процесса и так далее) находятся в согласованном состоянии. Однако, другие структуры данных могут быть повреждены, поэтому очистка памяти небезопасна.
  • Информация о состоянии приложения [persistent state], которую нужно сохранить, должна быть переписана на постоянное запоминающее устройство [permanent storage].

Синхронизация потоков в функции DllMain в обработчике события DLL_THREAD_DETACH во время выгрузки DLL
  • Когда DLL выгружается, адресное пространство [которое она использовала] не выбрасывается. Поэтому от DLL ожидается, что она выполнит завершение работы с очисткой [адресного пространства]. Эта операция очистки касается синхронизации потоков, открытых дескрипторов, информации о состоянии приложения [persistent state] и зарезервированных ресурсов.
  • Синхронизация потоков — это тема, которая полна подводных камней, потому что ожидание, пока потоки завершат свою работу, в функции DllMain может вызвать взаимную блокировку. Например, библиотека DLL, обозначенная буквой A, удерживает блокировку от загрузчика. Она сигнализирует потоку T завершить работу и ожидает, пока поток завершит работу. Поток T завершает работу и загрузчик пытается установить блокировку от загрузчика, чтобы вызвать функцию DllMain библиотеки DLL A с событием DLL_THREAD_DETACH. Такая ситуация приведет к взаимной блокировке. Для минимизации риска взаимной блокировки:
    • библиотека DLL A получает сообщение DLL_THREAD_DETACH в ее функции DllMain и запускает событие для потока T, сигнализируя ему завершать работу;
    • поток T заканчивает своё текущее задание, приводит себя в согласованное состояние, сигнализирует библиотеке DLL A и переходит в состояние бесконечного ожидания. Отметим, что действия по проверке согласованности потока должны следовать тем же ограничениям, что и функция DllMain, чтобы избежать взаимной блокировки;
    • библиотека DLL A завершает поток T, зная, что он находится в согласованном состоянии.

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

Дополнительные рекомендации

Рекомендуется выполнять нижеследующие указания:
  • используйте утилиту «Application Verifier» [по-русски «верификатор приложения»], чтобы отловить самые распространенные ошибки в функции DllMain;
  • если используете свою собственную внутреннюю блокировку в функции DllMain, то определите иерархию блокировок и соблюдайте ее постоянно. Блокировка от загрузчика должна быть в основе этой иерархии [то есть приоритетной];
  • проверьте, что нет вызовов, зависящих от другой DLL, которая еще может быть загружена не полностью;
  • выполняйте простые инициализации статически во время компиляции, а не в функции DllMain;
  • отложите любые вызовы функций, которые вы собирались сделать в функции DllMain, на более поздний срок, если это возможно;
  • отложите инициализационные задачи, которые могут подождать, на более поздний срок. Некоторые условия ошибок должны проверяться как можно раньше, чтобы приложение могло при возникновении этих ошибок управляемо завершать свою работу. Однако, тут требуется сделать выбор между возможностью раннего обнаружения ошибок и соблюдением устойчивости программы, которая может уменьшится в результате ранней инициализации этого механизма обнаружения ошибок. Отложить инициализацию в этой ситуации — часто лучший выбор.