Но обо всем по-порядку...
Действующие лица
- Многопоточное приложение (написанное на Delphi 2007)
- Exception hooking routines из Project JEDI Code Library (JCL)
- Ошибка, возникающая в условиях многопоточности
Изначальная диспозиция: имеем, в определенных условиях, зависание приложения (действующее лицо №1) при его закрытии. Это не есть хорошо, поэтому снимаем с него дамп и начинаем его
Вырисовывается следующая картина:
- Главный поток (порядковый номер в дампе #0) ждет завершения потока #61 с обработкой сообщений (MsgWaitForMultipleObjects)
- Поток #61 заблокирован при попытке захватить критическую секцию, которой владеет #66
- Поток #66 застрял в каком-то цикле, где периодически вызывается Sleep
Начнем детальный разбор полетов. Главный поток оставим в покое, т.к. он честно пытается дождаться корректного завершения другого потока.
Посмотрим повнимательнее на #61 - а в нем произошло исключение (AV) и системный обработчик передал управление процедуре HookedExceptObjProc (из модуля JclHookExcept). Та, в свою очередь, вызвала DoExceptNotify.
Логика работы DoExceptNotify простая - оповестить все заинтересованные стороны (заявившие о своей заинтересованности путем вызова JclAddExceptNotifier, который добавляет обратную процедуру вызова в список Notifiers: TThreadList) о возникшем исключении. Для этого сначала захватывается критическая секция, защищающая доступ к списку Notifiers в условиях многопоточности. А после захвата по очереди вызывается калбеки из списке Notifiers.
Поток #61 как раз заблокирован при попытке захвата критической секции, защищающей список Notifiers:
ntdll.dll!RtlEnterCriticalSection
rtl100.bpl!Classes.TThreadList.LockList
applicatiion.exe!JclHookExcept.DoExceptNotify
applicatiion.exe!JclHookExcept.HookedExceptObjProc
rtl100.bpl!System.HandleAnyException
ntdll.dll!ExecuteHandler
ntdll.dll!KiUserExceptionDispatcher
...
К слову сказать, приложению перед зависанием видать очень сильно "поплохело" - возникло сразу несколько исключений Access violation в разных потоках. Кроме #61 еще один поток был заблокирован при попытке захватить блокировку списка Notifiers.
Теперь переходим ко второй части марлезонского балета - изучению потока #66. По стеку видно, что он пытается захватить спин-блокировку делфевой кучи для того, чтобы освободить память:
kernel32.dll!Sleep(AdditionalSleepTime)
rtl100.bpl!System.SysFreeMem
rtl100.bpl!System.FreeMem
...
Дефолтный менеджер Delphi в многопоточном приложении (IsMultiThread=True) защищает свою кучу с помощью спин-блокировок (LockCmpxchg, Sleep).
Итак, выяснилось, что поток #66 ждет освобождения кучи, чтобы освободить память. Теперь для окончательного прояснения ситуации остается определить, кто и при каких обстоятельствах заблокировал кучу?
Более детальный анализ стека #66 показал, что в нем произошло два исключения: сначала возникло одно, а при его обработке - второе. Рассмотрим их подробнее.
Ниже приведен стек потока в момент возникновения второго исключения
rtl100.bpl!System.SysGetMem
applicatiion.exe!JclFileUtils.TJclFileVersionInfo.Create
applicatiion.exe!uExceptionLog.SaveException
applicatiion.exe!JclHookExcept.TNotifierItem.DoNotify
applicatiion.exe!JclHookExcept.DoExceptNotify
applicatiion.exe!JclHookExcept.HookedExceptObjProc
rtl100.bpl!System.HandleAnyException
ntdll.dll!ExecuteHandler
ntdll.dll!KiUserExceptionDispatcher
...
Как видим, при возникновении первого исключения системный обработчик передал управление процедуре HookedExceptObjProc. Захватив блокировку списка Notifiers, был вызван калбэк одного из зарегистрированных обработчиков (TNotifierItem.DoNotify -> uExceptionLog.SaveException). Также видно, что исключение возникло в конструкторе класса TJclFileVersionInfo, а именно - на выделении памяти под новый экземпляр.
Посмотрим на код SaveException из модуля uExceptionLog (не полный, а только ключевые моменты). Ее цель - сохранить информацию о возникшем исключении в лог-файл.
procedure SaveException(EMessage: AnsiString; const SystemInfo: TExcDialogSystemInfos); var list, buffer: TStringList; j: Integer; ModuleName: TFileName; NtHeaders: PImageNtHeaders; ModuleBase: Cardinal; ImageBase DWORD; verBinary, verFile: AnsiString; begin list := TStringList.Create; buffer := TStringList.Create; try ... if (siModuleList in SystemInfo) and LoadedModulesList(list, GetCurrentProcessId) then begin buffer.Add(''); list.CustomSort(SortModulesList); for j := 0 to list.Count - 1 do begin ModuleName := list[j]; ModuleBase := Cardinal(list.Objects[j]); NtHeaders := PeMapImgNtHeaders(Pointer(ModuleBase)); if (NtHeaders <> nil) and (NtHeaders^.OptionalHeader.ImageBase <> ModuleBase) then ImageBase := NtHeaders^.OptionalHeader.ImageBase else ImageBase := 0; if VersionResourceAvailable(ModuleName) then with TJclFileVersionInfo.Create(ModuleName) do try verBinary := BinFileVersion; verFile := FileVersion; finally Free; end else begin verBinary := ''; verFile := ''; end; // // Format ModuleBase, ImageBase, ModuleName, verBinary, verFile // buffer.Add('...'); end; buffer.Add(' '); end; ... finally list.Free; buffer.Free; end; end;
И на "окончательный" стек зависшего потока:
kernel32.dll!Sleep(AdditionalSleepTime) rtl100.bpl!System.SysFreeMem rtl100.bpl!System.FreeMem rtl100.bpl!System.LStrClr rtl100.bpl!System.FinalizeArray rtl100.bpl!System.FinalizeRecord rtl100.bpl!System.FinalizeArray rtl100.bpl!Classes.TStringList.Destroy rtl100.bpl!System.TObject.Free ntdll.dll!ExecuteHandler2 ntdll.dll!ExecuteHandler rtl100.bpl!System.HandleFinally applicatiion.exe!uExceptionLog.SaveException applicatiion.exe!JclHookExcept.TNotifierItem.DoNotify applicatiion.exe!JclHookExcept.DoExceptNotify applicatiion.exe!JclHookExcept.HookedExceptObjProc rtl100.bpl!System.HandleAnyException ntdll.dll!ExecuteHandler ntdll.dll!KiUserExceptionDispatcher ...
Подведем итоги. Очевидно хронология событий была такой:
- В работающем потоке возникает исключение Access violation.
- Управление получает процедура DoExceptNotify, которая, захватив блокировку списка Notifiers, по очереди вызывает зарегистрированные обработчики. Т.о. образом управление попадает в SaveException.
- В самом начале SaveException создается 2 объекта TStringList. После чего начинается наполение их смыслом (различного рода информацией).
- В какой-то момент, при выделении памяти во время создания экземпляра объекта TJclFileVersionInfo возникает еще одно исключение типа Access violation (по-видимому все оказалось очень плохо). Но т.к. исключительная ситуация возникла где-то в недрах SysGetMem, то очевидно, какая-то из спин-блокировок делфевой кучи (на самом деле их несколько) оказалась захваченной вследствие того, что выход из SysGetMem был досрочным и флаг блокировки не был сброшен.
- Во время обработки нового (второго по счету для данного потока) исключения, при глобальной раскрутке стека, система вызывает обработчик finally в SaveException, в котором вызывается деструктор TStringList (см. п.3).
... finally list.Free; buffer.Free; end; ...
- Но вследствие того, что флаг блокировки кучи не сброшен (п.4), поток не выйдет из деструктора - SysFreeMem не вернет управление, пока не освободит память. А сделать она этого не сможет, потому что в цикле пытается захватить блокировку, которую уже не кому освободить.
- Поток заблокировал самого себя.
Капец. И у меня такое же вылезло, и юзер жалуется ((( а как пофиксить - непонятно, потому что комп не дает.
ОтветитьУдалить