...

вторник, 21 января 2014 г.

Здравствуйте, я ошибка 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 }
);




Итог




Ну что могу сказать…

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

А если нет — значит буду вторым :)

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


Удачи.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


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

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