DllMain и блокировка загрузчика — настоящая причина, почему говорят «ничего не делай в инициализации DLL»
· Го Комура · Windows, DLL, Разработка под Windows, C++, Устранение неполадок, Многопоточность, Win32 API
«Приложение зависает при старте, но только в конкретной среде.» «Когда загружаем свою DLL, LoadLibrary иногда так и не возвращается.» «Взаимно блокируется только в момент старта службы.» — Проследите такие расследования достаточно далеко — и чаще всего вы приходите в одно и то же место. Код инициализации DLL — то есть DllMain.
Документация Microsoft предупреждает о DllMain необычно жёстким тоном. Не вызывайте LoadLibrary. Не синхронизируйтесь с другими потоками. Не вызывайте функции User, Shell или COM. Идеальный DllMain — пустая заглушка — почему язык такой жёсткий? Причина сосредоточена в одном внутреннем механизме, блокировке загрузчика. Рассчитанная на разработчиков, которые пишут DLL, подключаемые модули и обёртки C++/CLI под Windows, эта статья объясняет по первичным источникам, как работает блокировка загрузчика, структуру, из-за которой взаимная блокировка держится, и безопасное проектирование и процедуру расследования.
1. Сначала вывод
DllMainвызывается, удерживая блокировку загрузчика — общую блокировку, которой в процессе ровно одна. Поэтому вызов изDllMainработы, которая пытается взять блокировку загрузчика (прямо или косвенно), создаёт возможность взаимной блокировки или падения от прикосновения к DLL, которая ещё не инициализирована.1- Вызов
LoadLibrary/FreeLibraryзапрещён. Он создаёт циклическую зависимость порядка загрузки и может заставить код инициализации выполниться против DLL, чья собственная инициализация ещё не выполнилась.2 - Синхронизация с другими потоками тоже запрещена. Уведомления DLL сериализованы, поэтому ожидание внутри
DllMainстарта или выхода потока оставляет сам этот поток остановленным в ожидании блокировки загрузчика — и вы взаимно блокируетесь.23 - Что можно безопасно вызывать — на практике только подмножество Kernel32.dll. И официальная документация прямо говорит, что «полного списка безопасных функций не существует». Функции User, Shell и COM загружают другие компоненты и вызывают нарушения доступа.2
- В DLL, слинкованной с CRT, те же ограничения действуют на конструкторы и деструкторы глобальных объектов. Они выполняются как фактическая часть
DllMain.2 - Правильный проект — «откладывать». Сделайте ту инициализацию, которую можете, во время компиляции (статически); то, что не можете, отложите до первого использования. Это официальная лучшая практика.1
- Смешанные DLL C++/CLI особенно опасны. Чтобы не выполнять MSIL под блокировкой загрузчика,
DllMainи его дерево вызовов должны быть скомпилированы нативно.4
2. Когда и как вызывается DllMain
DllMain — точка входа, которую загрузчик ОС вызывает, когда DLL входит в процесс или поток или покидает их. Есть четыре уведомления.
| Уведомление | Момент |
|---|---|
| DLL_PROCESS_ATTACH | Когда DLL загружается в процесс |
| DLL_THREAD_ATTACH | Когда в процессе запускается новый поток |
| DLL_THREAD_DETACH | Когда поток завершается нормально |
| DLL_PROCESS_DETACH | Когда DLL выгружается или когда процесс завершается |
Два факта легко пропустить. Первый: каждый раз, когда создаётся один поток, DllMain каждой уже загруженной DLL вызывается с DLL_THREAD_ATTACH. Иными словами, DllMain — не «то, что выполняется один раз, когда загружается моя DLL»; это код, который продолжают вызывать из-за потоковой активности процесса. Если вам это не нужно, можно остановить, вызвав DisableThreadLibraryCalls внутри DLL_PROCESS_ATTACH (не вызывайте из DLL, слинкованной со статическим CRT).5
Второй: в DLL, слинкованной с CRT (средой выполнения C/C++), конструкторы и деструкторы глобальных и статических объектов C++ выполняются через точку входа CRT как часть DllMain.2 Даже если вы думаете «наш DllMain пуст, значит мы в безопасности», глобальный объект с изощрённой инициализацией — то же самое, что выполнять эту работу в DllMain.
flowchart TB
accTitle: Четыре момента, в которые вызывается DllMain
accDescr: DLL_PROCESS_ATTACH выполняется при загрузке DLL; DLL_THREAD_ATTACH и DETACH выполняются на каждой уже загруженной DLL при каждом старте и выходе потока в процессе; DLL_PROCESS_DETACH выполняется при выгрузке или выходе процесса; и конструкторы статических объектов тоже выполняются внутри этого через CRT
load["Загрузка DLL"] --> pa["DLL_PROCESS_ATTACH"]
pa --> ta["DLL_THREAD_ATTACH (на каждый старт потока)"]
ta --> td["DLL_THREAD_DETACH (на каждый выход потока)"]
td --> pd["DLL_PROCESS_DETACH (при выгрузке или выходе)"]
pa -.-> crt["Конструирование статических объектов тоже идёт здесь"]
Рис. 1: DllMain вызывается не только в момент загрузки, но при каждом старте и выходе потока, и инициализация статических объектов тоже выполняется как часть этого.
3. Блокировка загрузчика — одна блокировка, которая сериализует каждое уведомление
Почему ограничения именно на DllMain так суровы? Ответ — в структуре загрузчика.
Чтобы серия операций — загрузка DLL, выгрузка и различные уведомления — оставалась согласованной, загрузчик ОС сериализует работу одной блокировкой загрузчика на процесс. И важный пункт в том, что DllMain вызывается, пока эта блокировка загрузчика удерживается.1 Пока вы внутри DllMain, каждая другая загрузка DLL в этом процессе и каждое уведомление о старте потока ждут освобождения этой блокировки.
Из этой структуры причины запретов следуют одна за другой.
- Нельзя вызывать
LoadLibrary, потому что это создаёт повторный вход в блокировку загрузчика или циклическую зависимость порядка загрузки. Это также может привести к вызову функции DLL, чья инициализация ещё не закончилась.2 - Синхронизация с другими потоками опасна, потому что у потока, которого вы ждёте, есть моменты, когда ему нужна блокировка загрузчика (уведомления при старте и выходе, вызовы API семейства
GetModuleHandleи так далее). Вы удерживаете блокировку загрузчика и ждёте другую сторону; другая сторона ждёт блокировку загрузчика — классическая инверсия порядка блокировок.6 - Функции User, Shell и COM опасны, потому что внутри они загружают другие системные компоненты. Вы касаетесь компонента до того, как он инициализирован, или после того, как его разобрали, и получаете нарушение доступа.2
sequenceDiagram
accTitle: Почему ожидание потока внутри DllMain взаимно блокируется
accDescr: DllMain, удерживая блокировку загрузчика, ждёт выхода рабочего потока, но рабочий, который пытается выйти, ждёт освобождения блокировки загрузчика, чтобы получить DLL_THREAD_DETACH, поэтому они ждут друг друга и взаимно блокируются
participant L as Загрузчик(держит блокировку)
participant D as DllMain
participant W as Рабочий
L->>D: DLL_PROCESS_DETACH
D->>W: Запросить выход и ждать
W->>W: Закончить работу, затем выйти
Note over W: Уведомление о выходе нужно блокировке
Note over D,W: DllMain держит блокировку, W ждёт
Рис. 2: «DllMain ждёт выхода потока» — структурная взаимная блокировка, потому что самому выходу потока нужна блокировка загрузчика.
Суть в том, что это не из тех вещей, которые «случаются, если не повезёт»; это структурно гарантированно держится. Документация говорит трактовать блокировку загрузчика как верхушку иерархии блокировок, которую определяет приложение (ту, что берут первой). Внутри DllMain вы уже держите эту блокировку верхнего уровня, поэтому любой акт пойти оттуда и ждать чего-то ещё опасен — так это удобно запомнить.6
flowchart TB
accTitle: Инверсия порядка между блокировкой загрузчика и частной блокировкой
accDescr: DllMain, удерживая блокировку загрузчика, идёт брать частную блокировку, а рабочий, удерживая эту частную блокировку, идёт брать блокировку загрузчика для GetModuleHandle или подобного, поэтому порядок захвата инвертируется и они взаимно блокируются
d["DllMain: держит блокировку загрузчика"] --> dg["Идёт взять частную блокировку G"]
w["Рабочий: держит частную блокировку G"] --> wl["Идёт взять блокировку загрузчика"]
dg -.-> dead["Взаимная блокировка от инвертированного порядка захвата"]
wl -.-> dead
wl -.-> api["GetModuleHandle и подобные требуют её внутри"]
Рис. 3: Даже безобидный API вроде GetModuleHandle внутри требует блокировку загрузчика, поэтому инверсия порядка с частной блокировкой может держаться.
Кроме того, сам вызов CreateThread изнутри DllMain не рекомендуется. Созданному потоку нужна блокировка загрузчика, чтобы обработать уведомление DLL_THREAD_ATTACH, поэтому он не может начать выполняться, пока выполняющийся сейчас DllMain не вернётся и не освободит блокировку. Поэтому ожидание внутри DllMain старта или завершения этого потока — немедленная взаимная блокировка. Есть ещё проблема времени жизни — если после возврата DllMain DLL выгружают, пока поток, который ещё не начал выполняться, всё ещё оставлен, стартовый адрес потока всё ещё указывает на уже освобождённый код, и вы падаете.3
4. Две мины, на которые легко наступают разработчики C++
Мина 1: динамическая инициализация глобальных объектов. Как сказано в главе 2, конструкторы статических объектов выполняются под ограничениями DllMain. Чтение файла конфигурации, постановка средства журналирования, инициализация COM, запуск потока — в тот момент, когда вы кладёте в DLL глобальный объект, чей конструктор делает такую работу, вы выполняете «вещи, которые нельзя делать в DllMain». Константная инициализация, которая фиксируется во время компиляции (всё, что можно сделать constexpr), безопасна; инициализацию, которая включает вызов функции, следует откладывать.
flowchart TB
accTitle: Путь, которым инициализация глобального объекта становится миной
accDescr: Блокировка загрузчика берётся при загрузке DLL, и конструкторы глобальных объектов выполняются через точку входа CRT, поэтому LoadLibrary, синхронизация потоков и инициализация COM внутри этих конструкторов — выполнения запретов DllMain
load["Загрузка DLL (блокировка загрузчика взята)"] --> crt["Точка входа CRT"]
crt --> ctor["Конструктор глобального объекта"]
ctor --> ng1["Работа, эквивалентная LoadLibrary"]
ctor --> ng2["Запуск потока и ожидание его завершения"]
ctor --> ng3["Использование COM или User32"]
ng1 -.-> risk["Всё это попадает под запреты DllMain"]
ng2 -.-> risk
ng3 -.-> risk
Рис. 4: Даже «DllMain пуст, значит мы в безопасности» возрождает ту же опасность в тот момент, когда появляется глобальный объект с изощрённой инициализацией.
Мина 2: C++/CLI (смешанные сборки). В конфигурации, которая оборачивает нативную DLL через C++/CLI (форма, разобранная в статье об обёртке), есть опасность выполнить MSIL (управляемый код) под блокировкой загрузчика. Выполнение MSIL может запустить инициализацию CLR или загрузку другой сборки. Компилятор выдаёт предупреждение C4747 на коде, где DllMain выполняет MSIL напрямую, но не может обнаружить косвенное выполнение через функцию в другом модуле. Компилируйте DllMain и функции, вызываемые из него, как нативные через #pragma unmanaged или используйте конфигурацию, в которой DllMain вообще нет.4
flowchart TB
accTitle: Можно ли обнаружить выполнение MSIL под блокировкой загрузчика
accDescr: Код, где DllMain выполняет MSIL напрямую, компилятор может обнаружить предупреждением C4747, но косвенное выполнение через функцию в другом модуле — нет, поэтому это приходится предотвращать ревью дерева вызовов и требованием нативной компиляции
d2["Вызовы из DllMain"] --> dir["Выполнить MSIL напрямую"]
d2 --> ind["Выполнить через другой модуль"]
dir --> c47["Обнаруживается предупреждением C4747"]
ind --> nc["Компилятор не может это обнаружить"]
nc -.-> rv["Предотвратить ревью и #pragma unmanaged"]
Рис. 5: C4747 защищает вас только от прямого выполнения. Косвенные пути можно поймать только ревью.
5. Правильный проект — сделать «откладывать» политикой по умолчанию
Официальная рекомендация лучшей практики ясна.1
- Закончите ту инициализацию, которую можете, во время компиляции (статически). Сначала спросите, можно ли динамическую инициализацию заменить статической.
- Остальное отложите до первого использования. Пока первое использование происходит из обычного API, который вызывают после того, как DLL закончила загружаться, инициализация выполняется вне блокировки загрузчика, и вы можете безопасно использовать почти весь Windows API. Для исключения при первом доступе можно использовать
INIT_ONCE(однократная инициализация) или магические статики C++ (статические переменные внутри функции). Откладывание, однако, не панацея — если сам этот первый доступ делается изDllMainили статического инициализатора, инициализатор всё равно выполняется под блокировкой загрузчика, и вы снова под теми же ограничениями. - Делайте исключение только для сбоев, которые нужно обнаружить рано. Может быть требование, чтобы сломанный файл конфигурации делал саму загрузку неудачной. Даже тогда держите это минимумом «попробовать и сразу упасть».
- Рассмотрите
DisableThreadLibraryCallsв DLL_PROCESS_ATTACH. Если DLL не использует уведомления о потоках, можно снять саму стоимость уведомлений (кроме случая статического CRT или статического TLS).5 - Проверяйте через Application Verifier. Многие опасные вызовы внутри
DllMain— те, которые Application Verifier обнаружит во время выполнения.1
flowchart TB
accTitle: Руководство по проектированию инициализации DLL
accDescr: Сначала рассмотрите, может ли инициализация быть статической во время компиляции; если нет, по умолчанию откладывайте до первого использования, а в DllMain оставляйте только минимум, который нужно рано обнаружить как сбой загрузки
q1{"Можно решить во время компиляции?"} -->|"да"| s["Сделать статической инициализацией"]
q1 -->|"нет"| q2{"Нужно обнаружить сбой в момент загрузки?"}
q2 -->|"нет"| lazy["Отложить до первого использования (по умолчанию)"]
q2 -->|"да"| min["В DllMain делать только минимум"]
lazy -.-> once["Исключать через INIT_ONCE или статическую переменную внутри функции"]
Рис. 6: Порядок решения — «можно ли статически → можно ли отложить», а в DllMain оставляете только минимум, который нужно обнаружить рано.
Применять ли DisableThreadLibraryCalls, можно решить механически следующим ветвлением.
flowchart TB
accTitle: Вызывать ли DisableThreadLibraryCalls
accDescr: Не вызывайте из DLL, слинкованной со статическим CRT; если действует статический TLS, сам вызов падает, поэтому не вызывайте; если ни то ни другое не применяется и DLL не использует уведомления о потоках, вызывайте в DLL_PROCESS_ATTACH, проверяя возвращаемое значение, чтобы срезать стоимость уведомлений
q1{"Слинкована со статическим CRT?"} -->|"да"| no2["Вызывать нельзя"]
q1 -->|"нет"| q2{"Используете статический TLS?"}
q2 -->|"да"| eff["Вызов всё равно падает (FALSE)"]
q2 -->|"нет"| q3{"Нужны уведомления о потоках?"}
q3 -->|"нет"| yes["Вызвать в ATTACH (проверить возвращаемое значение)"]
q3 -->|"да"| keep["Не вызывать; обрабатывать уведомления"]
Рис. 7: Три условия — статический CRT, статический TLS и нужны ли уведомления — однозначно решают, следует ли вызывать.
Для остановки потоков при выгрузке официальная документация даёт конкретный протокол. Вместо того чтобы «ждать» выхода рабочих потоков в DLL_PROCESS_DETACH (при выгрузке через FreeLibrary), форма такая: (1) просигналить выход событием, (2) сторона потока сворачивает работу до согласованного состояния, сигналит обратно и входит в бесконечное ожидание, (3) сторона DllMain подтверждает согласованное состояние и затем сворачивает поток через TerminateThread.3 Выглядит грубо, но это задокументировано как реалистичный ответ внутри ограничения «нельзя ждать естественного выхода потока внутри DllMain».
sequenceDiagram
accTitle: Протокол остановки потока при выгрузке
accDescr: DllMain сигналит рабочему потоку выйти событием; рабочий сворачивает работу до согласованного состояния, сигналит обратно и входит в бесконечное ожидание; DllMain подтверждает согласованное состояние и затем завершает поток
participant D as DllMain (обработка DETACH)
participant W as Рабочий поток
D->>W: Просигналить выход событием
W->>W: Свернуть работу до согласованного состояния
W->>D: Просигналить согласованность и ждать вечно
D->>W: Завершить через TerminateThread
Note over D,W: Нет ожидания естественного выхода, поэтому нет взаимной блокировки
Рис. 8: Вместо «ждать естественного выхода» «ждать сигнала согласованности и затем отрезать» избегает столкновения с блокировкой загрузчика.
Как дело первых принципов, самый безопасный проект — не владеть потоками в DLL, которую можно выгрузить, и держать владение потоками на стороне EXE.
DLL_PROCESS_DETACH при выходе процесса — наоборот: ничего не делать и вернуться — идеал. К этому моменту каждый другой поток уже принудительно завершён, и на состояние зависимых DLL или среды выполнения тоже нельзя полагаться. Изощрённая работа здесь только вызывает взаимные блокировки и падения. Данные, которые нужно сохранить, записывайте на собственном пути остановки приложения; не зависьте от этого уведомления.3
6. Как расследовать, когда вы в это попали
Зависания блокировки загрузчика имеют узнаваемый отпечаток.
Смотрите стеки в дампе зависания. Возьмите дамп замороженного момента и осмотрите стек каждого потока. Если найдёте пару «поток, ждущий блокировку внутри функций загрузчика ntdll.dll (семейство, чьи имена начинаются с Ldr)» и «поток, ждущий чего-то ещё внутри DllMain или статического инициализатора (dynamic initializer)», вы почти уверены. Поток, остановившийся посреди вызова LoadLibrary, — ещё один типичный персонаж.
flowchart TB
accTitle: Отпечаток зависания блокировки загрузчика
accDescr: В дампе зависания, если вы находите и поток, ждущий блокировку внутри функций загрузчика ntdll, и поток, ждущий чего-то ещё внутри DllMain или статического инициализатора, можно почти наверняка трактовать это как взаимную блокировку блокировки загрузчика
dump["Дамп зависания"] --> t1["Поток, ждущий блокировку в функциях семейства Ldr"]
dump --> t2["Поток, ждущий внутри DllMain или статического инициализатора"]
t1 --> pair{"Оба есть?"}
t2 --> pair
pair -->|"да"| conf["Почти наверняка взаимная блокировка блокировки загрузчика"]
pair -->|"нет"| other["Расследовать как зависание другого вида"]
Рис. 9: У зависаний блокировки загрузчика узнаваемый отпечаток «ожидание в Ldr + ожидание внутри DllMain».
Подозревайте характер «зависит от момента». Взаимная блокировка блокировки загрузчика держится только в момент, когда загрузка DLL совпадает со стартом или выходом потока. Условия воспроизведения вроде «иногда при старте», «только на конкретной машине» и «только когда запущено как служба» — признаки такого рода проблемы.
Запускайте профилактическую проверку. Включите Application Verifier и гоняйте тесты — и вы можете обнаружить опасные вызовы внутри DllMain во время выполнения.1 Для C++/CLI не игнорируйте предупреждение C4747; в ревью функций, достижимых из DllMain, добавьте угол «функции, которые вызывают LoadLibrary косвенно» (инициализация COM, некоторые возможности CRT, отложенные импорты и так далее) в контрольный список ревью — и вы поймаете аварии до поставки. Первый вызов отложенного импорта, становящийся внутри LoadLibrary, — точка, которую легко пропустить.
7. Итоги
DllMainвызывается, удерживая блокировку загрузчика (одна на процесс, блокировка, которая сериализует каждое уведомление DLL). Каждое ограничение следует из этого.- Ядро запретов — «не вызывайте
LoadLibrary/FreeLibrary», «не синхронизируйтесь с другими потоками» и «не вызывайте функции, которые зависят от DLL, отличной от Kernel32». Конструкторы и деструкторы статических объектов, которые выполняются через CRT, попадают под те же ограничения. - Базовая политика проектирования — откладывание. Сделайте статической ту инициализацию, которую можно сделать статической; остальное отложите до первого использования. Используйте
DisableThreadLibraryCallsи Application Verifier. - Остановка потоков при выгрузке следует официальному протоколу (сигнал → подтвердить согласованность → завершить). DLL_PROCESS_DETACH при выходе процесса в идеале пуст.
- В C++/CLI выполнение MSIL под блокировкой загрузчика — отдельная мина. Требуйте нативной компиляции дерева вызовов
DllMain.
Ограничения DllMain сначала выглядят как неразумный список запретов. Но как только вы удерживаете одну точку — «вызывается, удерживая блокировку загрузчика, блокировку верхнего уровня», — каждый запрет — пересказ того же принципа. Запомните это как принцип — и когда встретите крайний случай, которого нет в документации, вы всё равно сможете задать правильный вопрос: «Это работа, которую мне позволено делать, удерживая блокировку?»
Похожие статьи
- Как работает разрешение имён DLL в Windows — порядок поиска и SxS
- Вызов нативных DLL из C#: обёртка на C++/CLI или P/Invoke
- Практические рекомендации по многопоточности: издание C++ — устранение аварий структурой через RAII и jthread
- Ложные пробуждения — почему условные переменные просыпаются «без уведомления» и как правильно ждать в Windows
- Чтение крэш-дампов с WinDbg + SOS ── практическое введение в анализ после сбора
- Основы COM STA/MTA — модели потоков и как избежать зависаний
Смежные области консультирования
KomuraSoft LLC занимается расследованием первопричин зависаний и взаимных блокировок при старте или загрузке DLL (анализ дампа), ревью проекта вокруг DllMain и статической инициализации и доведением обёрток C++/CLI и подключаемых DLL до безопасного проекта инициализации. Можно консультироваться даже на стадии «зависает при старте только в конкретной среде», которую трудно воспроизвести.
- Исследование дефектов и анализ первопричин
- Техническая консультация и ревью проекта
- Разработка Windows-приложений
- Связаться с нами
Справочные ссылки
-
Microsoft Learn, Dynamic-Link Library Best Practices. О том, что DllMain вызывается, пока удерживается блокировка загрузчика, так что функции, которые можно вызывать, сильно ограничены; о том, что идеальный DllMain — пустая заглушка и инициализацию откладывают насколько возможно; о рекомендации статической инициализации во время компиляции; о том, что для сбоев, которые нужно обнаружить рано, делают только минимум; и об обнаружении типичных ошибок DllMain через Application Verifier. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, DllMain entry point. О выполнении в точке входа только простой инициализации и завершения; почему нельзя вызывать LoadLibrary / FreeLibrary (циклический порядок загрузки и использование DLL до инициализации или после завершения); о том, что Kernel32.dll гарантированно уже загружена, так что её можно вызывать в диапазоне, который не загружает другие DLL; о том, что исчерпывающего списка безопасных функций нет; о том, что функции User, Shell и COM вызывают нарушения доступа; о том, что уведомления DLL сериализованы, так что общение с другими потоками или процессами вызывает взаимные блокировки; и о том, что те же ограничения действуют на конструкторы и деструкторы статических объектов, когда слинкован CRT. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. О структуре, которая взаимно блокируется, если ждать выхода потока внутри DllMain (уведомлению DLL_THREAD_DETACH о выходе потока нужна блокировка загрузчика); о протоколе остановки потока при выгрузке (просигналить событием, подтвердить согласованное состояние, затем завершить); о том, что при DLL_PROCESS_DETACH на выходе процесса другие потоки уже принудительно завершены и нет гарантии согласованности адресного пространства, так что идеальный обработчик пуст; и о том, что создание потока в DllMain оставляет уведомления в очереди при незавершённой инициализации и вызывает проблемы. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Initialization of Mixed Assemblies. О том, чтобы не выполнять MSIL под блокировкой загрузчика; о том, чтобы не компилировать DllMain и его дерево вызовов в MSIL и решать это через #pragma unmanaged; о том, что предупреждение C4747 выдаётся, когда DllMain пытается выполнить MSIL напрямую, но косвенное выполнение через другой модуль не обнаруживается; и о том, что динамические инициализаторы статических объектов могут вызвать ту же проблему. ↩ ↩2
-
Microsoft Learn, DisableThreadLibraryCalls function (libloaderapi.h). Об отключении уведомлений DLL_THREAD_ATTACH / DLL_THREAD_DETACH, чтобы снизить накладные расходы при создании и уничтожении потока; о том, что не следует вызывать из DLL, слинкованной со статическим CRT; и о том, что оптимизация не выполняется, когда действует статический TLS (thread_local или __declspec(thread)). ↩ ↩2
-
Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion. Об определении иерархии блокировок и всегда захвате в одном порядке; о том, что загрузчик захватывает блокировку загрузчика перед вызовом DllMain, так что блокировка загрузчика должна сидеть на верхушке иерархии блокировок; о соблюдении порядка захвата между API, которые берут блокировку загрузчика косвенно, например GetModuleFileName, и частными блокировками; и о конкретном примере взаимной блокировки от инверсии порядка блокировок. ↩ ↩2
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Win32 Thread Pool API — параллелизм без создания потоков через CreateThreadpoolWork
По всему нативному коду разбросаны вызовы CreateThread? Статья разбирает Thread Pool API Win32, переработанный в Vista, — четыре объекта ...
Что на самом деле значит «Не отвечает» — как Windows решает, что приложение зависло, и как проектировать приложения, которые не зависают
«Не отвечает» в Windows — механизм, в котором ОС судит, что окно не извлекало сообщение 5 секунд, и подменяет его окном-призраком. Статья...
Ложные пробуждения — почему условные переменные просыпаются «без уведомления» и как правильно ждать в Windows
Ожидание условной переменной может вернуться, даже когда уведомление не пришло (ложное пробуждение). Статья объясняет по реализации Windo...
Именованные каналы на практике — стандартный IPC Windows: от проектирования до безопасности
Практическое руководство по именованным каналам — стандартному межпроцессному взаимодействию Windows. Статья по первичным источникам разб...
Приложения, которые ломаются после выхода из сна — как работают события питания Windows и как строить бизнес-приложения, которые это переживают
Открыли ноутбук — и соединения бизнес-приложения мертвы: причина в проекте, который не учитывал сон. Статья разбирает поток уведомлений W...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Бизнес-приложения, интеграция оборудования и средства связи — от требований до разработки.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Неужели в DllMain действительно нельзя делать вообще ничего?
- «Ничего не делать» — не гипербола; это официальная проектная позиция, и сама Microsoft говорит, что идеальный DllMain — почти пустая заглушка. Безопасно — подмножество функций Kernel32.dll: к моменту выполнения DllMain Kernel32 гарантированно загружен — в диапазоне, который не загружает другие DLL. Создание критической секции или мьютекса и использование TLS — примеры того, что можно. Наоборот, LoadLibrary/FreeLibrary, синхронизация с другими потоками и вызов функций User32, Shell, COM и подобных запрещены, потому что вызывают взаимные блокировки и нарушения доступа. Инициализацию, в которой вы не уверены, в DllMain делать не нужно; отложите её до первого использования.
- Попадают ли конструкторы глобальных объектов C++ (статических объектов) под те же ограничения DllMain?
- Попадают. Когда DLL слинкована с CRT (средой выполнения C++), конструкторы и деструкторы глобальных и статических объектов выполняются через точку входа, которую даёт CRT, как фактическая часть DllMain. Значит, вызов LoadLibrary из конструктора, запуск другого потока и ожидание его завершения, инициализация COM и тому подобное несут ту же опасность, что и те же действия в DllMain. Для глобального объекта с нетривиальной инициализацией держите указатель и конструируйте его при первом доступе либо используйте статическую переменную внутри функции, чтобы работа шла вне DllMain.
- Стоит ли вызывать DisableThreadLibraryCalls?
- Условно да. Если DLL не нужны уведомления DLL_THREAD_ATTACH/DETACH, вызов DisableThreadLibraryCalls в DLL_PROCESS_ATTACH останавливает уведомления на каждое создание и завершение потока и снижает накладные расходы в процессе, который часто создаёт потоки. Есть два исключения. Не вызывайте её из DLL, слинкованной со статическим CRT (статическому CRT нужны уведомления о потоках). И если действует статический TLS через thread_local или __declspec(thread), сам вызов падает и возвращает FALSE, поэтому возьмите привычку проверять возвращаемое значение. Используйте её на типичной DLL, которая использует динамически слинкованный CRT, после того как подтвердили, что от уведомлений о потоках ничего не зависит.
- Почему DLL на C++/CLI (смешанная управляемая) зависает при старте?
- Типичная причина — попытка выполнить MSIL (управляемый код), пока удерживается блокировка загрузчика. В смешанной сборке C++/CLI, если DllMain, функции, вызываемые из него, или динамические инициализаторы глобальных объектов скомпилированы в MSIL, под блокировкой загрузчика может потребоваться инициализация CLR или загрузка другой сборки, и это может взаимно заблокироваться. Компилятор выдаёт предупреждение C4747, когда сам DllMain пытается выполнить MSIL напрямую, но не может обнаружить косвенное выполнение через другой модуль. Смягчение — скомпилировать DllMain и его дерево вызовов как нативное через #pragma unmanaged — или вообще не иметь DllMain.
- Можно ли освобождать ресурсы в DLL_PROCESS_DETACH?
- Ответ меняется между «выходом процесса» и «выгрузкой через FreeLibrary». При DLL_PROCESS_DETACH на выходе процесса другие потоки уже завершены, и нет гарантии, что адресное пространство ещё согласовано, поэтому уборка вроде освобождения памяти на самом деле опасна; официальное указание — «идеальный обработчик пуст». Данные, которые нужно сохранить, записывайте на собственном пути остановки приложения, а здесь по сути ничего не делайте и возвращайтесь. При выгрузке через FreeLibrary процесс продолжается, поэтому полная уборка нужна — остановить потоки, закрыть дескрипторы и так далее. Ожидание выхода потока внутри DllMain, однако, взаимно блокируется, поэтому нужно следовать официальному протоколу: просигналить, дождаться согласованного состояния и закончить работу вне DllMain.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.