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

в 17:16, , рубрики: Без рубрики

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

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

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

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

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

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

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

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

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

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

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

Идея здравая и причины понятны, но вот как-же так, ресурсы все-же не освобождены, 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.

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

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

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

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

Итог

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

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

Удачи.

Автор: Rouse

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js