- Как отлаживать программы на языке C в Linux с помощью отладчика GDB
- Основные аспекты использования GDB
- Пример использования GDB
- Заключение
- Отладка C++ программ в ОС GNU/Linux
- Пишем низкоуровневый отладчик под Linux на Python
- Подготовка
- Считываем значения
- Ассемблерный REPL
- Вызов функции, первая попытка
- Получаем адрес функции
- Ставим точки останова
- Вызов функции, вторая попытка
- Выделяем память
- Получаем следующие инструкции
Как отлаживать программы на языке C в Linux с помощью отладчика GDB
Оригинал: 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 и сообщить ему имя исполняемого файла, который мы хотим отлаживать. Для этого используется следующая команда:
Теперь следует обратить внимание на то, что в процессе исполнения программы выводится сообщение об ошибке «floating point exception» и, как многие из вас наверняка знают, данная ошибка обычно связана с делением произвольного значения на ноль. Помня об этом, я помещаю точку останова в строку под номером 11, где осуществляется деление. Это делается следующим образом:
Обратите внимание на приветствие отладчика (gdb) , после которого я ввел команду break 11 .
Теперь я прошу GDB начать исполнение программы:
При достижении точки останова в первый раз GDB выводит следующую информацию:
Как несложно заметить, отладчик вывел код строки, в которой была установлена точка останова. Теперь давайте выведем текущее значение переменной out . Это делается следующим образом:
Очевидно, что отладчик вывел значение 5 . На основе этой информации можно сделать вывод, что в текущее время программа работает корректно. Поэтому я прошу отладчик продолжить исполнение программы до достижения следующей точки останова путем ввода команды c .
Я продолжаю выполнять аналогичные действия до того момента, пока не вижу нулевое значение переменной out .
Теперь для подтверждения предположения о том, что имеется проблема, я использую команду GDB s (или step ) вместо c . Это делается потому, что я хочу, чтобы строка 11, на которой на данный момент остановилось исполнение программы, была исполнена, в результате чего исполнение программы может аварийно завершиться.
В результате случается следующее:
Да, приведенный выше вывод подтверждает, что системный сигнал генерируется именно в этой строке. Окончательное подтверждение происходит при попытке повторного исполнения команды s :
Вы можете отлаживать свои программы с помощью GDB аналогичным образом.
Заключение
В данной статье мы рассмотрели лишь малую толику возможностей отладчика GDB, доступных для изучения и использования. Обратитесь к странице руководства GDB для получения дополнительной информации о данном инструменте и попытайтесь самостоятельно использовать его для отладки своего кода. Рассматриваемый отладчик не является дружелюбным, но трата времени на его изучение вполне оправдана.
Источник
Отладка C++ программ в ОС GNU/Linux
Так уж случилось, что по долгу работы очень много времени провожу с операционными системами семейства GNU/Linux. Основным видом моей деятельности является разработка программного обеспечения на С++.
Так вот, основной проблемой при использовании отладчика – это отображение сложных контейнеров, например, stl-контейнеров.
Решение, которое я предлагаю, актуально для gdb. Этот отладчик поддерживает скрипты, написанные на языке python, а механизмы отображения сложных объектов, называются pretty printers. Т.е. чтобы отладчик отображал нам все правильно, необходимо указать ему где находятся скрипты с этими самыми pretty printers. Для указания отладчику дополнительных команд необходим файл .gdbinit.
Итак, попробую оформить все, как инструкцию, так и читать удобней, и сам не забуду.
1. Установить gcc, g++, gdb, libstdc++-dbg, последнее очень важно, т.к. в свежих дистрибутивах именно с отладочными символами происходит установка скриптов с pretty printers, например, в Ubuntu 14.04 появляется директория /usr/share/gcc-4.8/python/libstdcxx.
2. Создать в домашней директории файл .gdbinit со следующим содержимым:
Вроде все хорошо, но есть проблема, в последних версиях GNU/Linux, gdb собирают с поддержкой python версии 3.*, а по умолчанию установлен python версии 2.*. Чтобы устранить эту проблему, т.е. сделать скрипты с поддержкой обеих версий python есть патч, можно найти его на просторах сети, можно взять тут: _https://www.dropbox.com/s/ef265fbo00yk1x8/libstdcpp.patch. Изменениям подлежит файл /usr/share/gcc-4.8/python/libstdcxx/v6/printers.py
И снова, вроде все хорошо, но опять есть проблема, на некоторых дистрибутивах не устанавливаются те самые скрипты с pretty printers. Для этого есть svn репозиторий _svn://gcc.gnu.org/svn/gcc/trunk/libstdc++-v3/python. Вот ссылка с дополнительным описанием: _http://sourceware.org/gdb/wiki/STLSupport. Если это Ваш случай, то, после выгрузки репозитория, не забудьте в файл .gdbinit изменить путь до скачанных скриптов.
3. При сборке проекта с отладкой не забудьте указать флаг _GLIBCXX_DEBUG. Это необходимо, чтобы правильно отображались такие контейнеры, как stringstream.
В заключение, хочу сказать, что «пляски» с pretty printers (второй пункт) не нужны, если вы используете в качестве IDE QtCreator, т.к. эта IDE использует свои скрипты для взаимодействия с gdb.
Источник
Пишем низкоуровневый отладчик под Linux на Python
Существуют отличные отладчики вроде GDB и LLDB. И хотя их можно настраивать с помощью скриптов, порой хочется иметь больше контроля над работой отладчика. В этой серии статей мы попробуем создать свой отладчик с помощью библиотек python-ptrace, pyelftools и distorm3.
Исходники доступны на GitHub. Всё написано и скомпилировано на Linux x86_64.
Прим.перев. В этой статье используется устаревшая версия Python 2.
Подготовка
Чтобы избежать проблем с правами доступа, мы будем запускать отлаживаемый процесс как дочерний:
Здесь используется системный вызов ptrace для присоединения к дочернему процессу и его остановки. Теперь process содержит много удобных методов. Идея была заимствована из примера в документации python-ptrace.
Считываем значения
Начнём с простого. Получаем регистры:
Считываем байты из памяти:
Ассемблерный REPL
Теперь нам нужно научиться запускать ассемблерные инструкции по одной за раз. Давайте соберём нужные составляющие.
rip — это указатель на инструкцию. Префикс r обозначает длину в 64 бита. Как видите, он действительно смещается вперёд, когда мы делаем шаг.
Продолжаем до тех пор, пока дочерний процесс не получит сигнал (в нашем случае 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 ).
Источник