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

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

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

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

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

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

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

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

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

Здравствуйте, я ошибка 217 и я вам ничего не скажу (2)
Открыть картинку в полный размер…

На основании данной картинки можно сделать грубый вывод: пока 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, о котором я писал выше, сможет достаточно корректно произвести отображение возникшего исключения.

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

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

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

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

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

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

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

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

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

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

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

Здравствуйте, я ошибка 217 и я вам ничего не скажу (4)
Получилось.

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

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

И уже только после него пошли всякие там 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 });

Итог

Вот таким небрежным кодом, мы можем получить то, о чем нам не хочет говорить ошибка под кодом 217.

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

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

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

Удачи.

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

References

Top Articles
Latest Posts
Article information

Author: Kimberely Baumbach CPA

Last Updated:

Views: 6146

Rating: 4 / 5 (61 voted)

Reviews: 84% of readers found this page helpful

Author information

Name: Kimberely Baumbach CPA

Birthday: 1996-01-14

Address: 8381 Boyce Course, Imeldachester, ND 74681

Phone: +3571286597580

Job: Product Banking Analyst

Hobby: Cosplaying, Inline skating, Amateur radio, Baton twirling, Mountaineering, Flying, Archery

Introduction: My name is Kimberely Baumbach CPA, I am a gorgeous, bright, charming, encouraging, zealous, lively, good person who loves writing and wants to share my knowledge and understanding with you.