Но обо всем по-порядку...
Действующие лица
- Многопоточное приложение (написанное на 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 не вернет управление, пока не освободит память. А сделать она этого не сможет, потому что в цикле пытается захватить блокировку, которую уже не кому освободить.
- Поток заблокировал самого себя.

Капец. И у меня такое же вылезло, и юзер жалуется ((( а как пофиксить - непонятно, потому что комп не дает.
ОтветитьУдалить