Локализация ошибок в приложениях Delphi c помощью библиотеки Jedi Code Library

Автор: Владимир Николаевич Лихачёв
Источник: RSDN Magazine #3-2005
Опубликовано: 07.10.2005
Версия текста: 1.0
Введение
Получение информации о стеке вызова подпрограмм
Получение информации о необработанных исключениях
Перехват всех исключений приложения
Литература

Введение

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

В настоящее время общепринятым подходом к обработке ошибочных ситуаций является использование исключений, которые генерируются при возникновении ошибки и обрабатываются программой с помощь конструкций try...except и try...finally. Такая схема обычно используется для обработки исключительных ситуаций, возникновение которых предусмотрено при разработке программы. Непредвиденные исключения, которые не были перехвачены с помощью конструкции try...except, генерируют событие OnException глобального объекта Application, которому передается информация об этих исключениях. Сложность практического использования события OnException для выявления источника ошибки заключается в том, что одно и то же исключение может возникать при выполнении различных фрагментов программы, а данных, передаваемых в объекте исключения, часто недостаточно для локализации ошибки.

Один из вариантов решения этой проблемы предлагает библиотека не визуальных компонентов Jedi Code Library (JCL) [1]. Библиотека JCL разрабатывается в рамках проекта Joint Endeavour of Delphi Innovators (JEDI) [2]. Основной целью этого проекта является расширение возможностей таких средств разработки, как Delphi, Kylix и кросплатформенного компилятора Free Pascal [3] путем реализации системных интерфейсов и дополнительных библиотек. В рамках проекта JEDI ведется создание:

Все разработки проекта JEDI распространяются свободно с исходным кодом по лицензии Mozilla Public License (MPL) [4]. В рамках библиотеки JCL ведется создание не визуальных компонентов более чем в 25 направлениях, одним из которых является локализация источника ошибки во время работы программы её собственными средствами. Для этого библиотека JCL предоставляет набор классов и функций, которые позволяют:

Рассмотрим более детально возможности библиотеки JCL, перечисленные выше.

Получение информации о стеке вызова подпрограмм

В императивном программировании выполнение программы представляет собой последовательный вызов и выполнение подпрограмм, которые в случае использования процедурного стиля программирования представляют собой процедуры и функции, а при использовании объектно-ориентированного – методы классов. Информацию о порядке вложенных вызовов подпрограмм, выполняемых в определенный момент времени, обычно называют стеком вызова подпрограмм или стеком вызова функций (сall stack), если используются терминология таких языков, как C или C++.

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

Для отладки вложенных вызовов подпрограмм компилятор Delphi создает связанный список структур данных, которые называются стековыми фреймами (stack frames). При вызове подпрограммы компилятор выделяет область памяти для стекового фрейма и добавляет его в связанный список, при завершении подпрограммы стековый фрейм из списка удаляется. В каждом фрейме сохраняется информация, необходимая для выполнения вызываемой подпрограммы: параметры, передаваемые подпрограмме; локальные переменные; адрес возврата из подпрограммы. Таким образом, с помощью связанного списка стековых фреймов можно в любой момент получить информацию о выполняемых подпрограммах [5]. Если отладка вложенных вызовов подпрограмм не требуется, то данные, необходимые для выполнения подпрограмм, могут располагаться компилятором непосредственно в стеке программы, без сохранения информации о вложенных вызовах подпрограмм. Это позволяет реализовать более быстрый вызов подпрограмм и уменьшить объем оперативной памяти, используемый программой. Компилятор Delphi позволяет управлять созданием стековых фреймов, в IDE Delphi это можно сделать с помощью параметра компиляции проекта [Project]-[Options]-[Compiler]-[Stack Frames].

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

      function JclCreateStackList(Raw: Boolean; AIgnoreLevels: Integer; FirstCaller: Pointer): TJclStackInfoList; 

Параметры функции определяют информацию, получаемую классом о порядке вызова подпрограмм:

Для получения такой информации о стеке вызовов, как имя подпрограммы, расположение её реализации и вызова в исходном тексте программы, класс TJclStackInfoList реализует метод AddToStrings, который позволяет получить эту информацию в текстовом виде:

      procedure TJclStackInfoList.AddToStrings(Strings: TStrings; IncludeModuleName, IncludeAddressOffset, IncludeStartProcLineOffset: Boolean); 

Параметр Strings - это набор строк, к которому добавляется информация о стеке вызова подпрограмм. Остальные параметры метода позволяют определить содержание дополнительной информации о месте вызова подпрограммы: имя выполняемого модуля (IncludeModuleName), смещение адреса команды процессора вызова подпрограммы (IncludeAddressOffset) и смещение строки исходного текста программы с вызовом подпрограммы (IncludeStartProcLineOffset) относительно начала процедуры или функции, в котором осуществляется её вызов.

Для того чтобы метод TJclStackInfoList.AddToStrings мог вывести информацию о стеке вызова подпрограмм, при компиляции и сборке программы необходимо включить в проект соответствующую отладочную информацию (табл. 1).

Информация о стеке вызова подпрограмм Отладочная информация
Имя вызываемой подпрограммы Необходима информация об адресах расположения подпрограмм в приложении. Эта информация сохраняется компилятором в отдельном файле, который обычно называется картой памяти программы (map file) и имеет расширение “map”. Чтобы компилятором создал этот файл, в IDE Delphi необходимо задать параметр сборки проекта:[Project]-[Options]-[Linker]-[Map file]-[Detailed].
Информация о расположении вызова в исходном тексте Необходимо наличие отладочной информации приложения и информации об адресах расположения подпрограмм в приложении (см. выше). Отладочная информация приложения включается непосредственно в выполняемый модуль проекта при установке параметра компиляции проекта в IDE Delphi:[Project]-[Options]-[Compiler]-[Debugging]-[Debug information]
Информация о вызовах подпрограмм стандартных библиотек Delphi Эта информация становится доступной при использовании отладочной версии модулей стандартной библиотеки Delphi. Для использования их в проекте необходимо в IDE Delphi включить опцию компиляции:[Project]-[Options]-[Compiler]-[Debugging]-[Use Debug DCUs]
Таблица 1.Отладочная информация, необходимая для получения данных о стеке вызова подпрограмм

Необходимость распространения файлов карты памяти вместе с выполняемыми файлами программы в ряде случаев может представлять некоторые неудобства. Библиотека JCL позволяет включать эту информацию непосредственно в выполняемый модуль, предварительно упаковывая, что уменьшает её размер. Для этого предназначен эксперт “Insert JCL Debug data”, устанавливаемый инсталлятором библиотеки в меню “Project” интегрированной среды разработки Delphi (рис. 1).


Рисунок. 1. Расположение эксперта “Insert JCL Debug data” в меню “Project” IDE Delphi.

После сборки проекта (с помощью команды Project-Build в IDE Delphi) эксперт выводит информационное окно об общем размере программы и о размере отладочной информации, добавленной им в приложение (рис. 2).


Рисунок. 2. Информация об успешном добавлении отладочной информации о карте памяти программы в выполняемый модуль проекта, выдаваемая экспертом “Insert JCL Debug data”.

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

      procedure TForm1.Button1Click(Sender: TObject); 
var 
  StackInfoList: TJclStackInfoList; 
begin 
  StackInfoList := JclCreateStackList(true, 0, nil); 
  try 
    Memo1.Lines.BeginUpdate; 
    Memo1.Lines.Clear; 
    StackInfoList.AddToStrings(Memo1.Lines, true, true, true); 
    Memo1.Lines.EndUpdate; 
  finally 
    StackInfoList.Free; 
  end; 
end; 


Рисунок. 3. Результат использования класса TJclStackInfoList для получения информации о стеке вызова подпрограмм.

Библиотека JCL предоставляет достаточный для практического применения набор программных средств для работы с файлом карты памяти программы, основные из которых перечислены ниже:

В состав примеров библиотеки JCL входит консольная утилита MakeJclDbg, демонстрирующая возможности использования перечисленных выше классов и функций.

Получение информации о необработанных исключениях

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

Все исключения, возникающие в ходе работы приложения, могут быть отнесены к одной из групп:

Получение информации об исключениях, инициируемых оператором Raise, основано на том, что при его выполнении вызывается системная функция RaiseException, которая и является непосредственным инициатором программного исключения [6]:

      procedure RaiseException(
  dwExceptionCode, dwExceptionFlags, 
  nNumberOfArguments: DWORD; lpArguments: PDWORD);  

Библиотека JCL перехватывает её вызов, и перед выполнением функции получает информацию о содержании стека вызова подпрограмм.

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

      function JclLastExceptStackListToStrings(
  Strings: TStrings; IncludeModuleName, IncludeAddressOffset,
  IncludeStartProcLineOffset: Boolean): Boolean;  

Назначение параметров функции аналогично параметрам метода AddToStrings класса TJclStackInfoList, описанному выше. В глобальном списке объектов GlobalStackInfoListхранится информация только о последнем исключении, возникшем в каждом из потоков, поэтому для получения информации о причине возникновения необработанного исключения лучше всего подходит событие OnException глобального объекта Application, которое генерируется при возникновении такого исключения. Инициализация механизма слежения за исключениями выполняется с помощью функции:

      function JclStartExceptionTracking: Boolean;

При нормальной инициализации режима функция возвращает значение True, если режим перехвата исключений уже инициализирован - False.

Приведенный ниже фрагмент обработчика события OnException глобального объекта Application выводит информацию о стеке вызова подпрограмм в момент возникновения исключения.

      procedure TForm1.ApplicationEvents1Exception(Sender: TObject; 
  E: Exception); 
begin 
  Memo1.Lines.BeginUpdate; 
  Memo1.Lines.Clear; 
  JclLastExceptStackListToStrings(Memo1.Lines, true, true); 
  Memo1.Lines.EndUpdate; 
end; 

Перехват всех исключений приложения

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

      function JclAddExceptNotifier(const NotifyProc: TJclExceptNotifyProc; Priority: TJclExceptNotifyPriority = npNormal): Boolean; overload; 
function JclAddExceptNotifier(const NotifyMethod: TJclExceptNotifyMethod; Priority: TJclExceptNotifyPriority = npNormal): Boolean; overload; 

Зарегистрированные таким образом подпрограммы выполняются при возникновении исключения с учетом приоритета Priority:

TJclExceptNotifyPriority = (npNormal, npFirstChain); 

Подпрограммы, зарегистрированные с приоритетом npFirstChain, вызываются первыми. Регистрируемые подпрограммы должны соответствовать одному из типов:

TJclExceptNotifyProc = procedure (ExceptObj: TObject; ExceptAddr: Pointer; OSException: Boolean); 
TJclExceptNotifyMethod = procedure (ExceptObj: TObject; ExceptAddr: Pointer; OSException: Boolean) ofobject; 

Основное отличие между ними заключается в том, что обработчик события типа TJclExceptNotifyMethod является методом класса, а TJclExceptNotifyProc – обычной процедурой. При вызове зарегистрированных подпрограмм им передаются данные об исключении:

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

      unit UntForm1; 
interface// ...implementation//...// процедура, вызываемая при возникновении исключенияprocedure AnyExceptionNotify(
  ExceptObj: TObject; ExceptAddr: Pointer; OSException: Boolean);
beginwith Form1 dobegin 
    Memo1.Lines.BeginUpdate; 
    JclLastExceptStackListToStrings(Memo1.Lines, false, True, True); 
    Memo1.Lines.EndUpdate; 
  end; 
end; 

//... initialization//... // инициализация механизма перехвата исключений 
Include(JclStackTrackingOptions, stRawMode); 
JclStartExceptionTracking; 
JclAddExceptNotifier(AnyExceptionNotify); 

//...end. 

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

Проблема определения источника ошибки в исходном коде программы средствами самого приложения возникает при разработке приложений с использованием различных языков программирования и средств разработки. Например, решению подобной задачи для среды разработки Microsoft Visual C++ посвящен ряд статей на сайте “CodeProject” [7]. Получение информации о стеке вызова подпрограмм во время возникновения исключений настолько актуально, что в платформе .NET реализована возможность получения этой информации с помощью свойства StackTrace стандартного класса Exception [8].

Литература

  1. http://sourceforge.net/projects/jcl/
  2. http://www.delphi-jedi.org
  3. http://www.freepascal.org
  4. http://www.mozilla.org/MPL/
  5. P. D. Terry, Compilers and compiler generators an introduction with C++. - Rhodes University, 1996.
  6. Рихтер Дж. Windows для профессионалов: создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows/Пер. с англ. - 4-е изд. – СПб: Питер; М.: Издательско-торговый дом “Русская Редакция”, 2001. – 752 с.
  7. http://www.codeproject.com
  8. http://msdn.microsoft.com


Эта статья опубликована в журнале RSDN Magazine #3-2005. Информацию о журнале можно найти здесь