понедельник, 5 января 2009 г.

Ещё причины, почему не надо делать ничего страшного в DllMain: случайная блокировка

Это перевод Another reason not to do anything scary in your DllMain: Inadvertent deadlock. Автор: Реймонд Чен.

Ваша функция 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.

4 комментария:

  1. Видимо, я плохо владею теорией.
    Александр, я правильно понял, что безымянный блок begin/end, который обычно бывает в коде библиотеки - это и есть, образно говоря, процедура DllMain? В чем, вообще, разница между DllMain и DllProc, и в какой последовательности они выполняются?

    ОтветитьУдалить
  2. Блок 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.

    ОтветитьУдалить
  3. С прикладной точки зрения удобно блок begin/end .dpr файла считать секцией initialization "модуля" .dpr файла.

    ОтветитьУдалить
    Ответы
    1. Спасибо за подробное объяснение. Не буду больше путать сишную терминологию с делфийной)

      Удалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.