Здравствуйте, я ошибка 217 и я вам ничего не скажу

от автора

Вероятно многие встречались с таким вот «партизаном» при старте или завершении приложения:

Очень информативное сообщение, сразу понятна причина ошибки, место и способ ее решения 🙂
Впрочем, если без шуток, что это вообще такое?
Конечно-же это исключение, но ни тип исключения, ни его описание нам не доступны — просто «Runtime error 217» и адрес, а дальше сами…

Если честно, раньше я как-то даже не задумывался по поводу данного исключения, т.к. в моих проектах оно явление редкое, пока однажды у целой череды пользователей не начала воспроизводится именно 217-я ошибка.
Впрочем, даже тогда я не пошел по правильному пути и просто добавил дополнительный уровень логирования в проект, по результатам которого достаточно оперативно нашел причину и исправил ее.
Но, по сути, я просто потратил свое время…

И тратил бы его в дальнейшем, если бы на днях со мной не связался Виктор Федоренков и не рассказал о своих мыслях по поводу ошибки за номером 217.

Теория и анализ проблемы

Без теории нам никуда, иначе можем уткнуться в пределы собственных знаний 🙂
Поэтому начнем, конечно, с теоретической части.

Для начала я немного освежил мои представления об ошибках в принципе, перечитав часть статьи «Обработка ошибок — глава 1.2.2» за авторством Александра Алексеева, откуда вынес информацию о том, что ошибка 217 будет отображена в том случае, если не инициализирован модуль SysUtils, причем это у Александра проиллюстрированно достаточно наглядно:


Открыть картинку в полный размер…

На основании данной картинки можно сделать грубый вывод: пока SysUtils жив — все исключения должны отображаться в нормальном виде, о чем идет отдельное упоминание:

Например, если вы видите сообщение о runtime-ошибке, то, судя по приведённой схеме, маловероятно, чтобы ошибка возникла в обработчиках событий на форме. Зато гораздо вероятнее, что она возникает, скажем, в какой-то секции finalization (которая выполняется после секции finalization модуля SysUtils) или в назначенной процедуре ExitProcessProc. Но, разумеется, причина ошибки может сидеть где угодно — в том числе и в упоминаемых обработчиках событий.

Ну что-ж давайте проверим, пишем код, в котором SysUtils должна быть финализирована позже модуля Unit1, в котором искусственно генерируем исключение:

unit Unit1;   interface   uses   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,   Dialogs;   type   TForm1 = class(TForm)   private     { Private declarations }   public     { Public declarations }   end;   var   Form1: TForm1;   implementation   {$R *.dfm}   initialization   finalization     raise Exception.Create('finalization exception');   end. 

Билдим, запускаем, закрываем форму и… Runtime error 217.

Утверждение о том, что 217 отображается после финализации SysUtils полностью верное, но давайте-ка посмотрим на сам код финализации:

procedure FinalizeUnits; ... begin ...   Count := InitContext.InitCount;   Table := InitContext.InitTable^.UnitInfo; ...   try     while Count > 0 do     begin       Dec(Count);       InitContext.InitCount := Count;       P := Table^[Count].FInit;       if Assigned(P) then ...         TProc(P)(); ...       end;     end;   except     FinalizeUnits;  { try to finalize the others }     raise;   end; end; 

Смотрите что происходит: в процедуре FinalizeUnits вызываются все финализирующие процедуры, адреса которых расположены в массиве InitContext.InitTable^.UnitInfo в том порядке, в котором происходила их инициализация, т.е. самые первые расположены в начале массива (а финализация идет с конца).
Где-то в самом низу расположен и SysUtils + System, ну а мы, с нашим модулем Unit1 где-то в самом верху.
Но вдруг происходит исключение в нашем модуле и «бабах», порядок катарсиса нарушен.

После «бабах» FinalizeUnits вызывается повторно, пропуская наш модуль, вызвавший исключение, вследствие чего разрушается SysUtils и разные, встречающиеся по пути, class destructor-ы, до кучи грохается System с менеджером памяти (сидящий одним из первых в начале списка), после чего идет контрольный выстрел в лоб — RAISE, вот тут-то мы и приплыли — здравствуй 217.

А что если произойдет исключение в секции инициализации любого модуля?

Да все тоже самое:

procedure InitUnits; ... begin ...   try ...   except     FinalizeUnits;     raise;   end; end; 

Делаем вывод: любое необработанное исключение в секциях инициализации или финализации будет приводить к потере описания исключения и приводить к ошибке 217.

На этом с теорией, думаю, закончим.
Имея на руках понимание о причине возникновения Runtime error 217, попробуем получить на руки более привычный нам вариант сообщения об исключении.

Отключаем финализацию модулей

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

Его анализ заключался в следующем: общая инициализация обработчика исключений производится в процедуре InitExceptions модуля SysUtils, а финализация вызовом DoneExceptions.

Если каким либо образом отключить вызов DoneExceptions плюс не дать разрушиться менеджеру памяти, заблокировав вызов блока финализации System — на руки мы получим сообщение об исключении в приемлимом виде.

Как вариант решения был предложен следующий код, который нужно подключить к файлу проекта самым первым модулем (будет работать начиная с D2005 и выше):

unit suShowExceptionsInInitializeSections;   interface   uses   SysUtils;   implementation   uses   Windows;   //Получение структуры PackageInfo нашего приложения //В System она находится в переменной InitTable, но не видна из других модулей function GetInitTable: PackageInfo; var   Lib: PLibModule;   TypeInfo: PPackageTypeInfo; begin   Result := nil;     Lib := LibModuleList;     if not Assigned(Lib) then     Exit;     //Если загружено несколько модулей (BPL пакетов), то выходим,   //я не изучал как работает механизм загрузки/выгрузки BPL, поэтому на всякий   //случай выходим   if Assigned(Lib^.Next) then     Exit;     Typeinfo := Lib^.TypeInfo;   if Assigned(TypeInfo) then   begin     //Мы имеем TPackageTypeInfo     //Теперь по нему можно получить PackageInfo     //Воспользуемся особенностями компилятора.     //В IDA видно, что ссылка TypeInfo указывает на середину структуры     //PackageInfo программы     //Поэтому для того что бы вычислить PackageInfo нужно вычесть из адреса     //TypeInfo смещение этого поля     Result := PackageInfo(PByte(TypeInfo) - (LongWord(@PackageInfoTable(nil^).TypeInfo)));   end; end;   //Отключить секцию финализации для всех модулей procedure DisableAllFinalization; var   Loop: Integer;   OldProtect: LongWord;   InitTable: PackageInfo;   Table: PUnitEntryTable; begin   InitTable := GetInitTable;     if Assigned(InitTable) then   begin     Table := InitTable^.UnitInfo;     if Assigned(Table) then       //Разрешаем изменять структуру в которой хранятся ссылки на инициализаю/финализацию всех юнитов       if VirtualProtect(Table, SizeOf(PackageUnitEntry) * InitTable^.UnitCount,          PAGE_READWRITE, OldProtect) then         for Loop := 0 to InitTable^.UnitCount - 1 do           Table^[Loop].FInit := nil;   end; end;   initialization   finalization   //Сейчас идет финализация всех модулей, модуль SysUtils создан раньше, поэтому   //он еще не финализирован. Наша задача здесь не дать ему финализироваться,   //Как и другим модулям которые он использует (интересует только System),   //это нужно для правильной отработки обработчиков исключений.     //Сюда мы можем попасть по двум причинам   //1. Произошел Exception во время инициализации каком-то модуля   //2. Нормальное завершение программы   //   //Мы не будем определять причину, так как процесс все равно завершается, а ОС   //сама освободит занятые ресурсы после смерти процесса.   //Но нужно иметь ввиду, данную технику использовать в DLL нельзя, что бы не   //допускать утечек памяти   if IsLibrary then     Exit;     //Мы не можем выборочно заблокировать финализацию юнитов по их имени   //так как нет соответствующих данных в RTTI. Тем не менее, мы можем отключить   //финализацию всех юнитов, которые идут в списке до этого   //модуля. Таким образом если данный модуль расположить первым в DPR файле,   //то мы минимизируем утечки.   //Вычислять адрес процедуры финализации данного юнита не обязательно,   //ведь к моменту выполнения данного кода уже финализированы все следующие юниты.   //Поэтому просто заблокируем финализцию всех оставшихся   DisableAllFinalization; end. 

Если честно — аплодировал стоя.
Вот он: хак в самом грязном виде как он есть — такие вещи могут делать только те, кто действительно понимает, чем это грозит 🙂
И данный модуль вывел работу нашего IT отдела примерно на три часа — это была жесткая дискуссия 🙂

Но, впрочем, давайте разберем логику работы данного кода:
Суть его проста, необходимо выйти на данные о загруженных модулях (включая BPL) в том виде, в котором их понимает Delphi приложение. Это было сделано посредством доступа к началу однонаправленного списка структур TLibModule. Первым элементом списка будет структура, описывающая текущий образ, откуда нам нужно всего-то и получить данные о структуре UnitInfo, которая содержит в себе данные как о количестве инициализированных модулей, так и об адресах их процедур инициализации и финализации в виде записи PackageUnitEntry.

Блокирование финализации модулей происходит посредством присвоения параметру FInit значения nil у каждой записи PackageUnitEntry.

При обниливании данного параметра FinalizeUnits не сможет произвести вызов обработчика и в итоге тот самый raise, о котором я писал выше, сможет достаточно корректно произвести отображение возникшего исключения.


Но вот дальше все сложнее.

Пытаемся причесать хорошую мысль

Идея здравая и причины понятны, но вот как-же так, ресурсы все-же не освобождены, FastMem перестанет нормально работать (она собирает утечки как раз при финализации), да и совместимости маловато, к примеру, как я и сказал выше, под Delphi 7 данный код вообще работать не сможет.

После первого часа обсуждений в IT отделе мы даже умудрились прийти и к такому выводу: «да и хрен с ними с SysUtils и System — что-то критичного они за собой не несут».
А потом, опять начали спорить — ну не устраивал нас этот подход, вроде все хорошо, но не аккуратненько как-то 🙂

Рассматривались даже варианты прямого сплайсинга блоков финализации и до кучи деструктора Exception — но дополнительный хак, на уже существующий хак не устраивал вообще никого 🙂

И тут, сидя в отладчике и прогоняя код по 70-му разу пришла мысля.
Дык эта… а как вообще выводится сообщение о произошедшем исключении? 🙂

А выводится оно посредством передачи управления на ExceptHandler, в коде которого нет ничего секретного.
А что мы делаем убирая финализацию модулей?
Правильно, заставляем вызваться его-же.

Попробуем-ка проэмулировать вызов ExceptHandler.
Пишем тестовый юнит и подключаем его к проекту самым первым:

unit Test;   interface   uses   SysUtils;   var   E: Exception;   implementation initialization finalization   E := AcquireExceptionObject;   if E <> nil then   begin     ShowException(E, ExceptAddr);     E.Free;     Halt(1);   end; end. 

Запускаем на выполнение и…


Получилось.

Встроившись в цикл финализации, мы отобразили произошедшее исключение и продолжили финализацию дальше вызовом Halt(1).

В итоге задача решена, грамотно и документировано, и совместимо с Delphi 7, но…

А не развить ли идею?

Есть такое понятие, как «наведенные ошибки», т.е. ошибки произошедшие из-за того что перед ними тоже произошла ошибка.

Ну к примеру, функция А, которая должна возвращать экземпляр некоего класса и функция Б, использующая этот экземпляр в работе. К примеру в функции А произошло необработанное исключение (например нет доступа к файлу) и она не создала класс, а потом где-то гораздо позже по коду приложения процедура Б выполняет обращение к этому экземпляру и в итоге происходит Access Violation.

Тоже самое может произойти и в процедурах инициализации/финализации, причем исключение, произошедшее в финализации скроет от нас саму причину.

Для демонстрации напишем вот такой код, в котором при инициализации приложения будет создаваться некий логер, в который будут писаться этапы работы приложения, а в финализации будет писаться что приложение завершило работу.
Для генерации исключения заставим логер создаваться по несуществующему пути:

uses   Classes;   var   Logger: TFileStream;   const   StartLog: AnsiString = 'Начало работы приложения' + sLineBreak;   EndLog: AnsiString = 'Работа приложения завершена' + sLineBreak;   implementation   initialization     Logger := TFileStream.Create('A:\MyLog,txt', fmCreate);   Logger.WriteBuffer(StartLog[1], Length(StartLog));   finalization     Logger.WriteBuffer(EndLog[1], Length(EndLog));   Logger.Free;   end. 

Мало у кого в системе присутствует диск «А» поэтому результатом этого кода будет либо «Runtime error 216» (именно 216, а не 217), либо, если подключим код из предыдущей главы:
Exception EAccessViolation in module Project2.exe at 001B1593.
Access violation at address 005B1593 in module ‘Project2.exe’. Read of address 00000000.
А ведь причина то кроется в самом первом исключении, которое нами не отображается и с наскока разобраться в причине ошибки не получится.

Для того чтобы исправить эту несправедливость, можно немного причесать код и довести его до вот такого состояния:

unit ShowExceptSample;   interface   uses   SysUtils,   Classes;   implementation   type   PRaiseFrame = ^TRaiseFrame;   TRaiseFrame = packed record     NextRaise: PRaiseFrame;     ExceptAddr: Pointer;     ExceptObject: TObject;     ExceptionRecord: PExceptionRecord;   end;   var   // Указатель на вершину списка исключений   CurrentRaiseList: Pointer = nil;   // Функция возвращяет текущее исключение со стека function GetNextException: Pointer; begin   if CurrentRaiseList = nil then CurrentRaiseList := RaiseList;   if CurrentRaiseList <> nil then   begin     Result := PRaiseFrame(CurrentRaiseList)^.ExceptObject;     PRaiseFrame(CurrentRaiseList)^.ExceptObject := nil;     CurrentRaiseList := PRaiseFrame(CurrentRaiseList)^.NextRaise;   end   else     Result := nil; end;   var   ExceptionStack: TList;   E: Exception;     initialization   finalization     // Смотрим, есть ли вообще исключения?   E := GetNextException;     if E <> nil then   begin     ExceptionStack := TList.Create;     try         // если есть, собираем о них информацию       while E <> nil do       begin         ExceptionStack.Add(E);         E := GetNextException;       end;         // и отображаем их в том порядке, в котором они произошли       while ExceptionStack.Count > 0 do       begin         E := ExceptionStack[ExceptionStack.Count - 1];         ExceptionStack.Delete(ExceptionStack.Count - 1);         ShowException(E, ExceptAddr);         E.Free;       end;     finally       ExceptionStack.Free;     end;       // финализируем все что осталось     Halt(1);   end; end. 

Здесь идея проста, функция GetNextException по сути повторяет вызов AcquireExceptionObject, но после своего вызова не теряет ссылку на следующее в очереди исключение, а запоминает адрес следующего фрейма во внешней переменной.
После чего все исключения заносятся в список (самое последнее будет первым в списке) и выводятся программисту с соблюдением очередности, в результате чего нам будет сразу понятно, что сначала произошло вот это:

И уже только после него пошли всякие там AV.

Теперь по поводу остальных кодов ошибок.
Почему я начал именно с «Runtime error 217»?
Ну потому что она наиболее легко воспроизводима, а так технически, используя выше приведенный модуль, мы получим на руки вполне нормальное описание всех возможных Runtime ошибок, коих в наличии у нас вон сколько:

  reMap: array [TRunTimeError] of Byte = (     0,   { reNone }     203, { reOutOfMemory }     204, { reInvalidPtr }     200, { reDivByZero }     201, { reRangeError } {   210    Abstract error }     215, { reIntOverflow }     207, { reInvalidOp }     200, { reZeroDivide }     205, { reOverflow }     206, { reUnderflow }     219, { reInvalidCast }     216, { reAccessViolation }     218, { rePrivInstruction }     217, { reControlBreak }     202, { reStackOverflow }     220, { reVarTypeCast }     221, { reVarInvalidOp }     222, { reVarDispatch }     223, { reVarArrayCreate }     224, { reVarNotArray }     225, { reVarArrayBounds } {   226    Thread init failure }     227, { reAssertionFailed }     0,   { reExternalException not used here; in SysUtils }     228, { reIntfCastError }     229, { reSafeCallError }     235, { reMonitorNotLocked }     236  { reNoMonitorSupport } {$IFDEF PC_MAPPED_EXCEPTIONS} {   230   Reserved by the compiler for unhandled exceptions } {$ENDIF PC_MAPPED_EXCEPTIONS} {$IF defined(PC_MAPPED_EXCEPTIONS) or defined(STACK_BASED_EXCEPTIONS)} {   231   Too many nested exceptions } {$ENDIF} {$IF Defined(LINUX) or Defined(MACOS)} {   232   Fatal signal raised on a non-Delphi thread }     ,     233 { reQuit } {$ENDIF LINUX or MACOS} {$IFDEF POSIX}     ,     234  { reCodesetConversion } {$ENDIF POSIX}     ,     237, { rePlatformNotImplemented }     238  { reObjectDisposed } ); 

Итог

Ну что могу сказать…
Скорее всего — здравствуй велосипед, ибо думаю что данная проблема вероятней всего кем-то уже решалась ранее, но я просто не знал о данном решении 🙂
А если нет — значит буду вторым 🙂

Отдельный респект соавтору и вдохновителю данной статьи — Виктору Федоренкову.

Удачи.

ссылка на оригинал статьи http://habrahabr.ru/post/209902/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *