Copyright (C) А.Гавва V-0.4w май 2004

10. Отладка проекта

10.1 Директивы компилятора для отладки

GNAT предусматривает две директивы компилятора, которые могут быть полезны при отладке программ:

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

Директива компилятора Assert позволяет осуществлять проверку истинности некоторых условий в процессе выполнения программы. Если указанные условия ложны, то осуществляется возбуждение исключения. Эту директиву удобно использовать при отладке программы когда над разработкой проекта работает несколько программистов, поскольку в таком случае некоторые условия выполнения программы могут изменяться достаточно часто. Примером использования директивы Assert может служить следующее:

    
    
    pragma Assert (Screen_Height = 24);
    

Здесь, если значение переменной Screen_Height не будет равно 24, то будет возбуждено исключение ASSERT_ERROR.

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

    
    
    X := 5;
    pragma Debug (Print_To_Log_File( "X is now" & X'Img ));
    

В данном случае, если предположить, что процедура Print_To_Log_File используется для сохранения сообщения в каком-либо файле протокола (log-файле), то этот пример сохранит в этом файле протокола сообщение "X is now 5". Следует также заметить, что действие директивы компилятора Debug может быть подавлено с помощью директивы Suppress_Debug_Info.

При выполнении отладки программ могут быть полезными еще две директивы компилятора. Директива компилятора No_Return может быть использована для указания подпрограмм, которые никогда не завершаются (иначе, не возвращают управление). Это позволяет подавить соответствующие предупреждающие сообщения компилятора.

Директива компилятора Normalize_Scalars инициализирует любую переменную, которая не является массивом, записью или тэговой записью, недопустимым значением (если это возможно). Применение этой директивы позволяет осуществлять проверку правильности инициализации переменных перед тем как они будут использованы (при этом, ее следует размещать в начале подпрограммы или пакета).

В качестве примера, предположим, что существует целочисленная переменная, диапазон значений которой ограничен величинами от 1 до 100. Обычно Ада не осуществляет установку начальных значений, пока это не указано явно. При указании директивы Normalize_Scalars, такая переменная будет инициализирована каким-либо начальным значением, величина значения которого будет заведомо находиться вне указанного диапазона значений (например, -1). Таким образом, попытка использования значения этой переменной без предварительной инициализации приведет к возбуждению исключения CONSTRAINT_ERROR.

Следует заметить, что директива Normalize_Scalars будет работать лучше если при компиляции исходных текстов использована опция компилятора -gnatVf.

10.2 Получение расширенной информации компилятора

Опция командной строки компилятора -gnatG позволяет получить информацию о том как GNAT интерпретирует исходный текст после его первоначального анализа. Если при этом одновременно использована опция -gnatD, то GNAT сохранит эту информацию в файле с расширением имени .dg (для отладки "debug").

Для демонстрации использования опции -gnatG рассмотрим пример простой программы Pointers:

    
    
    with System.Address_To_Access_Conversions;
    with Ada.Text_IO;                          use  Ada.Text_IO;
    
    procedure Pointers is
    
      package Int_Ptrs is new System.Address_To_Access_Conversions( Integer );
        -- Конкретизация настраиваемого пакета для обеспечения возможности
        -- выполнения преобразования между ссылочными типами и адресами.
        -- Это также дополнительно создает ссылочный тип Object_Pointer,
        -- позволяющий ссылаться на значения целочисленного типа Integer.
    
      Five        : aliased Integer := 5;
        -- Переменная Five описана как косвенно адресуемая,
        -- поскольку мы будем обращаться к ней с помощью
        -- значений ссылочного типа
    
      Int_Pointer : Int_Ptrs.Object_Pointer;
        -- Обобщенный ссылочный тип Ады
    
      Int_Address : System.Address;
        -- Адрес в памяти, подобный указателю языка C
    
    begin
    
      Int_Pointer := Five'Unchecked_Access;
        -- Использование атрибута 'Unchecked_Access необходимо,
        -- поскольку переменная Five локальна для головной программы.
        -- Если бы она была глобальной, то мы могли бы использовать
        -- атрибут 'Access вместо атрибута  'Unchecked_Access.
    
      Int_Address := Five'Address;
        -- Адреса моут быть определены с помощью атрибута 'Address.
        -- Это эквивалентно указателям языка C.
    
      Int_Pointer := Int_Ptrs.To_Pointer( Int_Address );
      Int_Address := Int_Ptrs.To_Address( Int_Pointer );
        -- Преобразование типов указателей Ады и C.
    
    end Pointers;
    

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

    
    
    Source recreated from tree for Pointers (body)
    ----------------------------------------------
    
    with ada;
    with system;
    with system.system__address_to_access_conversions;
    with ada.ada__text_io;
    use ada.ada__text_io;
    with system;
    with system;
    with unchecked_conversion;
    
    procedure pointers is
       
       package int_ptrs is
          subtype int_ptrs__object is integer;
          package int_ptrs__address_to_access_conversions renames int_ptrs;
          type int_ptrs__object_pointer is access all int_ptrs__object;
          for int_ptrs__object_pointer'size use 32;
          function pointers__int_ptrs__to_pointer (value : system__address)
            return int_ptrs__object_pointer;
          function pointers__int_ptrs__to_address (value : 
            int_ptrs__object_pointer) return system__address;
          pragma convention (intrinsic, pointers__int_ptrs__to_pointer);
          pragma convention (intrinsic, pointers__int_ptrs__to_address);
       end int_ptrs;
       
       package body int_ptrs is
          
          function pointers__int_ptrs__to_address (value : 
            int_ptrs__object_pointer) return system__address is
          begin
             if value  =  null then
                return system__null_address;
             else
                return value.all'address;
             end if;
          end pointers__int_ptrs__to_address;
          
          function pointers__int_ptrs__to_pointer (value : system__address)
            return int_ptrs__object_pointer is
             
             package a_to_pGP3183 is
                subtype a_to_pGP3183__source is system__address;
                subtype a_to_pGP3183__target is int_ptrs__object_pointer;
                function 
                  pointers__int_ptrs__to_pointer__a_to_pGP3183__a_to_pR (s : 
                  a_to_pGP3183__source) return a_to_pGP3183__target;
             end a_to_pGP3183;
             function a_to_p is new unchecked_conversion (system__address, 
               int_ptrs__object_pointer);
          begin
             return a_to_pGP3183__target!(a_to_pGP3183__source(value));
          end pointers__int_ptrs__to_pointer;
       end int_ptrs;
       
       package int_ptrs is new system.system__address_to_access_conversions 
         (integer);
       five : aliased integer := 5;
       int_pointer : int_ptrs.int_ptrs__object_pointer := null;
       int_address : system.system__address;
    begin
       int_pointer := five'unchecked_access;
       int_address := five'address;
       int_pointer := int_ptrs.pointers__int_ptrs__to_pointer (int_address);
       int_address := int_ptrs.pointers__int_ptrs__to_address (int_pointer);
       return;
    end pointers;
    

10.3 Использование отладчика GNU GDB

Начиная с версии 3.11, компилятор GNAT поставляется вместе с работающим в режиме командной строки отладчиком GNU GDB. Следует заметить, что при программировании на языке Ада частое использование отладчика, как правило, не требуется. Это объясняется тем, что Ада обеспечивает хорошую раннюю диагностику, которая осуществляется на этапе компиляции, а при необходимости более точной проверки правильности работы программы удобно использовать соответствующие отладочные директивы компилятора и хорошо известную процедуру Put_Line, которая выведет нуждающиеся в проверке значения в какой-либо файл протокола. Тем не менее, бывают случаи, когда в результате какой-либо смутной ошибки выполнение программы выглядит непредсказуемо или, по непонятной причине, программа вовсе не выполняется. В подобных ситуациях, в качестве инструмента для поиска источника проблемы можно использовать отладчик GNU GDB.

10.3.1 Общие сведения об отладчике GNU GDB

Отладчик GDB является платформенно независимым отладчиком общего назначения. Он может быть использован для отладки программ, которые написаны на разных языках программирования и скомпилированы с помощью GCC. В частности, с его помощью можно отлаживать Ада-программы, которые скомпилированы с помощью компилятора GNAT. Последние версии отладчика GDB обеспечивают поддержку Ада-программ и позволяют работать со сложными структурами данных Ады. Следует заметить, что здесь приводится только краткий обзор, а исчерпывающая информация по использованию отладчика GDB содержится в руководстве "Отладка с помощью GDB" (Debugging with GDB).

В процессе компиляции программы, компилятор, опционально, способен записывать в генерируемый объектный файл отладочную информацию, которая может включать номера строк, описываемые типы данных и переменные Эта информация хранится в результирующем файле отдельно от сгенерированного объектного кода. Следует заметить, что наличие отладочной информации может значительно увеличить результирующий файл, но отладочная информация никак не влияет на исполняемый код, который непосредственно загружается в память, и на его производительность. Генерация отладочной информации инициируется указанием опциии -g в команде запуска компилятора gnatgcc или утилиты gnatmake. Необходимо особо подчеркнуть, что использование этой опции никак не влияет на генерируемый машинный код.

Отладочная информация записывается в стандартном системном формате, который используется множеством инструментальных средств, включая отладчики и профиляторы. Обычно, используемый формат разработан для описания семантики и типов языка C, но GNAT реализует схему трансляции использование которой позволяет кодировать информацию о типах и переменных Ады в стандартном формате языка C. В большинстве случаев подробности реализации этой схемы трансляции не представляют интереса для пользователя, поскольку GDB выполняет необходимые преобразования автоматически. Однако, в случае необходимости, информация об этой схеме трансляции может быть получена из файла exp_dbug.ads дистрибутива исходных текстов GNAT.

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

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

Здесь рассматривается базовое использование GDB в текстовом режиме. Команда запуска GDB имеет следующий вид:

    
    
    $ gdb program
    

Здесь, program - это имя исполняемого файла программы.

Заметим, что в случае использования дистрибутива GNAT от ALT, вместо команды gdb следует использовать команду gnatgdb:

    
    
    $ gnatgdb program
    

В результате выполнения показанной команды осуществляется активация отладчика и отображение приглашения командной строки отладчика "(gdb)". В этом случае, самая простая команда run, которая запускает программу на выполнение таким же образом, как и без отладчика.

10.3.2 Знакомство с командами GDB

Отладчик GDB поддерживает обширный набор команд. Руководство "Отладка с помощтью GDB" (Debugging with GDB) содержит полное описание всех команд и предоставляет большое количество примеров их использования. Более того, GDB имеет встроенную команду help, которая выдает краткую справку о доступных командах и их опциях. Следует заметить, что здесь, для демонстрации принципов использования отладчика GDB, рассматриваются только несколько наиболее часто употребляемых команд. При чтении данного материала следует создать какую-нибудь простую программу с отладочной информацией и поэкспериментировать с указанными командами.

set args arguments
С помощью данной команды можно указать список аргументов arguments, которые будут переданы программе при выполнении последующей команды run. В использовании команды set args нет необходимости, когда программа не нуждается в аргументах.
run
Команда run осуществляет запуск программы на выполнение со стартовой точки. Если программа была запущена ранее, а в текущий момент ее выполнение приостановлено в какой-либо точке прерывания, то отладчик запросит подтверждение на прекращение текущего выполнения программы и перезапуск выполнения программы со стартовой точки.
breakpoint location
С помощью данной команды можно осуществлять установку точек прерывания, в которых GDB приостановит выполнение программы и будет ожидать последующие команды. В качестве location можно использовать номер строки в файле, который указывается в виде файл:номер_строки, или имя подерограммы. При указании совмещенного имени, которое используют несколько подпрограмм, отладчик потребует уточнение на какой из этих подпрограмм необходима установка точки прерывания. В этом случае можно также указать, что точки прерывания должны быть установлены на всех подпрограммах. Когда выполняемая программа достигает точки прерывания, ее выполнение приостанавливается, а GDB сигнализирует об этом печатью строки кода, перед которой произошла остановка выполнения программы.
breakpoint exception name
Это специальная форма команды установки точки прерывания, которая позволяет прервать выполнение программы при возбуждении исключения с именем name. Если имя name не указано, то приостановка выполнения программы осуществляется при возбуждении любого исключения.
print expression
Эта команда печатает значение выражения expression. GDB нормально обрабатывает основные простые выражения Ады, благодаря чему, выражение expression может содержать вызовы функций, переменные, знаки операций и обращения к атрибутам.
continue
Эта команда позволяет продолжить выполнение программы, после приостановки в какой-либо точке прерывания, вплоть до завершения работы программы или до достижения какой-либо другой точки прерывания.
step
Выполнить одну строку кода после точки прерывания. Если следующая инструкция является вызовом подпрограммы, то последующее выполнение будет продолжено внутри (с первой инструкции) вызываемой подпрограммы
next
Выполнить одну строку кода. Если следующая инструкция является вызовом подпрограммы, то выполнение вызова подпрограммы и возврат из нее осуществляется без остановок.
list
Отобразить несколько строк вокруг текущего положения в исходном тексте. Практически, более удобно иметь отдельное окно редактирования, в котором открыт соответствующий файл с исходным текстом. Последующее исполнение этой команды отобразит несколько последующих строк исходного текста. Эта команда может принимать номер строки в качестве аргумента. В этом случае она отобразит несколько строк исходного текста вокруг указанной строки.
backtrace
Отобразить обратную трассировку цепочки вызовов. Обычно эта команда используется для отображения последовательности вызовов, которые привели в текущую точку прерывания. Отображение содержит по одной строке для каждой записи активации (кадр стека) соответствующей активной подпрограмме.
up
При остановке выполнения программы в точке прерывания, GDB способен отобразить значения переменных, которые локальны для текущего кадра стека (иначе - уровень вложения вызовов). Команда up может быть использована для анализа содержимого других активных кадров стека, перемещаясь на один кадр стека вверх (от кадра стека вызванной подпрограммы к кадру стека вызвавшей подпрограммы).
down
Выполняет перемещение на один кадр стека вниз. Действие этой команды противоположно действию команды up.
frame n
Переместиться в кадр стека с номером n. Значение 0 указывает на кадр стека, который соответствует положению текущей точки прерывания, то есть, вершине стека вызовов.

Показанный выше список команд GDB является ознакомительным, и он очень краток. О дополнительных возможностях, которыми являются условные точки прерывания, способность исполнения последовательности команд в точке прерывания, способность выполнять отладку программы на уровне команд процессора, и многих других полезных свойствах отладчика GDB можно узнать из руководства "Отладка с помощью GDB" (Debugging with GDB). Примечательно, что многие основные команды имеют сокращенные аббривиатуры. Например, c - для continue bt - для backtrace.

10.3.3 Использование выражений Ады

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

Таким образом, для краткости, отладчик действует так, как будто спецификаторы with и use явно указаны для всех написанных пользователем пакетов, что позволяет избавиться от необходимости использования полной точечной нотации для большинства имен. В случае возникновения двусмысленности GDB запрашивает у пользователя соответствующее уточнение. Более подпробно поддержка синтаксиса Ады рассматривается в руководстве "Отладка с помощью GDB" (Debugging with GDB).

10.3.4 Вызов подпрограмм определяемых пользователем

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

    
    
    call subprogram-name (parameters)
    

Ключевое слово call может быть опущено в случае, когда имя подпрограммы subprogram-name не совпадает с какой-либо предопределенной командой GDB.

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

Важность данного средства заключается в том, что оно позволяет использовать в программе различные отладочные подпрограммы, которые приспособлены к каким-либо специфическим структурам данных отлаживаемой программы. Подобные подпрограммы отладки могут быть написаны с целью получения высокоуровневого отображения внутреннего состояния какого-либо абстрактного типа данных, что намного удобнее чем использование распечатки содержимого памяти (иначе - дамп памяти), которая заната структурой данных, на физическом уровне. Следует учитывать, что стандартная команда отладчика GDB print имеет представление только о физическом расположении типов в памяти, а не об их смысловом значении и абстрактном представлении. Отладочные подпрограммы могут предусматривать отображение информации с учетом требуемого семантического уровня и, таким образом, их использование при отладке программы может быть очень полезно.

Например, при непосредственной отладке GNAT, ключевым моментом является наличие доступа к содержимому узлов семантического дерева, которое содержит внутреннее представление программы. Однако, узлы семантического дерева представляются в виде целочисленных значений, которые являются индексами в таблицу узлов. Использование встроенной команды print, для узлов семантического дерева, обеспечит простое отображение целочисленных значений, что не обладает желаемой информативностью. Таким образом, намного удобнее использовать подпрограмму PN (описана в файле treepr.adb дистрибутива исходных текстов GNAT), которая принимает на входе узел семантического дерева и отображает его высокоуровневое представление, включающее синтаксическую категорию данного узла, его место в исходном тексте, целочисленные значения, соответствующие родительскому узлу и узлам потомкам, а также множество дополнительной семантической информации. Для более детального изучения этого примера можно обратиться к телу этой процедуры в указанном файле.

10.3.5 Исключения и точки прерывания

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

break exception
Установить точку прерывания, в которой будет осуществляться приостановка выполнения программы, когда программа (или любая ее задача) возбуждает какое-либо (любое) исключение.
break exception name
Установить точку прерывания, в которой будет осуществляться приостановка выполнения программы, когда программа (или любая ее задача) возбуждает исключение с именем name.
break exception unhandled
Установить точку прерывания, в которой будет осуществляться приостановка выполнения программы, когда программа (или любая ее задача) возбуждает какое-либо исключение для которого не предусмотрен обработчик.
info exceptions
info exceptions regexp
Команда info exceptions позволяет пользователю анализировать все исключения, которые определены внутри Ада-программы. При использовании в качестве аргумента регулярного выражения regexp, будет отображена информация только о тех исключениях, чьи имена совпадают с указанным регулярным выражением.

10.3.6 Задачи Ады

При работе с задачами Ады, GDB позволяет использовать следующие команды:

info tasks
Эта команда отображает список текущих задач Ады, который имеет вид подобный следующему:

    
    
    @leftskip=0cm
    (gdb) info tasks
      ID       TID P-ID   Thread Pri State                 Name
       1   8088000   0   807e000  15 Child Activation Wait main_task
       2   80a4000   1   80ae000  15 Accept/Select Wait    b
       3   809a800   1   80a4800  15 Child Activation Wait a
    *  4   80ae800   3   80b8000  15 Running               c
    

При отображении списка, символ звездочки указывает на выполнение задачи в текущий момент времени. Первый столбец отображает идентификатор задачи (task ID), который используется для обращения к задачам в следующих командах.

break linespec task taskid
break linespec task taskid if ...
Эти команды подобны командам break ... thread ..., а linespec указывает строки исходного текста. Чтобы указать GDB на необходимость приостановки выполнения программы, когда определенная задача Ады достигнет точки прерывания, следует использовать квалификатор "task taskid". При этом, taskid является численным идентификатором задачи, который назначается отладчиком GDB (он отображается в первом столбце, при отображении списка задач с помощью команды "info tasks"). Если при установке точки прерывания квалификатор "task taskid" не указан, то точки прерывания будут установлены для всех задач программы. Квалификатор task может быть также использован при установке условных точек прерывания. В этом случае квалификатор "task taskid" указывается перед указанием условия точки прерывания (перед if).
task taskno
Эта команда позволяет переключиться на задачу, которая указывается с помощью параметра taskno. В частности, это позволяет просматривать и анализировать обратную трассировку стека указанной задачи. Необходимо заметить, что перед продолжением выполнения программы следует выполнить переключение на оригинальную задачу, в которой произошла приостановка выполнения программы, иначе возможен сбой работы планировщика задач программы.

Более подробные сведения о поддержке задач содержатся в руководстве "Отладка с помощью GDB" (Debugging with GDB).

10.3.7 Отладка настраиваемых модулей

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

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

    
    
    procedure g is
    
       generic package k is
          procedure kp (v1 : in out integer);
       end k;
    
       package body k is
          procedure kp (v1 : in out integer) is
          begin
             v1 := v1 + 1;
          end kp;
       end k;
    
       package k1 is new k;
       package k2 is new k;
    
       var : integer := 1;
    
    begin
       k1.kp (var);
       k2.kp (var);
       k1.kp (var);
       k2.kp (var);
    end;
    

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

    
    
    (gdb) break g.k2.kp
    

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

10.4 Ограничение возможностей языка

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

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

Следует заметить, что директива компилятора No_Run_Time также является директивой установки ограничений, поскольку осуществляет принудительное отключение использования библиотеки времени выполнения Ады.

Более подробная информация об использовании этих директив компилятора находится в документации компилятора GNAT.


Copyright (C) А.Гавва V-0.4w май 2004