Ваша функция DllMain работает внутри блокировки загрузчика - это один из немногих моментов, когда ОС позволяет вашему коду выполняться в момент удерживания внутренней блокировки. Это означает, что ваш код должен быть очень внимателен, чтобы не нарушить иерархию блокировок в своей DllMain; иначе вам грозит мёртвая блокировка.
(У вас ведь есть такая иерархия, да?).
Блокировка загрузчика ОС требуется для любой функции, которой нужен доступ к списку загруженных в процесс DLL. Это включает в себя такие функции как GetModuleHandle и GetModuleFileName. Если ваша DllMain входит в критическую секцию или ожидает на объекте синхронизации, а эти критическая секция или объект синхронизации принадлежат (owned) какому-то другому потоку, который, в свою очередь, ждёт освобождения блокировки загрузчика, то вы только что создали мёртвую блокировку (deadlock):
// Глобальная переменная var csGlobal: TCriticalSection; // Где-то есть такой код csGlobal.Enter; ... GetModuleFileName(HInstance, ...); csGlobal.Leave; // Эта процедура присваивается DllProc procedure MyDllMain(dwReason: DWord); begin case dwReason of ... DLL_THREAD_DETACH: begin csGlobal.Enter; ... end; ... end; end;Теперь представьте, что какой-то поток счастливо выполняет первый блок кода и входит в csGlobal, потом управление передайтся ещё кому-то. В это время другой поток завершает свою работу. При этом берётся блокировка загрузчика и рассылается сообщение DLL_THREAD_DETACH (блокировка загрузчика в это время держится).
Вы получаете DLL_THREAD_DETACH и пытаетесь войти в csGlobal. Это блокируется первым потоком, который сейчас владеет критической секцией. Потом этот поток продолжает выполнение и вызывает GetModuleFileName. Эта функция требует блокировки загрузчика (поскольку ей нужен доступ к списку DLL, загруженных в процесс), поэтому поток блокируется, потому что блокировкой загрузчика владеет кто-то ещё.
Теперь у вас deadlock:
- csGlobal-ом владеет первый поток, который ждёт блокировки загрузчика.
- Блокировкой загрузчика владеет второй поток, который ждёт csGlobal.
Мораль истории: не забывайте про блокировку загрузчика. Включайте её в свои иерархии блокировок, если вы хотите использовать любые блокировки в своей DllMain.
Видимо, я плохо владею теорией.
ОтветитьУдалитьАлександр, я правильно понял, что безымянный блок begin/end, который обычно бывает в коде библиотеки - это и есть, образно говоря, процедура DllMain? В чем, вообще, разница между DllMain и DllProc, и в какой последовательности они выполняются?
Блок begin/end в .dpr файле DLL - это достаточно хитрая магия компилятора. Это часть "DllMain", но только для DLL_PROCESS_ATTACH.
ОтветитьУдалитьDllEntryPoint - это понятие API Windows. По сути, это точка входа в модуль (IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint). Для exe это указатель на первую инструкцию, которая будет выполняться. DllEntryPoint не экспортируется и не имеет символьного имени. "DllEntryPoint" - это просто набор букв для обозначения концепции. Для DLL это указатель на функцию, которая будет служить "как DllMain". В языках программирования высокого уровня в качестве точки входа (включая и случай с DllMain) всегда используется код библиотеки поддержки языка (RTL). В частности, в Delphi для программ это будет _InitExe, а для DLL - _InitLib. В C++ это будет _DllMainCRTStartup (для DLL).
DllMain - это понятие RTL C++. Это функция, которая вызывается из DllEntryPoint (_DllMainCRTStartup), но ещё не написана - её обязан написать программист. Функция обязана иметь это же имя, иначе её не найдёт компоновщик.
DllProc - это понятие RTL Delphi. Это обычный указатель на функцию (callback, событие). Он вызывается из DllEntryPoint (_InitLib). Сам указатель называется DllProc, но функция, на которую он указывает, может называться как угодно - DllProc, DllMain, DllEntryPoint, MySuperDuperHandler, ... По умолчанию (и в 99% случаев) DllProc не заполняется и = nil.
Что тут запутывает, DllMain не существует в Delphi, это понятие RTL C++. DllMain описывается в MSDN, потому что MSDN говорит про C++. По сути, все "по привычке" копируют это название, хотя в контексте Delphi правильнее говорить только о DllEntryPoint и DllProc.
В любом случае, цепочка вызовов при загрузке/выгрузке DLL Delphi идёт так:
- LoadLibrary -> Kernel32/NTDLL (где-то внутри там идёт захват критической секции системного загрузчика) -> _InitLib (a.k.a. DllEntryPoint) -> InitializeModule (только для DLL_PROCESS_ATTACH) -> _StartLib -> DllProc (если есть) -> InitUnits -> секции initialization модулей -> возврат к begin/end .dpr -> выход из _InitLib.
- FreeLibrary -> Kerenel32/NTDLL (где-то внутри там идёт захват критической секции системного загрузчика) -> _InitLib (a.k.a. DllEntryPoint) -> _StartLib -> DllProc (если есть) -> _Halt0 -> FinalizeUnits -> отработка секций finalization модулей -> выход из _InitLib.
С прикладной точки зрения удобно блок begin/end .dpr файла считать секцией initialization "модуля" .dpr файла.
ОтветитьУдалитьСпасибо за подробное объяснение. Не буду больше путать сишную терминологию с делфийной)
Удалить