Как написать отладчик под линукс

Обновлено: 07.07.2024

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

В сегодняшней статье мы рассмотрим как пользоваться gdb для отладки и анализа выполнения программ, написанных на Си. Попытаемся разобраться с основными возможностями программы.

Как пользоваться gdb

1. Установка gdb

Обычно, отладчик устанавливается вместе с другими инструментами для сборки программного обеспечения. Для установки всего необходимого в Ubuntu или Debian достаточно выполнить:

sudo apt install build-essential

Для того чтобы установить отладчик отдельно, выполните:

sudo apt install gdb

В CentOS или Fedora команда установки будет выглядеть следующим образом:

sudo dnf install gdb

А для Arch Linux надо выполнить:

sudo pacman -S gdb

Теперь отладчик gdb установлен и вы можете его использовать.

2. Компиляция программы

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

Для того чтобы всё необходимое было включено в исполняемый файл, программу надо собрать с опцией -g. В данном случае команда будет выглядеть вот так:

gcc -g -o program program.c

Затем вы можете её выполнить:


3. Запуск отладчика

Для того чтобы запустить программу достаточно передать путь к ней отладчику. Какие-либо опции указывать не обязательно:

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

  • break или b - создание точки останова;
  • info или i - вывести информацию, доступные значения: break, registers, frame, locals, args;
  • run или r - запустить программу;
  • continue или c - продолжить выполнение программы после точки останова;
  • step или s - выполнить следующую строчку программы с заходом в функцию;
  • next или n - выполнить следующую строчку без захода в функцию;
  • print или p - вывести значение переменной;
  • backtrace или bt - вывести стек вызовов;
  • x - просмотр содержимого памяти по адресу;
  • ptype - просмотр типа переменной;
  • h или help - просмотр справки по команде;
  • q или quit - выход из программы.

4. Запуск программы

Для того чтобы запустить программу надо воспользоваться командой run в консоли gdb. Просто выполните:


И программа будет запущена и выполнена. Если вам надо передать программе какие-либо аргументы, то следует их передать команде run, например:

(gdb) run arg1 arg2

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

Программа сообщает на какой строчке исходного кода возникла проблема. Чтобы посмотреть весь исходник выполните команду list:


Для того чтобы вызвать ошибку Segmentation Fault и проверить как это работает можете добавить в программу такие строки и перекомпилировать её:

char *buffer = malloc(sizeof(char) * 10);
while(1) *(++buffer) = 'c';
>

Тут мы выделяем из памяти массив символов размером 10 элементов и заполняем его и память, которая находится за ним символами "с", пока программа не упадёт.

5. Точки останова

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


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


Например, давайте установим точку останова на восьмой строчке исходника:

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


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


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

Для отладки циклов можно устанавливать точки останова на основе условия, например, на 11-той строке, если значение переменной var1 будет 20:

(gdb) break 11 if var1==20


Чтобы этот пример заработал добавьте в код цикла следующую строчку, как на снимке:

Посмотреть все установленные точки останова можно такой командой:

6. Вывод информации

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

(gdb) info locals


А для вывода значений аргументов функции:

Вывести значение определённой переменной можно с помощью print:

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

(gdb) print var1+30


Имя временной переменной, куда были записаны данные отображается перед самим результатом, например, в последней команде это $2. Теперь можно ещё раз вывести это значение:


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

7. Изменение информации

С помощью команды set можно изменить значение переменной прямо во время выполнения программы. Например:

(gdb) break 7
(gdb) run
(gdb) set var2 = 20
(gdb) print var2


8. Вывод типа

С помощью команды ptype вы можете вывести тип переменной. Например:

(gdb) break 7
(gdb) run
(gdb) ptype var2


9. Просмотр адресов

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

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

Теперь вы можете вывести адреса всех переменных в памяти с помощью символа &. Например:

(gdb) print &argc
(gdb) print &var1
(gdb) print &buffer
(gdb) print buffer

Как видите, аргументы функции находятся в одном месте, локальные переменные в другому, но не очень далеко, а во память выделенная из кучи, на которую указывает указатель buffer - совсем далеко. Можете поменять программу и поэкспериментировать с разным количеством переменных.

10. Просмотр памяти

С помощью команды x или eXamine можно посмотреть содержимое памяти, по определённому адресу. Например, смотрим содержимое переменной var2:


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

Также можно указать размер выводимого блока:

Мы пытались вывести переменную типа int. Она занимает обычно четыре байта. Для её корректного вывода используйте такие параметры:


Ещё можно указать количество блоков, которые надо выводить, например два:


Но поскольку там уже не наша переменная, эти данные не имеют смысла. Аналогично всё работает с строками, символами и другими значениями. Только обратите внимание, что если вы будете выводить переменную не одним блоком, то на результат повлияет порядок расположения байт. Значение будет выводиться задом на перед.

11. Справка по программе

Мы рассмотрели основные возможности отладчика gdb. Но если этого вам не достаточно, вы можете посмотреть справку по любой команде программы в самой программе. Для этого используйте команду help. Например, для команды exemine:


Выводы

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

Нет похожих записей


Статья распространяется под лицензией Creative Commons ShareAlike 4.0 при копировании материала ссылка на источник обязательна.

Существуют отличные отладчики вроде GDB и LLDB. И хотя их можно настраивать с помощью скриптов, порой хочется иметь больше контроля над работой отладчика. В этой серии статей мы попробуем создать свой отладчик с помощью библиотек python-ptrace, pyelftools и distorm3.

Исходники доступны на GitHub. Всё написано и скомпилировано на Linux x86_64.

Прим.перев. В этой статье используется устаревшая версия Python 2.

Подготовка

Чтобы избежать проблем с правами доступа, мы будем запускать отлаживаемый процесс как дочерний:

Здесь используется системный вызов ptrace для присоединения к дочернему процессу и его остановки. Теперь process содержит много удобных методов. Идея была заимствована из примера в документации python-ptrace.

Считываем значения

Начнём с простого. Получаем регистры:

Считываем байты из памяти:

Ассемблерный REPL

Теперь нам нужно научиться запускать ассемблерные инструкции по одной за раз. Давайте соберём нужные составляющие.

rip — это указатель на инструкцию. Префикс r обозначает длину в 64 бита. Как видите, он действительно смещается вперёд, когда мы делаем шаг.

Sportmaster Lab , Санкт-Петербург, Москва, Липецк , От 100 000 до 150 000 ₽

Продолжаем до тех пор, пока дочерний процесс не получит сигнал (в нашем случае SIGTRAP ). Это может привести к ошибке, если процесс завершится или будет получен другой сигнал:

process.singleStep() неблокирующий, поэтому для удобства мы добавим блокирующую версию:

Так делать не стоит, но пусть process пока побудет глобальной переменной.

Пишем в память. В ассемблере выполнение ассемблерной инструкции int3 приводит к тому, что процессу отправляется сигнал SIGTRAP . Её можно записать в виде одного байта 0xCC :

Также мы можем сравнить регистр rip до и после, чтобы проверить, что значение увеличилось ровно на 1.

Теперь у нас есть всё, что нужно, для запуска одной инструкции, переданной в виде байтов:

Здесь мы перезаписываем байты перед указателем на инструкцию с помощью instr , делаем шаг и возвращаем перезаписанные байты и позицию указателя инструкции. Последнюю часть делаем, только если указатель на инструкцию не изменялся (как в случае с jump или call ).

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

Вызов функции, первая попытка

Что делать, если мы хотим вызвать ассемблерную функцию и приостановить выполнение после возвращения из неё?

Напишем для этого func_call(func_addr) (запустите её пошагово, чтобы посмотреть на промежуточные состояния). Сначала сохраним часть текущего состояния:

Мы могли бы просто использовать run_asm с инструкцией call . Это байт 0xE8 , за которым следуют 5 байт little endian, описывающих разницу между текущим и целевым значениями rip .

Чтобы приостановить дочерний процесс после вызова, мы можем записать int3 (байт 0xCC ) после инструкций вызова:

Мы можем перепроверить, что вызов был совершён:

Теперь пусть процесс работает, пока не будет получен сигнал SIGTRAP (желательно тот, что мы установили):

А теперь восстановим перезаписанные байты и значения регистра. В некоторых ситуациях они нам могут пригодиться:

Получаем адрес функции

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

А теперь сам вызов:

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

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

Пока что мы можем это сделать следующим образом. С регионами памяти и /proc/pid/maps разберёмся чуть позже:

Ставим точки останова

Теперь у нас есть адреса функций и мы можем поставить точку останова, просто написав int3 (байт 0xCC ) в начале функции:

И восстановить перезаписанное значение после прохождения точки останова:

Эти функции можно использовать следующим образом:

Вызов функции, вторая попытка

В общем и целом первый подход работает на удивление хорошо, хотя есть некоторые проблемы.

Слишком большое расстояние вызова. call ( 0xE8 ) принимает в качестве аргумента только 5 байт, однако для описания адреса ( diff ) может потребоваться 8 байт. Мы можем либо подождать, пока не окажемся в пределах функции, которую хотим вызвать (это работает только в том случае, если нам не нужно вызывать функцию сразу же), либо поместить целевой адрес в регистр, например, rax , и воспользоваться инструкцией call rax (байты FF D0 ).

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

В теории мы могли бы восстановить 6 из 7 байт после одного шага, оставив только 0xCC . Однако это не решает проблему, а только уменьшает её размер.

Ещё мы могли бы вручную создать стековый кадр.

Вместо этого мы зарезервируем новый участок памяти и запишем наши инструкции туда.

Выделяем память

Мы можем использовать системный вызов mmap() (номер вызова 9) для резервирования памяти. Ему требуются некоторые магические константы, часть которых можно найти в ptrace.syscall :

С помощью следующей функции мы можем вызвать mmap . Здесь syscall представлен байтами 0F 05 :

Данная стратегия была позаимствована из этого примера. Для справки, вот константы:

Адрес зарезервированной памяти находится в rax после вызова, поэтому мы его извлекаем и возвращаем.

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

Тем не менее, в этой функции по-прежнему могут возникать ошибки сегментации.

Получаем следующие инструкции

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

Воспользуемся методом PtraceProcess.disassemble для получения итератора по следующим десяти инструкциям:

Запуск этой функции даст примерно следующий результат:

Метод PtraceProcess.dumpCode работает похожим образом, но с другим форматированием.

На этом пока всё. В следующей статье мы разберёмся с чтением/записью Си-переменных, запуском одиночных Си-команд, библиотеками общего пользования, динамической загрузкой и картами памяти ( /proc/pid/maps ).

Оригинал: How to debug C programs in Linux using gdb
Автор: Himanshu Arora
Дата публикации: 16 января 2017 г.
Перевод: А.Панин
Дата перевода: 7 марта 2017 г.

Если вы разрабатываете программное обеспечение на языках C/C++ или пользуетесь такими более редкими языками программирования, как Fortran и Modula-2, вам будет полезно знать о существовании отличного отладчика под названием GDB , который позволяет достаточно просто отлаживать ваш код, помогая устранять ошибки и различные проблемные конструкции. В рамках данной статьи мы постараемся обсудить основные приемы работы с GDB, включая некоторые полезные функции/параметры данного инструмента.

Но перед тем, как двинуться дальше, стоит упомянуть о том, что все инструкции и примеры, приведенные в данной статье, были протестированы в системе Ubuntu 14.04 LTS. В статье был использован пример кода на языке C; в качестве командной оболочки использовалась командная оболочка Bash (версии 4.3.11); также стоит сказать о том, что для отладки тестовой программы использовался отладчик GDB версии 7.7.1.

Основные аспекты использования GDB

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

Во-первых, для успешного использования таких отладчиков, как GDB, вам придется компилировать вашу программу таким образом, чтобы компилятор генерировал отладочную информацию, необходимую отладчикам. Например, в случае компилятора GCC, который будет впоследствии использоваться для компиляции примера программы на языке C, вам придется дополнительно передать параметр -g на этапе компиляции кода.

Вы можете получить дополнительную информацию о данном параметре компилятора на его странице руководства .

На следующем шаге следует убедиться в том, что отладчик GDB установлен в вашей системе. Если он не установлен и вы используете основанный на Debian дистрибутив, такой, как Ubuntu, вы можете установить данный инструмент с помощью следующей команды:

Инструкции по установке отладчика в других дистрибутивах приведены на данной странице .

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

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

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

Стоит упомянуть и о том, что в том случае, если вашей программе нужно передать некие аргументы командной строки, вы можете передать их вместе с данной командной. Например:

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

Пример использования GDB

Теперь вы имеете базовое представление об отладчике GDB и принципе его использования. Поэтому предлагаю рассмотреть пример и применить полученные знания на практике. Это код примера:

В общем, данный код берет каждое значение из массива val , устанавливает это значение в качестве значения целочисленной переменной out , после чего рассчитывает значение переменной tot путем суммирования ее предыдущего значения и результата вычисления 0xffffffff/out .

Итак, для отладки кода в первую очередь следует скомпилировать код программы с использованием параметра компилятора -g . Это команда компиляции:

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

Обратите внимание на приветствие отладчика (gdb) , после которого я ввел команду break 11 .

Теперь я прошу GDB начать исполнение программы:

При достижении точки останова в первый раз GDB выводит следующую информацию:

Как несложно заметить, отладчик вывел код строки, в которой была установлена точка останова. Теперь давайте выведем текущее значение переменной out . Это делается следующим образом:

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

Я продолжаю выполнять аналогичные действия до того момента, пока не вижу нулевое значение переменной out .

Теперь для подтверждения предположения о том, что имеется проблема, я использую команду GDB s (или step ) вместо c . Это делается потому, что я хочу, чтобы строка 11, на которой на данный момент остановилось исполнение программы, была исполнена, в результате чего исполнение программы может аварийно завершиться.

В результате случается следующее:

Да, приведенный выше вывод подтверждает, что системный сигнал генерируется именно в этой строке. Окончательное подтверждение происходит при попытке повторного исполнения команды s :

Вы можете отлаживать свои программы с помощью GDB аналогичным образом.

Заключение

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


GNU Debugger, также известный как gdb, позволяет нам прокрасться через код во время его выполнения или то, что программа пыталась сделать в момент перед сбоем. GDB в основном помогает нам делать четыре основных вещи, чтобы выявлять недостатки в исходном коде.

  1. Запустите программу, указав аргументы, которые могут повлиять на общее поведение.
  2. Остановить программу при определенных условиях.
  3. Изучите сбой или время остановки программы.
  4. Измените код и сразу же поэкспериментируйте с измененным кодом.

Мы можем использовать gdb для отладки программ, написанных на C и C ++, без особых усилий. На данный момент поддержка других языков программирования, таких как D, Modula-2, Fortran, является частичной.

Начало работы с GNU Debugger или GDB

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

Введите help list, чтобы узнать о различных классах команд, доступных внутри gdb. Введите help , а затем имя класса, чтобы получить список команд в этом классе. Введите help all для получения списка всех команд. Сокращения названий команд разрешены, если они недвусмысленны. Например, вы можете ввести n вместо next или c для continue и так далее.

Обычно используемые команды gdb перечислены в следующей таблице. Эти команды должны использоваться из командной строки gdb ( gdb ).

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

Рассмотрим следующий исходный код.

Чтобы отладить выходной файл, нам нужно скомпилировать его с параметром -g для gcc следующим образом.

Выходной файл sum можно прикрепить к GDB одним из следующих двух способов:

1. Указав выходной файл в качестве аргумента для gdb.

2. Запуск выходного файла внутри GDB с помощью команды file .

Команда list выводит список строк в файле исходного кода и перемещает указатель. Таким образом, первый список будет отображать первые 10 строк, а следующий список - следующие 10 и так далее.

Чтобы начать выполнение, введите команду run . Теперь программа запускается нормально. Но мы забыли поставить точки останова в исходном коде для отладки, верно? Эти точки останова могут быть указаны для функций или в определенных строках.

Примечание. Я использовал аббревиатуру b для обозначения перерыва.

После установки точки останова в основной функции повторный запуск программы остановится на строке 11. То же самое можно сделать, если номер строки известен ранее.

Теперь просмотрите строки кода, используя команду next или n . Важно отметить, что команда next не входит в код функции, если для функции не установлена u200bu200bточка останова. Давайте сейчас опробуем команду print . Установите точку останова на сумме функций, как показано ниже.

Если для запускаемой программы требуются параметры командной строки, укажите их вместе с командой run как.

Файлы общей библиотеки, связанные с текущей запущенной программой, могут быть перечислены как файлы.

GDB также может изменять переменные во время выполнения программы. Давай попробуем это. Как упоминалось выше, установите точку останова в строке 16 и запустите программу.

Теперь a u003d 1, b u003d 2, и результат должен быть z u003d 3. Но здесь мы изменили окончательный результат на z u003d 4 в главной функции. Таким образом можно упростить отладку с помощью gdb.

Чтобы получить список всех точек останова, введите info breakpoints .

Здесь есть только одна точка останова - То. enabled отключить точки останова укажите номер точки останова вместе с командой disable . Для последующего включения используйте команду enable .

Вы также можете удалить точки останова с помощью команды delete .

В системе GNU/Linux в фоновом режиме работают многочисленные процессы. Чтобы отладить запущенный процесс, прежде всего, нам нужно найти идентификатор этого конкретного процесса. Команда pidof дает вам pid процесса.

Теперь нам нужно прикрепить этот pid к gdb. Есть 2 способа.

1. Указав pid вместе с gdb.

2. Используя команду attach из gdb.

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

Читайте также: