Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

GDB

Глава 4 из книги "Архитектура приложений с открытым исходным кодом", том 2.
Оригинал: GDB
Автор: Stan Shebs
Перевод: А.Панин

4.8. Часть, ответственная за работу в целевой системе

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

Векторы и стек целевой системы

Изначально часть GDB, ответственная за работу на целевой системе, была сформирована из набора специфичных для платформы файлов исходного кода, в рамках которых были реализованы алгоритмы вызова ptrace, запуска исполняемых файлов, и выполнения других подобных функций. Это решение не было достаточно гибким для использования при работе в рамках долговременных сессий отладки, когда пользователь может перейти от непосредственной к удаленной отладке, переключиться с отладки исполняемых файлов на анализ дампов памяти, после чего перейти к отладке работающих программ, подключать и отключать отладчик и выполнять другие аналогичные действия, поэтому в 1990 году John Gilmore провел повторное проектирование части GDB, ответственной за работу в целевой системе, направленное на выполнение всех специфичных для целевой системы операций посредством вектора целевой системы (target vector), являющегося по существу классом, на основе которого создаются объекты, каждый из которых описывает специфику типа целевой системы. Каждый вектор целевой системы реализован в форме структуры, состоящей из нескольких множеств указателей функций (обычно называемых "методами"), назначение которых варьируется от чтения и записи данных памяти и регистров до возобновления исполнения программы и установления параметров обработки разделяемых библиотек. В GDB существует около 40 векторов целевых систем, распределенных в диапазоне от часто используемого вектора целевой системы для Linux до малоизвестных векторов целевых систем, таких, как вектор для управления Xilinx MicroBlaze. Код поддержки дампов памяти использует вектор целевой системы, который получает данные путем чтения файла дампа памяти, при этом существует другой вектор целевой системы, читающий данные из исполняемого файла.

Обычно оказывается полезным смешивать методы нескольких векторов целевых систем. Представим случай вывода значения инициализированной глобальной переменной в Unix; перед началом исполнения программы вывод значения переменной будет работать, но в этот момент не будет процесса для чтения данных, поэтому байты данных должны быть получены из секции .data исполняемого файла. Таким образом, GDB использует векторы целевых систем для чтения исполняемых файлов и читает необходимые данные из бинарного файла. Но в процессе работы программы байты данных должны быть получены из адресного пространства процесса. Следовательно, в рамках GDB реализован "стек векторов целевых систем", в котором вектор целевой системы для работы с выполняющимися процессами помещается выше вектора для чтения данных из исполняемого файла при запуске процесса и перемещается вниз после завершения его работы.

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

(Несмотря на то, что мэйнтейнеры GDB недолюбливают реализацию стека целевых систем, никто не предложил и не создал прототип более совершенной альтернативы.)

Gdbarch

Являясь программой, работающей напрямую с инструкциями центрального процессора, GDB требуется подробная информация об особенностях устройства чипа. Требуется информация о регистрах, размерах различных типов данных, размере и виде адресного пространства, используемом соглашении о вызовах функций, инструкции, которая будет вызывать исключение ловушки, и.т.д. Код на языке C для работы с этой информацией в GDB обычно занимает от 1,000 до 10,000 строк в зависимости от сложности архитектуры.

Изначально работа с этой информацией осуществлялась путем использования специфичных для целевой платформы макросов препроцессора, но по мере усложнения отладчика макросы все больше увеличивались в размере и с течением времени длинные макроопределения были преобразованы в обычные функции языка C, вызываемые из макросов. Хотя это преобразование и было полезным, оно не могло применяться для различных вариантов архитектуры (ARM и ARM Thumb, 32-битная и 64-битная версии MIPS или x86, и.т.д.) и, что еще хуже, скоро должны были появиться многоархитектурные системы, в случае использования которых макросы не работали бы вообще. В 1995 году я предложил решение этой проблемы, заключающееся в использовании архитектуры на основе объектов и с 1998 года компания Cygnus Solutions начала финансирование работы Andrew Cagney, заключающейся в начальной реализации предложенных изменений. (Компания Cygnus Solutions была создана в 1989 году для коммерческой поддержки свободного программного обеспечения и в 2000 году была приобретена компанией RedHat.) Для завершения работы потребовалось несколько лет, причем в код также были внесены изменения от множества сторонних разработчиков и в итоге в ходе работы было изменено около 80,000 строк кода.

Новые реализованные конструкции стали называть объектами gdbarch и в данный момент эти объекты могут содержать до 130 методов и переменных, описывающих целевую архитектуру, но, несмотря на это, простая целевая архитектура может потребовать использования только части этих методов и переменных.

Для получения представления о том, чем отличаются старый и новый методы, рассмотрим объявление размера для типа данных long double архитектуры x86, равного 96 битам, из файла gdb/config/i386/tm-i386.h от 2002 года:
#define TARGET_LONG_DOUBLE_BIT 96
и из файла gdb/i386-tdep.c от 2012 года:
i386_gdbarch_init( [...] )
{
  [...]

  set_gdbarch_long_double_bit (gdbarch, 96);

  [...]
}

Управление исполнением

Сердцем GDB является цикл управления исполнением. Мы затрагивали его ранее при описании одиночных построчных переходов; алгоритм выполнял обход множества инструкций до момента нахождения инструкции, ассоциированной с другой строкой исходного кода. Этот цикл обхода называется wait_for_inferior или "wfi" для краткости.

Концептуально он находится внутри главного цикла обработки команд и используется только при вводе команд, которые направлены на продолжение исполнения программы. В момент, когда пользователь вводит команды continue или step и ожидает, не замечая никаких изменений, отладчик GDB на самом деле может быть загружен работой. В дополнение к описанному выше циклу пошагового перехода, программа может столкнуться с инструкциями-ловушками и сообщить об исключении GDB. Если исключение было сгенерировано из-за установки точки останова средствами GDB, производится проверка условия установки точки останова и в том случае, если условие не выполняется, ловушка убирается, осуществляется шаг по направлению к исходной инструкции, повторная установка ловушки, после чего выполнение программы возобновляется. Аналогично, в случае генерации сигнала GDB может принять решение о том, следует ли игнорировать его или произвести обработку одним из заранее заданных способов.

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

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

Преобразование к ориентированной на события модели заняло несколько лет. Я разделил цикл wait_for_interior на части в 1999 году, представив структуру управления состоянием выполнения приложения, предназначенную для замены набора локальных и глобальных переменных и преобразования беспорядочных переходов в небольшие независимые друг от друга функции. В то же время Elena Zanonni и другие разработчики представили очереди событий, которые позволяли получать и пользовательский ввод, и уведомления от цикла.

Протокол удаленной отладки

Несмотря на то, что векторы целевых архитектур GDB позволяют реализовать множество различных методов для управления процессом исполнения программы на удаленном компьютере, для этой цели у нас есть отдельный предпочтительный протокол. Для него не придумано определенного названия, поэтому для обозначения обычно используются термины: "удаленный протокол" ("remote protocol"), "удаленный протокол GDB" ("GDB remote protocol"), "удаленный последовательный протокол" (также используется аббревиатура "RSP", расшифровывающаяся как "remote serial protocol"), "удаленный протокол .c" ("remote.c protocol", в соответствии с расширением файла исходного кода с его реализацией) или иногда "протокол-заглушка" ("stub protocol") для указания на то, что реализация протокола осуществлена на целевой системе.

Основной протокол достаточно прост и отражает желание его реализации для работы с небольшими встраиваемыми системами 1980-х годов, объемы памяти которых измерялись в килобайтах. Например, в рамках протокола пакет $g запрашивает данные всех регистров и ожидает ответа, содержащего все байты данных из этих регистров, в итоге передаются все данные - предполагается, что количество регистров, их размер и порядок следования данных совпадают с используемыми в рамках проекта GDB соглашениями.

Протокол ожидает одиночного ответа на каждый отправленный пакет и предполагает, что соединение является надежным, добавляя контрольные суммы только к отправляемым пакетам (таким образом, пакет $g при отправке по сети на самом деле будет выглядеть как $g#67).

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

На самой же целевой системе реализация удаленного протокола может быть представлена в широком диапазоне форм. Протокол в полной мере документирован в руководстве GDB и это значит, что имеется возможность создания реализации, которая не будет обременена условиями лицензии GNU и на самом деле многие производители оборудования уже реализовали код, который позволяет работать с удаленным протоколом GDB как во время лабораторных испытаний, так и во время эксплуатации оборудования. Система IOS компании Cisco, под управлением которой работает большая часть выпускаемого этой компанией сетевого оборудования, является широко известным примером.

Реализация протокола на целевой системе обычно называется "отладочная заглушка" или просто "заглушка" для того, чтобы подчеркнуть тот факт, что она выполняет не так много работы самостоятельно. Исходные коды GDB содержат несколько примеров реализации подобных заглушек, которые обычно реализуются в форме примерно 1,000 строк низкоуровневого исходного кода на языке C. На совершенно новой системе вез установленной ОС код заглушки должен установить собственные обработчики для перехвата аппаратных исключений и, что более важно, для перехвата инструкций-ловушек. Также потребуется драйвер последовательного порта, если сеть организуется посредством последовательного соединения. Сама работа протокола достаточно проста, так как все необходимые пакеты содержат одиночные символы, которые могут быть декодированы при помощи оператора switch.

Другим подходом к реализации удаленного протокола является создание "спрайта" для обмена данными между копией GDB и удаленным отлаживаемым аппаратным обеспечением, представленным устройствами JTAG, устройствами "wigglers" для интерфейса JTAG, и.т.д. Обычно эти устройства имеют библиотеку, которая может использоваться на компьютере, физически соединенным с целевым устройством, при этом обычно API библиотеки не совместим в внутренней архитектурой GDB. Таким образом, хотя и существуют примеры непосредственного вызова функций библиотек из GDB, наиболее простым зарекомендовавшим себя способом является запуск спрайта в качестве независимой программы, которая понимает удаленный протокол и преобразует пакеты в вызовы функций предназначенной для работы с устройством библиотеки.

GDBServer

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

GDBserver не выполняет каких-либо действий, которые не может выполнять сам отладчик GDB; если в вашей целевой системе может выполняться GDBserver, то теоретически в ней сможет работать и GDB. Однако, приложение GDBserver в 10 раз меньше и ему не требуется осуществлять управление таблицами символов, поэтому оно очень хорошо подходит для работы со встраиваемыми системами на основе GNU/Linux, а также с другими подобными системами.

GDBserver
Рисунок 4.2: GDBserver

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


Продолжение статьи: Интерфейсы GDB