понедельник, 2 ноября 2009 г.

Чудеса DllMain

Давно известно, что определенные практики разработки (типа использования глобальных объектов/загрузки DLL при своей загрузке и т.д.) способствуют возникновению проблем на этапе инициализации/деинициализации, но тут наткнулся на статью.

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

"

Пикник на обочине или не ходите, дети, в DllMain гулять, а то ноги оторвёт


Точка входа в DLL, так же как и точка входа в программу, - это очень специальное место. Зона. В Зоне действуют свои правила касательно того, что можно делать, а что делать нельзя. В Зоне можно инициализировать локальные данные DLL, можно создавать критические секции. В Зоне нельзя динамически загружать другие Модули или создавать потоки. Любой Сталкер знает и следует правилам Зоны. Все остальные рано или поздно нарушают правила и расплачиваются за это.
Что делает Зону особенной? Иные утверждают, что во всем виноват Загрузчик. Загрузчик единственный, кто понимает язык зависимостей между модулями. Он говорит с модулями, загружает их и вызывает из точки входа. Но Загрузчик слаб. Он не в силах совладать с Модулями, нарушающими правила Зоны.
Модули коварны и злы. Они стремятся окружить себя другими Модулями, от которых они зависят. Они любят создавать циклические зависимости между собой. Они загружают другие DLL в ответ на DLL_PROCESS_ATTACH и вызывают функции из непроинициализированных Модулей. Модули пытаются замаскировать и приумножить свое коварство. Они прикрываются статусом “Delay-Loaded DLL” и расставляют ловушки в конструкторах и деструкторах статических объектов.
Это люди сделали их такими. Люди нарушили правила Зоны. И теперь они расплачиваются за это каждый раз, когда идут в Зону."

Экспрессия экспрессией, но проблема действительно существует. И ссылочка http://msdn2.microsoft.com/en-us/library/ms682583.aspx для внимательного чтения просто обязательна.

Причем на "кухонном уровне" наличие гарантированных проблем просто очевидно.

Посыл N1 - должен быть какой-то код (Загрузчик), который в runtime обеспечит загрузку модулей в адресное пространство процесса, настройку ссылок (если это требуется), начальную инициализацию загруженного модуля.

Посыл N2 - в многопоточной среде этот код не может не иметь средств межпоточной синхронизации, вероятнее всего какой-нибудь разновидности giant lock'a (будем реалистами).

Посыл N3 - Загрузчик вынужден отдавать управление внешнему коду (это наш "DllMain") в контексте пользовательского потока. Очевидно, что средства синхронизации при этом остаются захваченными.

C точки зрения Coffman conditions - классическая ситуация для возникновения deadlock'a.

Так что пользоваться чем-либо, кроме функций C-Runtime и маленького набоа WIN32 API внутри DllMain крайне рискованно.
Это кстати, дополнительный повод не создавать сложные глобальные объекты, особенно неизвестного внутреннего устройства (например, такой класс из третьесторонней библиотеки в конструкторе может породить поток или загрузить динамически компонуемую библиотеку).

Кстати, почему пользоваться C-Runtime безопасно?
Если посмотреть на реальную точку входа в созданную DLL, то мы увидим что-то типа _DllMainCRTStartup@12, если не занимались играми с ключами компилятора.
Это собственно и есть точка инициализации runtime - наша функция DllMain будет вызвана уже из нее.

Желающим полной свободы тяжелее - указав свою точку входа, необходимо будет в момент вызова функции с параметром DLL_PROCESS_ATTACH передать управление в CRT_INIT(), иначе нас ждут неприятности.
При выгрузке ситуация аналогичная.

Комментариев нет: