понедельник, 12 апреля 2010 г.

Deadlock при вызове CoRegisterClassObject в DllMain

   В процессе разработки одного плагина для браузера Chrome было замечено, вызов CoRegisterClassObject (ole32.dll) в точке входа динамической библиотеки (DLLMain) в Microsoft Windows Vista/7 приводит к зависанию потока.

   Эксперимент показал, что эта проблема может возникнуть не только с хромом.


   Причина кроется в том, что LoadLibrary(Ex) для динамической библиотеки, в DLLMain которой (или любой dll, загружаемой по зависимостям) есть вызов CoRegisterClassObject в операционных системах Microsoft Windows Vista/7, может привести к взаимной блокировке.

   Создадим тестовое приложение, содержащее такой код тестовое приложение, содержащее такой код (Delphi)
procedure TForm1.Button1Click(Sender: TObject);
begin
  CoInitialize(nil);  // Чтобы SomeProcedure не генерировала исключение
  LoadLibraryW("somelibrary.dll");
end;

   В секции initialization одного из модулей somelibrary.dll есть вызов TComObjectFactory.RegisterClassObject
unit FactoryUnit;
...
type
  TSomeFactory = class(TComObjectFactory)
    ...
  end;
...
procedure SomeProcedure();
var
  fct: TSomeFactory;
begin
  ...
  fct := TSomeFactory.Create(...);
  fct.RegisterClassObject;
  ...
end;

initialization
  SomeProcedure;
...
end.

   В операционной системе Microsoft Windows Vista/7 клик на кнопке Button1 привозит к зависанию главного потока тестового приложения. Самое интересное, что на Windows XP то же самое не приводит к зависанию ([1] – похожий случай).

   Как нетрудно догадаться, причина зависания - это взаимоблокировка с участием критической секции системного загрузчика (LoaderLock). Но почему же в XP тот же код не приводит к deadlock-у? Попробуем в этом разобраться.

   Microsoft давно рекомендует выполнять в DLLMain только простейшие рекомендации [6]. А с выходом ОС Windows Vista/7 эта проблема приобретает еще большую актуальность. Это связано с тем, что в новых ОС была произведена серьезная переделка "внутренностей" системы. Вследствие этого могут появится ошибки, которые не возникали в Windows XP. Что, собственно, и демонстрирует данный случай.


   Операции DLLMain
   Порядок загрузки файлов DLL при создании процессов не является гарантированным, и на него не следует полагаться при выполнении операций. Сложная обработка DllMain может вызвать зависание приложений или закрытие приложений с сообщением об ошибке, что связано с новыми зависимостями компонентов ОС.
...
("Статьи для разработчиков Windows Vista. Настольная книга по совместимости приложений", [3])


   Посмотрим, в чем же причина зависания потока в данном примере в Висте/Семерке. Беглый анализ показал, что "схема" зависания выглядит так:
  1.    Поток "А" (Project1.exe) захватывает критическую секцию загрузчика (ntdll!LdrpLoaderLock), создает (с помощью внутренней функции ThreadPool API) серверный фоновый поток RPC – поток "Б". Задача потока "Б" – ожидать подключения клиентов и устанавливать соединения с ними.
  2. После этого поток "А" делает синхронный вызов RPC (Remote Procedure Call). Т.к. при этом процессы взаимодействуют в одной системе, то используется разновидность RPC под названием локальный RPC. А в качестве сетевого API для локального RPC используется внутренний механизм – ALPC (Advanced Local Procedure Call).
    ntdll.dll!ZwAlpcSendWaitReceivePort
    rpcrt4.dll!LRPC_CASSOCIATION::AlpcSendWaitReceivePort
    rpcrt4.dll!LRPC_BASE_CCALL::DoSendReceive
    rpcrt4.dll!LRPC_BASE_CCALL::SendReceive
    rpcrt4.dll!I_RpcSendReceive
    rpcrt4.dll!NdrSendReceive
    rpcrt4.dll!NdrpSendReceive
    rpcrt4.dll!NdrClientCall2
    ole32.dll!ServerAllocateOXIDAndOIDs
    ole32.dll!CRpcResolver::ServerRegisterOXID
    ole32.dll!OXIDEntry::RegisterOXIDAndOIDs
    ole32.dll!OXIDEntry::AllocOIDs
    ole32.dll!CComApartment::CallTheResolver
    ole32.dll!CComApartment::InitRemoting
    ole32.dll!CComApartment::StartServer
    ole32.dll!InitChannelIfNecessary
    ole32.dll!MarshalInternalObjRef
    ole32.dll!CObjServer::CObjServer
    ole32.dll!GetOrCreateObjServer
    ole32.dll!CoRegisterClassObject
    rtl100.bpl!ComObj.TComObjectFactory.RegisterClassObject
    somelibrary.dll!FactoryUnit.SomeProcedure
    somelibrary.dll!FactoryUnit.FactoryUnit  // вызов initialization в FactoryUnit
    rtl100.bpl!System.InitUnits
    rtl100.bpl!System.StartLib
    ntdll.dll!LdrpCallInitRoutine
    ntdll.dll!LdrpRunInitializeRoutines
    ntdll.dll!LdrpLoadDll
    ntdll.dll!LdrLoadDll
    KERNELBASE.dll!LoadLibraryExW
    kernel32.dll!LoadLibraryW  // LoadLibraryW("somelibrary.dll")
    ...
  3.     Серверный поток "В" службы RPC (rpcss.dll в контексте svchost.exe) "берет" это сообщение, "обрабатывает" его, и шлет ответ. Отсылает он его с помощью ALPC. А т.к. у процесса Project1.exe этот механизм еще не "инициализирован", то предварительно выдается запрос на соединение.

    ntdll.dll!ZwAlpcConnectPort
    rpcrt4.dll!LRPC_CASSOCIATION::AlpcConnect
    rpcrt4.dll!LRPC_CASSOCIATION::Connect
    rpcrt4.dll!LRPC_BASE_BINDING_HANDLE::DriveStateForward
    rpcrt4.dll!LRPC_FAST_BINDING_HANDLE::Bind
    rpcrt4.dll!RpcBindingBind
    rpcss.dll!CFastBH::CreateFromBindingString
    rpcss.dll!CFastBH::GetOrCreate
    rpcss.dll!CProcess::CreateBindingHandle
    rpcss.dll!_ServerAllocateOXIDAndOIDs
    rpcrt4.dll!Invoke
    rpcrt4.dll!NdrStubCall2
    rpcrt4.dll!NdrServerCall2
    rpcrt4.dll!DispatchToStubInCNoAvrf
    rpcrt4.dll!RPC_INTERFACE::DispatchToStubWorker
    rpcrt4.dll!RPC_INTERFACE::DispatchToStub
    rpcrt4.dll!LRPC_SCALL::DispatchRequest
    rpcrt4.dll!LRPC_SCALL::QueueOrDispatchCall
    rpcrt4.dll!LRPC_SCALL::HandleRequest
    rpcrt4.dll!LRPC_SASSOCIATION::HandleRequest
    rpcrt4.dll!LRPC_ADDRESS::HandleRequest
    rpcrt4.dll!LRPC_ADDRESS::ProcessIO
    rpcrt4.dll!LrpcServerIoHandler
    rpcrt4.dll!LrpcIoComplete
    ntdll.dll!TppAlpcpExecuteCallback
    ntdll.dll!TppWorkerThread
    ...
  4.     Серверный поток RPC "Б" (Project1.exe), который должен обработать запрос на соединение ALPC от службы RPC (см. п.2), заблокирован на критической секции загрузчика (владеет которой поток "А") при попытке захватить ее для вызова DLLMain динамических библиотек с уведомлением DLL_THREAD_ATTACH. Т.о. "Б" образом не имеет ни единого шанса установить соединение для получения ALPC-ответа для потока "А".

    ntdll.dll!NtWaitForSingleObject
    ntdll.dll!RtlpWaitOnCriticalSection
    ntdll.dll!RtlEnterCriticalSection
    ntdll.dll!LdrpInitializeThread
    ntdll.dll!_LdrpInitialize
    ntdll.dll!LdrInitializeThunk

   Вот так как-то – немного хитро и запутанно.
   Теперь становится понятно, почему тестовое приложение без проблем работает в Windows XP: "появление" такой проблемы в Vista/7 – побочный результат эволюции LPC. Практика показывает, что проявляется эта проблема не всегда – по всей видимости, если у процесса уже запущен серверный фоновый поток RPC, то вызывающий поток не зависнет.



   ALPC (Advanced Local Procedure Call)
это механизм межпроцессной связи для высокоскоростной передачи сообщений. Он недоступен через Windows API напрямую и является внутренним механизмом, который используется только в компонентах ОС Windows.

   Примечание
   До Висты, ядро поддерживало механизм IPC, который назывался просто LPC (Local Procedure Call). Главная причина, которая привели к написанию ALPC – реализация User-Mode Driver Framework (UMDF), которому требовался высокоскоростной и масштабируемый механизм для взаимодействия между компонентами UMDF. LPC не подходит для этой цели, т.к. ему присущи ограничения в плане масштабируемости и возможные зависания в некоторых сценариях. Новый механизм IPC в Windows Vista/7 вытеснил LPC.



Ссылки
[1] CoRegisterClassObject in deadlock
[2] DllMain Callback Function
[3] Windows Vista. Риски, связанные с совместимостью
[4] Calls to an OLE Object Should Not Be Done from DllMain
[5] COM application hangs when you call CoCreateInstance from DllMain
[6] Best Practices for Creating DLLs

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

Отправить комментарий