четверг, 7 октября 2010 г.

Анализ дампов Delphi-приложений. Hang (Episode #1)

   Преамбула
   Прибегает значит на днях сотрудник тестлаба с криками "Шеф, все пропало, все пропало! Гипс снимают, клиент уезжает!" (с). Тьфу ты - "Все зависло. Ничего не работает".


   Во время первичного осмотра на месте происшествия картина была следующая: курсор мыши при наведении на главную форму приложения принимает вид "стрелка с кружочком" (дело было на Висте. На ХР это бы выглядело как "стрелка с песочными часами"). И при этом форма не была "зависшей" - контролы кликабельны, сама форма таскабельна и т.п.

   Чтобы покурить такую ситуацию в спокойной обстановке и не отвлекать доблестный тестлаб от решения проблем насущных (точнее - от поиска таковых :)) был снят дамп с "зависшего" процесса [1].

   Главный поток не завис (на что, впрочем, указывала работоспособность главной формы на месте происшествия):
ntdll.dll!KiFastSystemCallRet
USER32.dll!NtUserWaitMessage+0xc
vcl100.bpl!FormsTApplicationHandleMessage$qqrv+0x1c
agent.exe!agent.agent+0x172
kernel32.dll!BaseProcessStart+0x23

   Анализ стеков остальных потоков тоже не выявил аномалий. А может это и не зависание вовсе (мелькнула мысль)? Может это просто кто-то установил курсор мыши и забыл вернуть в нормальное состояние? Ответить на этот вопрос можно. Для этого надо определить, чему равно свойство Cursor глобального объекта Screen (именно оно определяет, как выглядит курсор мыши при наведении на окна приложения). Для этого есть все, что нужно: дамп, немного времени и кофе :)

   Итак, приступим.

   Для начало необходимо найти ссылку на объект Screen. Как известно, Screen - глобальный объект (объявлен в интерфейсной части модуля Forms). А т.к. приложение использует пакеты, то искать ссылку на него проще среди символов VCL.


   При наличии отсутствия полноценных символов VCL, для него используются export symbols. Что нас вполне устраивает, т.к доступ ко всему, что объявлено в интерфейсных частях модулей пакета, осуществляется через таблицу экспорта.

   [Лирическое отступление]
   Эх, как бы сильно упростилась бы жизнь, имея возможность создавать private symbols... Ну да ладно - попробуем сварить кашу из того, что имеем.


   Ищем "Screen" в символах VCL (часть вывода опущена):
0:000> x vcl100!*screen*
2013a758 vcl100!ControlsTControlClientToScreen$qqrrx12TypesTPoint (<no parameter info>)
2013a808 vcl100!ControlsTControlScreenToClient$qqrrx12TypesTPoint (<no parameter info>)
20155f48 vcl100!FormsTScreen (<no parameter info>)
20155f98 vcl100!$xp$13FormsTScreen (<no parameter info>)
2015f6bc vcl100!FormsTScreen$bctr$qqrp18ClassesTComponent (<no parameter info>)
...
201b02a0 vcl100!FormsTScreenGetPrimaryMonitor$qqrv (<no parameter info>)
201f01b0 vcl100!FormsScreen (<no parameter info>)

   vcl100!FormsScreen очень сильно похоже на то, что мы ищем. Проверим эту гипотезу (учитываем, что символ должен указывать на адрес переменной, которая хранит ссылку на объект Screen):
0:000> !object poi(vcl100!FormsScreen)
02056690 "TScreen"


   !object - это команда из extension-а dext.dll для WinDbg [2]. Она проверяет, является ли указанный адрес ссылкой на объект.


   Ура, товарищи, - интуиция нас не подвела! :)
   Теперь остается определить значение поля FCursor найденного объекта TScreen (а для этого необходимо определить смещение этого поля относительно адреса объекта).

   Установка нового значения свойства Cursor осуществляется через метод SetCursor класса TScreen:
procedure TScreen.SetCursor(Value: TCursor);
var
  P: TPoint;
  Handle: HWND;
  Code: Longint;
begin
  if Value <> Cursor then
  begin
    FCursor := Value;
    if Value = crDefault then
    begin
      { Reset the cursor to the default by sending a WM_SETCURSOR to the
        window under the cursor }
      GetCursorPos(P);
      Handle := WindowFromPoint(P);
      if (Handle <> 0) and
        (GetWindowThreadProcessId(Handle, nil) = GetCurrentThreadId) then
      begin
        Code := SendMessage(Handle, WM_NCHITTEST, 0, LongInt(PointToSmallPoint(P)));
        SendMessage(Handle, WM_SETCURSOR, WPARAM(Handle), MakeLong(Code, WM_MOUSEMOVE));
        Exit;
      end;
    end;
    Windows.SetCursor(Cursors[Value]);
  end;
  Inc(FCursorCount);
end;

   Как видно, в начале производится сравнение нового значение со старым и, если они не равны, сохранение нового значение в поле FCursor.

   Теперь найдем адрес метода TScreen.SetCursor:
0:000> x vcl100!*SetCursor*
20113ab8 vcl100!ExtctrlsTHeaderWMSetCursor$qqrr21MessagesTWMSetCursor
2013af4c vcl100!ControlsTControlSetCursor$qqr16ControlsTCursor
201415e0 vcl100!ControlsTWinControlWMSetCursor$qqrr21MessagesTWMSetCursor
201487bc vcl100!ControlsTMouseSetCursorPos$qqrrx12TypesTPoint
2015fe9c vcl100!FormsTScreenSetCursor$qqr16ControlsTCursor
2015ff38 vcl100!FormsTScreenSetCursors$qqriui
20192ca0 vcl100!ComctrlsTCustomRichEditWMSetCursor$qqrr21MessagesTWMSetCursor
201a457c vcl100!ComctrlsTCoolBarWMSetCursor$qqrr21MessagesTWMSetCursor
201a9a0c vcl100!MaskTCustomMaskEditSetCursor$qqri
201b2fb0 vcl100!GridsTCustomGridWMSetCursor$qqrr21MessagesTWMSetCursor
201b5f90 vcl100!GridsTInplaceEditListWMSetCursor$qqrr21MessagesTWMSetCursor

   Дизассемблируем начало и сравниваем с Delphi-кодом:
0:000> u 2015fe9c
vcl100!FormsTScreenSetCursor$qqr16ControlsTCursor:
; Параметры
; eax - Self
; edx - Value: TCursor
2015fe9c 53              push    ebx
2015fe9d 56              push    esi
2015fe9e 57              push    edi
2015fe9f 55              push    ebp
2015fea0 83c4f4          add     esp,0FFFFFFF4h
2015fea3 8bf2            mov     esi,edx                 ; Value -> esi
2015fea5 8bd8            mov     ebx,eax                 ; Self -> ebx
2015fea7 663b7344        cmp     si,word ptr [ebx+44h]   ; if Value <> Cursor then
...

   Таким образом, видно, что поле FCursor занимает в памяти 2 байта и располагается по смещению 0x44 относительно адреса объекта. Теперь можно посмотреть, чему оно равно:
0:000> dw poi(vcl100!FormsScreen)+0x44 l1
020566d4  ffed

   А т.к. тип TCursor в модуле Controls объявлет так:
type
  TCursor = -32768..32767;
...

   То 0xffed = TCursor(-19) = crAppStart. И "зависание" на самом деле оказалось вовсе не зависанием.

   Ну а дальше все просто - поиск по исходникам, кто и где в использует такой код:
Screen.Cursor := crAppStart


Ссылки
[1] Как создать дамп процесса
[2] Delphi WinDbg Extension Dll

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

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