Skip to content
Dmitry edited this page Jan 9, 2023 · 3 revisions

Оглавление


Отладчик GDB

GNU Debugger - Стандартный отладчик, является частью RISC-V Toolchain.

Возможности отладчика GDB:

  • Запуск программы;
  • Прерывание исполнения программы;
  • Установка точек останова (breakpoint) и точек слежения (watchpoint) в том числе условных;
  • Запуск отдельных функции или фрагментов исследуемой программы;
  • Выполнение исследуемой программы по шагам и по инструкциям;
  • Отображение текущего кода исследуемой программы;
  • Создание и исследование дампов памяти;
  • Чтение и запись памяти, в том числе глобальных и локальных переменных;
  • Чтение и запись регистров;
  • Исследование стека;
  • и другие возможности.

Работа с GDB осуществляется с помощью пользовательского интерфейса (с использованием IDE) или GDB-командами.

Все GDB-команды могут быть выполнены в ручном режиме и автоматизированно.
В ручном режиме - с помощью GDB-консоли.
Автоматизированно - с использованием файлов-скриптов, содержащих последовательность GDB-команд.
Основные GDB-команды приведены в документе 'RefCard - GDB Quick Reference'.

Также у GDB есть Python API, что позволяет выполнять действия с исследуемой целевой программой из Python-кода.


Python GDB API

Python GDB API позволяет расширить возможности GDB за счёт использования языка Python. Исследование целевой программы сводится к вызовам Python-конструкций.
Python GDB API даёт возможность создавать Python-скрипты для управления GDB. Также есть возможность реализовывать собственные GDB-команды.
Это позволяет автоматизировать работу с GDB и обеспечивает большую гибкость по сравнению с обычными GDB-скриптами.

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

Такой подход является универсальным: запуск и тестирование программ может проводиться и на реальной аппаратуре, и на симуляторе, и на ПК (для отработки программ на уровне языка) с использованием одних и тех же скриптов, тестовых данных и т.д.

Тестирование с помощью Python GDB API

Использование Python может применяться для генерации тестовых наборов и/или для подгрузки данных из имитационных файлов и анализа результатов. То есть, использование Python GDB API позволяет автоматизировать тестирование.

Автоматизировать тестирование можно, как для автономных модулей (например функций), так и для целых проектов и режимов.

Для unit-тестирования можно использовать стандартный Python-фреймворка 'unittest' совместно с Python-GDB-API.

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


Перед началом работы

Чтобы проверить, поддерживает ли данная сборка GDB-клиента Python API, можно выполнить:

    (gdb) python print('Text')

Если без ошибок вывелось Text, то всё в порядке.

Требования к C++ проекту

  • Должна быть крутилка с меткой trap (сейчас в startup.S).
  • В startup.S должен быть 'nop' после возврата из main(), перед trap.
  • Должна быть функция main().
  • Чтобы можно было тестировать методы и функции, которые определены но не вызываются, проект должен быть собран без -Xlinker --gc-sections (Remove unused sections).

Python-GDB-команды в GDB-консоле

[23.2.1 Python Commands]

(gdb) python-interactive[command] или pi[command] - Выполнение однострочных python-команд. python-interactive или pi (без команды) - это запуск интерактивной python-консоли ">>>" для выполнения многострочного python-кода. Выйти: Ctrl+D.

(gdb) python[command] или py[command] - Выполнение однострочных python-команд. python или py (без команды) - запуск python-консоли ">" для выполнения многострочного python-кода, блок кода нужно закончить командой end. Выйти: Ctrl+D. Такой режим можно использовать для отработки самих Python-GDB-скриптов.

Пример выполнения по одной python-команде:

    (gdb) python import gdb
    (gdb) python print(gdb.VERSION)

Пример работы в многострочном режиме python-интерпретатора:

    (gdb) python
    >import gdb
    >print(gdb.VERSION)
    >end

Python-GDB-скрипт

Для запуска python-GDB-скрипта, его нужно передать в GDB-клиент. Python-GDB-скрипт передаётся при запуске GDB-клиента параметром -x ...:

    .../riscv64-unknown-elf-gdb-py .../file.elf -x .../gdb.py --silent

(Параметры: file.elf - elf-файл исследуемой программы; gdb.py - python-GDB-скрипт; --silent - ключ, подавляющий вывод информации о сборке и лицензии GDB-клиента в начале GDB-сессии)

Или при уже запущенном GDB-клиенте - через команду 'source ...':

    (gdb) source .../gdb.py

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

Пакетный режим (batch mode):
Существует пакетный режим, в котором с помощью '-x ...' можно задать несколько GDB-скриптов. Включается пакетный режим с помощью '-batch' при запуске GDB-клиента. В пакетном режиме автоматически установлены настройки 'set height unlimited' и 'set confirm off'.

Вывод текста из python-GDB-скрипта:
Работает обычный питоновский print('...') - это стандартный вывод в консоль. Можно использовать gdb.write('...') - это вывод в поток вывода GDB-клиента. [23.2.2.1 Basic Python]

Завершение Debug-сессии:
В обычном GDB-скрипте для завершения достаточно выполнить:

    quit
    y

В python-GDB-скрипте так не получается. После 'quit' требуется подтверждение, но при этом на выполнение 'y' из скрипта нет реакции. На '^C' (INTERRUPT) и '^D' (Ctrl+D) (EOF) из скрипта, тоже не реагирует.

Для корректного завершения Debug-сессии, из python-GDB-скрипта необходимо выполнить команды:

    gdb.execute('monitor shutdown')
    gdb.execute('set confirm off')
    gdb.execute('quit')

Команда 'monitor shutdown' приводит к отключению соединения и завершению процесса OpenOCD.
Настройка 'set confirm off' отключает все запросы подтверждения (y or n).
После этого можно выполнить 'quit', при этом запроса подтверждения не будет, процесс GDB-клиента завершится.
(Такой подход получается универсальным и для симулятора и для OpenOCD)

Команда gdb.execute('...'):
Это вариант для ленивых. С помощью данной команды можно выполнить любую команду GDB-консоли. Но лучше так не делать, а использовать конструкции Python GDB API. Однако, некоторые GDB-команды (например 'continue') без gdb.execute('...') выполнить невозможно. [23.2.2.1 Basic Python]


Демонстрационный проект

Проект py-gdb-api_demo - это демонстрационный пример использования Python GDB API для автоматизации запуска и тестирования целевой программы.
Использовалась версия GDB-клиента: 10.1

Содержимое проекта py-gdb-api_demo

  • Каталог /modules/ содержит реализации собственных GDB-команд и GDB-функций Собственные GDB-команды и GDB-функции
  • Каталог /gdb-py/ содержит python-GDB-скрипты:
  • Каталог /launch-sh/ содержит bash-скрипты для запуска:
    • openocd_gdb_launch.sh - запуск с реальной аппаратурой
    • spike_openocd_gdb_launch.sh - запуск с симулятором Spike
  • Файл paths содержит пути OpenOCD, GDB-клиента, python-GDB-скрипта и др.

Quick start

Склонировать проект:

$ git clone ...
$ cd py-gdb-api_demo
$ git submodule update --init --recursive

Указать актуальные пути в файле '/py-gdb-api_demo/paths'.

Собрать проект:

    make

Запуск с реальной аппаратурой:

    cd ./launch-sh/
    ./openocd_gdb_launch.sh ../Debug/py-gdb-api_demo.elf

Запуск с симулятором Spike:

    cd ./launch-sh/
    ./spike_openocd_gdb_launch.sh ../Debug/py-gdb-api_demo.elf

Класс gdb.Value

[23.2.2.3 Values From Inferior]

В python GDB API практически любой объект (экземпляр класса, переменная, регистр и др.) существуют в виде объекта типа gdb.Value.

Некоторые атрибуты и методы для работы с gdb.Value:

  • gdb.parse_and_eval('expression') - Получить объект типа gdb.Value.
  • Value.dereference() - Получить объект по указателю на него.
  • Value['field'] - Считать поле объекта (класса/структуры).
  • Value.address - Адрес объекта.
  • Value.type - Тип объекта.
  • Value.is_optimized_out - Признак того, что объект (в виде gdb.Value) оптимизирован (выброшен компилятором).

Запись значения в регистр с помощью gdb.Value:

    pc = gdb.parse_and_eval('$pc')
    pc = 0x10000020

Запись в поле C++ объекта:

    gdb.parse_and_eval('obj.field = ...')

Чтение поля C++ объекта:

    field_val = gdb.parse_and_eval('obj.field')

Другой способ чтения поля C++ объекта:

    field_val = Value['field']

Но запись в поле объекта (класса/структуры) с помощью Value['field'] не работает:

    objInfo = gdb.parse_and_eval('exampleObj')
    objInfo['indexMax'] = 4
    "NotImplementedError: Setting of struct elements is not currently supported."

Вызов метода C++ объекта:

    gdb.parse_and_eval('obj.methodName(...)')

Можно вызвать объект gdb.Value, если он является С++ функцией (п.23.2.2.3):

    gdb.parse_and_eval('functionName')(...)

При этом его тип: Value.type.code == gdb.TYPE_CODE_FUNC.

Но если объект gdb.Value является С++ методом экземпляра класса, то есть его тип: Value.type.code == gdb.TYPE_CODE_METHOD, то не получается выполнить вызов или gdb.parse_and_eval('obj.methodName')(...):

    RuntimeError: Value is not callable (not TYPE_CODE_FUNC)

При этом Value.type.code недоступен для записи (невозможно заменить на gdb.TYPE_CODE_FUNC).

Вызов функции:

    ret_val = gdb.parse_and_eval('functionName')(...)

Другой способ вызова функции:

    ret_val = gdb.parse_and_eval('functionName(...)')

Чтение локальной переменной:

    val = gdb.parse_and_eval('var_name')

Другой способ чтения локальной переменной:

    frame_var = gdb.selected_frame().read_var('var_name')

Запись в локальную переменную:

    gdb.parse_and_eval('var_name = ...').

Вызов C++ метода

Вызов C++ метода объекта:

    gdb.parse_and_eval("obj.methodName(...)")

Результат выполнение - возвращаемое значение метода представленное в виде gdb.Value.

Другой вариант - использовать GDB-команду call expr [17.5 Calling Program Functions]. Команда call вызывает функцию/метод. Если возвращаемое значение не void, то оно печатается и сохраняется в переменную value history.
Переменные value history (п.10.10) - нумерованные переменные ($n) содержат значения, выводимые командой print. Value history чем-то похожи на обычные GDB-переменные [10.11 Convenience Variables].

  • show values - Вывести все value history переменные.
  • $n - value history переменная под номером n (например $1).
  • $ - Последняя на текущий момент value history переменная.
  • $$ - Предпоследняя value history переменная.

В Python GDB API есть средство для работы с convenience variables и с value history [23.2.2.1 Basic Python]:

  • gdb.convenience_variable('name')
  • gdb.set_convenience_variable('name', value)
  • gdb.history(number) - Возвращает значение value history переменной по её номеру. Если number == 0 - это последняя value history переменная.

Чтобы выражение expr вычислилось (в том числе вызов C++ метода) не обязательно делать вызов командой call, достаточно чтобы выражение expr поучавствовало в другом выражении (например в присваивании set $val = expr, в сравнении, или при передаче выражения expr параметром в функцию). Кроме того, команда call (и gdb.execute('call ...')) всегда делает вывод value history вида "$n = ...", что не очень красиво. Поэтому, для вызова C++ метода, вместо команды call удобно использовать присвоение выражения expr GDB-переменной: set $var = expr.

Python-код обёрнут в convenience function Модуль call_method [23.2.2.22 Writing new convenience functions].
Важно: При вызове из GDB-консоли convenience-функции call_method(...), выражение expr (С++ метод) вычисляется сразу же при передаче параметром в функцию; таким образом внутри функции находится уже вычисленный результат в виде объекта gdb.Value и больше ничего вычислять не нужно. Из функции возвращается объект простого типа (не gdb.Value).
А при вызове из Python-кода этой же convenience-функции CallMethod.invoke('...'), передача параметром производится не готовым выражением expr (выражением не получается), а строкой содержащей выражение; таким образом внутри функции находится строка (содержащая expr), которую можно вставить в выражение вида set $val = expr, то есть использовать конструкцию gdb.execute('set $val = expr'). Из функции возвращается объект gdb.Value.

Вызов из GDB CLI:

    print $call_method(obj.methodName(...))     # ! Вызываемое expr (C++ метод) без кавычек
    или сразу expr:
    set $ret = obj.methodName(...)
    print $ret

Вызов из Python-кода:

    ret = CallMethod.invoke('obj.methodName(...)')  # Передается строкой <str>

[17.5 Calling Program Functions]
[15.4.1.3 C++ Expressions]


Класс gdb.Inferiors

[23.2.2.16 Inferiors In Python]

В терминологии GDB, программа, запущенная под GDB, называется Inferior (подчинённый). В одной GDB-сессии может быть несколько inferiors.

Некоторые методы для работы с gdb.Inferiors:

  • gdb.inferiors() - Возвращает кортеж всех объектов типа inferior.
  • gdb.selected_inferior() - Возвращает текущий объект типа inferior.
  • Inferior.is_valid() - Проверки inferior-объект на валидность.
  • Inferior.read_memory(address, length) - Чтение памяти. Возвращает python-объект типа memoryview.
  • Inferior.write_memory(address, buffer [, length]) - Запись памяти.

Класс gdb.Type

[23.2.2.4 Types In Python]

Класс gdb.Type содержит тип С++ объекта.

Некоторые атрибуты и методы для работы с gdb.Type:

  • gdb.lookup_type('...') - Находит указанный тип и возвращает его в виде gdb.Type.
  • Type.name - Имя типа.
  • Type.sizeof - Размер типа.
  • Type.fields() - Возвращяет объект gdb.Field - поля структур и union-ов. ( Пример: <structObj>.type.fields() ).

Класс gdb.Symbol

[23.2.2.27 Python representation of Symbols]

Класс содержит представление символов переменных, функций, классов и др. (из таблицы символов).

Некоторые атрибуты и методы для работы с gdb.Symbol:

  • gdb.lookup_symbol(name [, block [, domain]]) - Находит символ по имени в определённой области и возвращает (gdb.Symbol, bool). Второй элемент == True, если символ - это поле или метод объекта, иначе False.
  • gdb.lookup_global_symbol(name [, domain]) - Находит глобальный символ по имени и возвращает gdb.Symbol.
  • Symbol.type - Тип в виде gdb.Type.
  • Symbol.line - Строка кода, где символ объявлен.
  • Symbol.value([frame]) - Вычисление значание символа, возвращается в виде gdb.Value.

Stack

Класс gdb.Frame

[23.2.2.25 Accessing inferior stack frames from Python]

В Python GDB API стек-фрейм существует в виде объекта типа gdb.Frame.
Некоторые атрибуты и методы для работы с gdb.Frame:

  • gdb.selected_frame() - Получить объект типа gdb.Frame для текущего frame.
  • Frame.is_valid() - Проверка корректности фрейма.
  • Frame.name() - Возвращает имя функции фрейма (Пример: 'main').
  • Frame.function() - Возвращает символ функции фрейма (Пример: 'main()').
  • Frame.pc() - Возращает адрес возврата из фрейма.
  • Frame.function() - Возврашяет символ функции фрейма.
  • Frame.older() - Возврашяет вышестоящий фрейм.
  • Frame.newer() - Возврашяет фрейм, вызываемый из данного фрейма.
  • Frame.find_sal() - Возвращает symtab и строку объекта.
  • Frame.find_sal().line - Номер строки объекта.
  • Frame.read_register('pc') - Считать значение регистра <class 'gdb.Value'>.
  • Frame.read_var('var') - Считать локальную переменную <class 'gdb.Value'>.
  • Frame.select() - Выбрать данный фрейм текущим.

Исследование стек-фрейма

gdb.execute('frame') - вывод похожий, как при останове на breakpoint: номер текущего фрейма (#0); имя текущей функции (метода); текущий файл; номер текущей строки.
Подобное можно сделать с помощью Python GDB API (но вывод получается менее удобный и менее информативный):

  • frame = gdb.selected_frame()
  • line = frame.find_sal().line
  • filename = frame.find_sal().symtab.filename
  • fnname = frame.function()
  • print(frame)
  • print(line)
  • print(filename)
  • print(fnname)

Исследование стека

'backtrace' (или 'bt') и 'info stack' (или 'i s') или 'where'
Исследование стека: трассировка стек-фреймов от текущего к вышестоящим. Номер текущего (нижнего) фрейма - #0, номер вышестоящего фрейма - #1, и т.д.
'bt full' - Вывод локальных переменных с учётом вложенности фреймов.


Класс gdb.Breakpoint

[23.2.2.30 Manipulating breakpoints using Python]

bp = gdb.Breakpoint('main') или bp = gdb.Breakpoint('main', gdb.BP_BREAKPOINT) - Создать обычный software breakpoint.
wp = gdb.Breakpoint('val', gdb.BP_WATCHPOINT) - Создать watchpoint.

Некоторые атрибуты и методы для работы с gdb.Breakpoint:

  • bp.delete() - Удалить breakpoint bp.
  • bp.enabled = <bool> - Признак активности breakpoint. Доступно для записи.
  • bp.silent = <bool> - Признак того, что breakpoint без вывода. Доступно для записи. (В некоторых случаях бывает полезно выключить вывод от конкретного breakpoint-а).
  • bp.ignore_count = <int> - Количество игнорирований перед срабатыванием. Доступно для записи.
  • bp.hit_count = <int> - Количество срабатываний breakpoint-а. Доступно для записи.
  • bp.condition = <str> - Условие для условных breakpoint. Доступно для записи.
  • .stop(self) - Метод, определяющий поведение при срабатывании останова для собственного класса, унаследованного от gdb.Breakpoint. Метод доступен для переопределения. Если метод возвращает True, то происходит останов программы на этом breakpoint, иначе - продолжается выполнение без останова. Если на одном адресе несколько breakpoint с методом stop(), каждый из них будет вызван независимо на return status предыдущего!

Класс gdb.FinishBreakpoint

[23.2.2.31 Finish Breakpoints]

По смыслу похож на GDB-команду 'finish'. Временный breakpoint ставится на адрес возврата стек-фрейма (объекта типа gdb.Frame).
Если конструктор gdb.FinishBreakpoint() без параметров - то это finish breakpoint для текущего фрейма, но можно поставить и на произвольный фрейм, для этого нужно в конструктор передать параметром объект типа gdb.Frame (gdb.FinishBreakpoint(frame)).
Finish breakpoint удаляется сам после срабатывания или при выходе из области видимости (например при execution; при выполнении gdb-команды 'return' и др.).

FinishBreakpoint.return_value - атрибут, содержащий возвращаемое значение функции/метода в виде объекта типа gdb.Value. Недоступно для записи.

Finish breakpoint по дефолту не работает для main(). Чтобы заработало для main() необходимо:

  • Выполнить set backtrace past-main on
  • В startup.S должен быть nop после возврата из main(), перед trap.

События (Events)

[23.2.2.17 Events In Python]

Event - объект, описывающий некоторое изменение состояния. Тип объекта и его атрибуты сильно зависят от особенностей изменения состояния.
Нужно зарегистрировать обработчик события: EventRegistry.connect(handler).

Перешагивание тела функции/метода

При отработке программы может возникнуть необходимость перешагнуть тело функции/метода (например драйвера).
Для перешагивания можно попробовать использовать GDB-команду return retval.
А можно реализовать перешагивание с помощью events.stop (как в ovlymgr_imit). В начале каждой функции/метода, тело которой нужно перешагнуть, поставить breakpoint. При этом должен быть общий обработчик (например Python-функция), который где-то в начале нужно зарегистрировать: gdb.events.stop.connect(fn_stop_event_handler). Общий обработчик будет вызван при срабатывании каждого breakpoint. Внутри общего обработчика, в зависимости от того, какой именно breakpoint сейчас сработал, нужно вызвать индивидуальный обработчик: если текущий breakpoint содержится в списке breakpoint-ов, предназначенных для перешагивания, то вызвать индивидуальный обработчик, иначе выйти из общего обработчика. Информация о breakpoint передаётся в общий обработчик через его параметр, объектом типа gdb.BreakpointEvent. [23.2.2.17 Events In Python]


Реализация собственной GDB-команды

[GDB doc: 23.2.2.20 Commands In Python]

Есть возможность реализовать собственную GDB-CLI-команду с помощью класса в Python. Собственная команда оформляется в виде Python-класса. Класс должен быть унаследован от gdb.Command (пример в п.23.2.2.20 Commands In Python).
Текст справки по команде оформляется в виде Python-документирующего комментария к классу. Текст справки будет показан при выполнении (gdb) help <cmd>.

Метод .invoke(argument, from_tty) - основной метод, определяющий поведение команды, метод вызывается при вызове GDB-команды.
Параметр argument - это строка, содержащая аргумент GDB-команды (то, что содержится после пробела после команды: (gdb)<cmd> ...). Для преобразования строку argument в argv-подобную строку рекомендуется использовать gdb.string_to_argv('...').
Параметр from_tty - это признак bool. Если True - значит команда была введена пользователем в терминале; если False - команда пришла откуда-то ещё.
Если метод .invoke(argument, from_tty) бросит exception, он преобразуется в gdb.error. Иначе возвращаемое значение игнорируется. [23.2.2.20 Commands In Python]
Если в методе .invoke(argument, from_tty) необходимо бросить exception, то это нужно делать с помощью gdb.GdbError. [23.2.2.2 Exception Handling]

Пример реализации команды:

В мануале [23.2.2.20 Commands In Python] рекомендуется в классе GDB-команды определять метод .invoke(...) как метод экземпляра объекта:

    def invoke(self, argument, from_tty):
        ...

При этом вызов этой GDB-команды из python-кода выглядит не очень красиво:

    session = BeginSession()
    session.invoke(elf, False)
        или
    BeginSession.invoke('begin', elf, False)

Но можно определить метод .invoke(...) как метод класса:

    @classmethod
    def invoke(cls, argument, from_tty):
        ...

И в этом случае вызов этой GDB-команды из python-кода выглядит лучше (без создания экземпляра объекта):

    BeginSession.invoke(elf, False).

Но метод класса можно использовать только если не нужно менять состояние экземпляра объекта.

В любом случае из терминала эту GDB-команду можно выполнить так:

    (gdb) begin <path-to-elf>

Реализация собственной GDB-функции

[23.2.2.22 Writing new convenience functions]

Собственная GDB-функция (convenience function). Похоже на собственную GDB-CLI-команду, только функция, то есть принимает параметры и возвращает результат и в GDB CLI используются скобки.
Также похожа на Convenience Vars.
Собственная GDB-функция оформляется в виде Python-класса. Класс должен быть унаследован от gdb.Function (пример в п.23.2.2.22 Writing new convenience functions).

Собственные GDB-команды и GDB-функции

[23.2.2.20 Commands In Python]
[23.2.2.22 Writing new convenience functions]

Реализации собственных GDB-команд и GDB-функций содержатся в /modules/.
GDB-команды и GDB-функции доступны для использования и в GDB-консоле (GDB CLI) и в python-GDB-скрипте.
Для вызова справки по конкретной команде, нужно в GDB-консоле выполнить: help <command>.

Использование в GDB-консоле как у обычных GDB-команд - команда и через пробел обязательные и необязательные аргументы (если есть). Пример: command arg1 arg2.
Использование в python-коде - у класса реализовывающего команду вызвать метод .invoke(...). У метода .invoke(cls, argument, from_tty) параметр argument<str> - это аргументы команды, переданные в виде строки; параметр from_tty<bool> при вызове из python-кода должен быть False. Пример: CommandClass.invoke('arg1, arg2', False).

Каждая GDB-команда и GDB-функция реализуется как отдельный python-класс, и при использовании в python-GDB-скрипте подключается из соответствующего python-модуля (файла) из /modules/.

Модуль gdb_connection

Команда begin - класс BeginSession:
Выполнение начальных действий при запуске GDB-сессии.
Аргументы: path-to-elf-file
GDB CLI: begin elf-file
Python-code: BeginSession.invoke('path-to-elf-file', False)

Команда shutdown - класс Shutdown:
Выполнение действий для завершения GDB-сессии.
shutdown
Без аргументов.

Команда output - класс Output:
Вывод жирного цветного текста.
output message_type message_text
message_type = "Ok:" | "Err:" | "Warn:" | "Info:"
Пример: output Warn: Something is wrong!

Модуль memory

Команда dumpmem - класс DumpMemory:
Дамп данных из памяти в файл (обёртка над GDB-командой dump memory).
dumpmem filename start_addr end_addr

Команда appmem - класс AppendMemory:
Добавление данных из памяти в файл (обёртка над GDB-командой append memory).
appmem filename start_addr end_addr

Команда restoremem - класс RestoreMemory:
Запись в память из файла (обёртка над GDB-командой restore).
restoremem filename addr [start_offset] [end_offset]

Команда rmem - класс ReadMemory:
Чтение данных из памяти и сохранение в файл.
rmem filename addr length
(addr - это символ)

Команда wmem - класс WriteMemory:
Чтение данных из файла и сохранение в память.
wmem filename addr [length]
(addr - это символ)

Модуль call_method

Функция (convenience function) $call_method(...) - класс CallMethod:
Вызывает C++ метод/функцию и возвращает его возвращаемое значение. В качестве аргумента принимается выражение.
GDB CLI: print $call_method(obj.methodName(...))
Python-code: CallMethod.invoke('obj.methodName(...)')

Модуль profile

Команда prof - классы EndPoint и StartPoint:
Профилирование функции/метода с использованием gdb.FinishBreakpoint после gdb.Breakpoint. (Сделано по примеру Python interpreter in GNU Debugger). Работает и для методов объекта и для static-методов.
При создании EndPoint: EndPoint(self, internal=True) используется параметр internal=True конструктора класса gdb.Breakpoint, при этом на останове на EndPoint не происходит вывод информации о сработавшем breakpoint (удобно!).
Проблема!: Время работы функции/метода считается как дельта значений mcycle, но так как при останове mcycle продолжает считаться, то замеры времени получается вообще неправильным.
Но можно реализовать tracepoint похожим способом (Python interpreter in GNU Debugger).
GDB CLI: prof methodName
Python-code: Profile.invoke('methodName', False)


Xmethod

[23.2.2.13 Xmethods In Python]
[23.2.2.14 Xmethod API]
[23.2.2.15 Writing an Xmethod]

Xmethod - это средство, предоставляющее возможность добавлять новые методы или заменять существующие методы C++ класса.
Xmethod удобен в тех случаях, когда метод, определённый в C++ коде становится недоступен для GDB: например метод встраивается (inlined) или выбрасывается компилятором при оптимизации.
GDB может исполнить Xmethod вместо реального C++ метода для отработки С++ выражений. Xmethod ещё может быть использован для работы с core-файлами. Кроме этого, Xmethod можно использовать при отладке life-программ, чтобы не вызывать настоящие методы (приводящие к изменению состояния системы.)

У Xmethod есть понятия matcher и worker (py-классы). Чтобы реализовать Xmethod, нужно реализовать его matcher и соответствующий worker (можно сделать несколько worker, при этом каждый worker будет обслуживать разные перегруженные экземпляры метода).
GDB исполняет метод match py-класса matcher чтобы сопоставить тип C++ класса и имя C++ метода. Метод match возвращает соответствующих worker-объектов. Каждый worker-объект соответствует перегруженному экземпляру Xmethod. Каждый worker содержит реализацию py-метода get_arg_types(), который возвращает последовательность типов, соответствующих аргументам Xmethod. GDB использует эту последовательность типов для сравнения перегруженных вариантов и выбора подходящего Xmethod worker. Также выбирается подходящий среди С++ методов, найденных с помощью GDB. Затем подходящий Xmethod worker и подходящий C++ метод сравниваются для выбора итогового подходящего во всём. В случае, если xmethod worker и C++ метод соответствуют, то xmethod worker выбирается итоговым подходящим.
То есть, если подходящий xmethod worker выбирается в качестве эквивалента подходящего C++ метода, то xmethod worker выполняется взамен C++ метода. В этом случае соответствующий Xmethod исполняется через метод __call__(...) объекта worker.

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

Xmethod matcher должен быть экземпляром класса, наследованного от класса gdb.xmethod.XMethodMatcher.
Атрибуты и методы класса gdb.xmethod.XMethodMatcher:

  • name - Имя matcher.
  • enabled - Признак того, что matcher включен/отключен.
  • methods - Список методов, формируемый в matcher. Каждый объект в списке - экземпляр класса Xmethod (а точнее, экземпляр класса, наследованного от Xmethod). Каждый объект содержит:
    • name - Имя xmethod. Имя должно быть уникальным для каждого xmethod, которыми управляет matcher.
    • enabled - Признак того, что xmethod включен/отключен.
    • XMethod.__init__(self, name) - Конструктор, создающий enabled xmethod с именем name.
  • __init__(self, name) - Создание enabled matcher с именем name. Атрибут methods инициализирован как None.
  • match(self, class_type, method_name) - Наследованные классы должны переопределить этот метод. Должен возвращать объект worker (список worker объектов) с соответствующими class_type и method_name. class_type - имеет тип class_type, method_name - это строка. Если matcher организует методы как список в атрибуте methods, то возвращены должны быть только те worker объекты, у которых соответствуют записям в списке methods разрешены.

Xmethod worker должен быть экземпляром класса, наследованного от класса gdb.xmethod.XMethodWorker. Методы класса gdb.xmethod.XMethodWorker:

  • get_arg_types(self) - Метод возвращает последовательность объектов gdb.Type, соответствующую аргументам, которые принимает xmethod. Может возвращать пустую последовательность или None, если xmethod не принимает аргументы. Если xmethod принимает единственный аргумент, то может возвращать единственный объект gdb.Type.
  • get_result_type(self,*args) - Метод возвращает объект gdb.Type соответвующий типу результата выполнения xmethod. args - такой же tuple аргументов, как тот, который передаётся в метод __call__(...) этого worker.
  • __call__(self,*args) - Это метод, в котором реализуется непосредственно логика работы xmethod. args - это tuple аргументов xmethod. Каждый элемент этого tuple - объект gdb.Value. Первый элемент - всегда значение по указателю this.

Чтобы GDB нашёл xmethod-ы, xmethod matcher-ы должны быть зарегистрированы с использованием функции gdb.xmethod.register_xmethod_matcher(...):
gdb.xmethod.register_xmethod_matcher(locus, matcher, replace=False) - После регистрации matcher перемещается в область locus, заменяя существующий matcher с таким же именем, как matcher, если replace=True. locus может быть объектом gdb.Objfile или объектом gdb.Progspace или None. Если None, то matcher регистрируется глобально.

Xmethod можно использовать для имитации чего-либо (например для имитации драйвера отсутствующего устройства).

Xmethod работает для методов существующего C++ объекта.
В worker->__call__(...) считать поле (в том числе и static) можно как обычно obj['field']. Для static методов xmethod почему-то не работает: если исполнить obj.method(arg), то возникают ошибка; а если исполнить class::method(arg), то выполняется изначальный метод (не заменяется на xmethod).

Unit-тесты: Python-unittest + Python-GDB-API

Идея в том, чтобы реализовать unit-тестирование C++ методов с помощью стандартного Python-фреймворка 'unittest' совместно с Python-GDB-API.

Python-unittest

Файл с unit-тестами передаётся GDB-клиенту так же, как обычный Python GDB script. Например при запуске GDB-клиента - через параметр '-x ...'.

Использование стандартного Python модуля unittest:
В отдельном файле (тест-модуле): импортировать модуль unittest и проверяемый класс (из его модуля).
Определить тест-класс. Тест-кейсом считается весь тест-класс, наследованный от unittest.TestCase. Имя тест-класса лучше выбрать связанное с проверяемым классом, и включить в него слово 'Test'. Тест-класс должен наследоваться от unittest.TestCase.
Имя каждого тест-метода должно начинаться с 'test_'. Mетод, имя которого начинается с 'test_', будет запускаться автоматически.
Модуль unittest предоставляет множество параметров и настроек, что позволяет реализовывать unit-тесты очень гибко.

Некоторые стандартные методы, определённые в классе unittest.TestCase:

  • setUp() - Содержит код инициализации. Запускается перед каждым тест-методом. Доступен для переопределения. Каждый из объектов, созданный в setUp() снабжается префиксом self, поэтому может использоваться где угодно в тест-классе.
  • tearDown() - Содержит завершающий код. Запускается после каждого тест-метода. Доступен для переопределения.
  • setUpClass(cls) - Метод класса (@classmethod). Запускается разово для тест-класса в начале перед всеми тест-методами.
  • tearDownClass(cls) - Метод класса (@classmethod). Запускается разово для тест-класса в конце после всех тест-методов.
  • setUpModule() - Функция модуля (не относится ни к одному классу модуля). Запускается перед всеми классами модуля.
  • tearDownModule() - Функция модуля (не относится ни к одному классу модуля). Запускается после всех классов модуля.

Запуск всех тестов из файла(тест-модуля):

    python ./test_module.py

Запуск отдельного тест-модуля:

    python -m unittest test_module

Запуск отдельного тест-класса:

    python -m unittest test_module.TestClass

Запуск отдельного тест-метода:

    python -m unittest test_module.TestClass.test_method

Можно запускать с ключём -v для подробного вывода прохождения тестов:

    python -m unittest test_module -v
    python -m unittest test_module.TestClass -v

Запуск с параметром unittest.main(exit=False ...): В конце, после завершения всех тестов unittest не выполняется sys.exit(), что может быть необходимо в некоторых случаях. В частности, в использовании unittest + Python GDB API после завершения работы unittest необходимо остановить GDB-server(OpenOCD) и сам GDB-client.

Запуск с параметром -c/--catch или unittest.main(catchbreak=True ...): При прерывании выполнения тестов unittest через Ctrl+C - текущий тест-метод выполнится до конца, после чего тестовый прогон завершится. Отчёт будет содержать результаты тестов, проведённых на данный момент.

Запуск с параметром unittest.main(verbosity=2 ...): Подробный вывод: Название и описание каждого тест-метода и результат прохождения.
Краткий вывод (включён по умолчанию): Во время работы тестового сценария выводится один символ для каждого модульного теста после его завершения. Для успешно прошедшего теста выводится точка. Если при выполнении произошла ошибка, выводится символ E, а если не прошла проверка условия assert, выводится символ F.

Отдельные тест-методы и тест-классы можно пропускать, для этого используется декоратор @unittest.skip('Skipping message') перед тест-методом или перед тест-классом. При выполнении, такие тесты будут обозначены буквой 's'.

Пример оформления тест-модуля:

    import unittest
    #from Module import UserClass
    
    def setUpModule():
    '''For all module'''
    pass
    
    def tearDownModule():
    '''For all module'''
    pass
    
    class TestUserClass(unittest.TestCase):
        """Тесты для ..."""
        
        @classmethod
        def setUpClass(cls):
            pass
        
        def setUp(self):
            pass
        
        def test_0(self):
            """Описание теста test_0"""
            pass
            self.assertEqual(True, True)
        
        def test_1(self):
            """Описание теста test_1"""
            pass
            self.assertEqual(True, True)
        
        @classmethod
        def tearDownClass(cls):
            pass
    
    if __name__ == "__main__":
        unittest.main(exit=False, verbosity=2, catchbreak=True)

Unit-тесты C++ методов

Идея была в том, чтобы средствами Python-GDB-API в целевом C++ проекте отрабатывать классы, но при этом не собирать отдельный проект с тест-кейсами (как в gtest). Очевидно, что без создания экземпляра, методы класса вызвать невозможно (Class::method(...)). И, так как идея была обойтись без пересборки проекта, то нужно вручную создать объект класса - средствами GDB вызвать конструктор. Но чтобы создавать объекты (и фреймы) должен быть настроен стек (sp), простой способ настроить стек - войти в main() (и остановиться в начале main()).
Вызвать конструктор возможно, объект класса создастся (в текущем месте стека). Но обратиться к созданному объекту по имени не получиться, поэтому вызывать методы можно по адресу класса с приведением типа. [17.5.1 Calling functions with no debug info], [15.4.1.3 C++ Expressions] Но если объект в C++ коде нигде не создавался (не вызывался конструктор), то в elf в секции с классом почему-то отсутствует конструктор.

Вызов конструктра:

    (gdb) p ExampleClass::ExampleClass(void)
    $5 = {void (ExampleClass * const)} 0x100002c8 <ExampleClass::ExampleClass()>

Затем вызов метода объекта:

    (gdb) p ((ExampleClass*)0x100002c8)->computeFactorial(4)
    $6 = 24

Так как объект класса не создан в C++ коде, то и gdb.parse_and_eval('...') не срабатывает. Но gdb.parse_and_eval('...') срабатывает в зоне видимости объекта даже если конструктор ещё не вызван (Опасно!).

Идея была такая: не просто создать объект в текущем месте стека (вызвать конструктор средствами GDB), а сделать этот объект в виде объекта Python GDB API - gdb.Value, чтобы иметь возможность обращаться к его полям gdb.Value['field']. Объект gdb.Value должен быть типа gdb.Type (gdb.Type должен соответствовать C++ классу).
Было несколько вариантов, как сделать объект gdb.Value типа gdb.Type:

  • Скастить gdb.Value, получившийся после вызова конструктора к соответствующему типу gdb.Type (== ExampleClass). Но из-за того, что получившийся gdb.Value имел адрес конструктора, а не адрес объекта в стеке, то значения полей объекта получились неправильными.

  • Скастить gdb.Value (== sp) к указателю на gdb.Type (то есть * ExampleClass), то есть чтобы получился gdb.Value с правильным адресом (адрес объекта в стеке). Но, видимо из-за того, что стек настроен неправильно, значения полей объекта получились неправильными (== 0).

  • Создать gdb.Value из gdb.Frame. Нет таких методов. gdb.Frame не предназначен для модификации.

  • Использовать Symbol.value(). Не работает для символа класса:

      cls_symb = gdb.lookup_global_symbol('ExampleClass')
      cls_symb.value(gdb.selected_frame())    #TypeError: cannot get the value of a typedef
    
  • Использовать конструктор gdb.Value(val, type). При этом val должен содержать Python buffer object. Но, видимо из-за того, что стек настроен неправильно, значения полей объекта получились неправильными (== 0).

Но всё оказалось сложнее! В C++ программе перед вызовом конструктора есть еще создание локального объекта. Сначала на стеке выделяется место под объект, затем вызывается конструктор. Если просто принудительно вызвать конструктор с помощью GDB, то стек (память и fp) будет не в том состоянии и, соответственно будут неправильные (не сформированы) адреса в фрейме. Из-за этого, при вызове конструктора объект не создастся или создастся неправильно, в частности получаются неправильные адреса полей. В конструкторе берутся адреса, хранящиеся в стек-фрейме (видимо это адреса полей локального объекта) и инициализируются начальным значением полей. Но так как без создания локального объекта эти адреса не сформированы, то поля инициализируются неправильно (== 0). То есть, недостаточно просто вызвать конструктор, нужно перед этим подготовить место в стеке под объект. Это тоже самое, что вызывать функцию, не выделив место под её возвращаемое значение.

Так что такой подход (создать объект просто вызвав конструктор) оказался неработоспособным. Но в любом случае, из python-кода невозможно напрямую вызывать методы C++ объекта, представленного в виде gdb.Value.

Однако методы класса вызывать можно, при этом если метод возвращает какой-нибудь простейший тип (bool, int, int*, и др.), то проблем быть не должно. Но неизвестно что будет, если метод возвращает что-нибудь более сложное, требующее выделения памяти в месте вызова. Методы класса можно вызывать по адресу их расположения в .text:

    result = CallMethod.invoke('((ExampleClass*)0x100002c8)->computeFactorial(4)')

или:

    TestExampleObj = CallMethod.invoke('ExampleClass::ExampleClass(void)')  #Вызов конструктора
    ref_val = TestExampleObj.referenced_value()
    result = CallMethod.invoke(f'((ExampleClass*){int(ref_val.address)})->computeFactorial(4)')

или (лучше вызывать так):

    print(gdb.parse_and_eval('((ExampleClass*)0x100002c8)->computeFactorial(4)'))   #24

Exception

Если при исполнении тест-метода unittest в C++ программе возникает Exception, то целесообразно пропустить (skip) остальные тесты unittest. Для этого в setUp() (то есть перед каждым тест-методом) проверять, не было ли exception ранее (на предыдущих тестах), и если exception был, то вызывать self.skipTest('...'). Понять, что exception был можно например если pc == адресу обработчика.


Дополнительно

Двумерный массив

Чтение двумерного массива:

    (gdb) p/x *arr2@len
    (gdb) p/x *((int(*)[2][4])arr2)

Чтение одномерного массива (по аналогии):

    (gdb) p/x *arr1@len
    (gdb) p/x *((int(*)[4])arr1)

Запись в элемент двумерного массива:

    (gdb) set *(*(arr2 + i) + j) = ...

Profiling

Использование gprof [gprof.pdf]:
Скомпилировать и слинковать целевую программу с ключём -pg (для этого требуется glibc). Можно скомпилить только отдельные модули (объектники).
При этом компилятор добавляет в преамбулы и постамбулы код для замеров времени, используется специальная библиотека.
Программа запускается как обычно, с обычными аргументами. При заврешении работы, программа сама сформирует/перезапишет файл gmon.out в рбочем каталоге, с результатами профилирования.
Запуск gprof: gprof options [executable-file [profile-data-files ...]] [> outfile ]
Можно вывести в том числе и call graph: gprof опция -c.

Использование gprof требует пересборки целевой программы и добавлением специального кода, что накладывает дополнительный overhead (и из-за пересборки сама программа становится другой).
Результаты профилирования (файл gmon.out) можно получить и с помощью GDB (команда maint set profile ...), при этом целевая программа должна быть также собрана с ключём -pg.
Для профилирования (и отслеживания памяти) без пересборки можно использовать инструмент Valgring (работает под Linux).
Но эти варианты не подходят для профилирования на bare metal embedded.

Coverage

GDB команда "maint set dwarf unwinders" похоже, ни на что не влияет. Этот режим итак включен постоянно. [23.2.2.12 Unwinding Frames] Unwinding - нахождение предыдущего (caller) frame. Можно реализовать свой frame unwinder.

Использование gcov [gcc.pdf. 10.gcov - a Test Coverage Program]:
gcov - инструмент для определения покрытия кода (и покрытия branch), используется только совместно с gcc. Можно использовать совместно с gprof. Целевая программа должна быть скомпилирована с '-fprofile-arcs -ftest-coverage' (для этого требуется glibc) и без оптимизации: компилятор сгенерирует дополнитульную информацию для определения покрытия.
После сборки появится дополнительные файлы. При запуске программы промежуточные результаты накапливаются в .gcda (и .gcno).
После запуска gcov 'gcov sourcefile.c' будет сформирован sourcefile.gcov, содержащий результаты покрытия.

Использование gcov требует пересборки целевой программы и добавлением специального кода, что накладывает дополнительный overhead (и из-за пересборки сама программа становится другой). Для такой сборки программы необходима glib (то есть ОС).
Такой вариант не подходит для применения на bare metal embedded.

Tracing

[7.Recording execution: record btrace]
[23.2.2.19. Recordings in Python]

Специальный режим записи (воспроизведения) для процессов. Должна быть поддержка со стороны платформы. Скорее всего для embedded не подходит.


Ссылки

  1. RefCard - GDB Quick Reference
  2. GDB doc: 23.2 Extending gdb using Python
  3. Python Interpreter in GNU Debugger
  4. Automate Debugging with GDB Python API
  5. The GDB Python API
  6. Metal.GDB: Controlling GDB through Python Scripts with the GDB Python API
  7. How to access the keys or values of Python GDB Value
  8. Change struct field in GDB using a gdb.value
  9. gdb python api: is it possible to make a call to a class/struct method
  10. How to call constructor in gdb for pretty-printers
  11. Writing a new gdb command
  12. gdb convenience functions