Полезные особенности GCC/GDB, о которых необходимо знать
kayo — Чт, 02/07/2015 - 23:31
Многие, как и я, уже очень давно пользуются набором инструментов разработчика GCC и отладчиком GDB, однако, далеко не все полезные возможности этого инструментария широко известны и активно применяются нами. Постараемся восполнить пробелы. Пусть эта статья будет чем-то вроде памятки.
Расширенная отладочная информация
По-умолчанию, опция -g или -ggdb включает сборку с уровнем отладочной информации 2. С этим уровнем GDB ничего не узнает о макроопределениях, что иногда создаёт неудобства. Например, при программировании под микроконтроллеры, для доступа к регистрам периферии используется набор макросов, которые связывают адреса регистров с именами. Эти имена не будут доступны в отладчике, если использовалась опция -g или -ggdb при сборке.
Уровень 3 включает макроопределения в отладочную информацию. Достаточно просто передать -g3 или -ggdb3, чтобы просматривать и вычислять макросы во время отладки. Также становятся доступны команды работы с макросами:
# Узнаём текущее значение в регистре данных АЦП (gdb) p ADC_DR(ADC1) $7 = 1495 # Узнаём адрес регистра данных АЦП (gdb) p &ADC_DR(ADC1) $8 = (volatile uint32_t *) 0x4001244c # Смотрим, как был посчитан этот адрес (gdb) macro expand ADC_DR(ADC1) expands to: (*(volatile uint32_t *)((((0x40000000U) + 0x10000) + 0x2400) + 0x4c))
Исследовалась прошивка для ARM микроконтроллера на ядре Cortex-M3, работающая с АЦП через библиотеку libopencm3. Поскольку все регистры периферии в этой библиотеке определены с использованием макросов, то мы не смогли бы так просто узнать текущее измеренное значение в регистре данных преобразователя, если бы собрали с отладочной информацией уровня меньше третьего.
Язык сценариев GDB
Продвинутый отладчик GDB имеет встроенный язык программирования, который поддерживает переменные, выражения и управляющие конструкции. Это даёт возможность исследовать сложные моменты функционирования наших программ и устройств, на которых они работают.
Давайте на примере рассмотрим работу с переменными и наиболее часто используемые управляющие конструкции if-else-end и while-end:
# Создаём переменную и присваиваем ей значение (gdb) set $i=0 # Смотрим значение переменной командой print (gdb) p $i $1 = 0 # Увеличиваем значение переменной на единицу (gdb) set $i=$i+1 # Смотрим новое значение переменной командой print (gdb) p $i $2 = 1 # Пробуем цикл с инкрементом и выводом значения (gdb) while $i<10 set $i=$i+1 p $i end $4 = 2 $5 = 3 $6 = 4 $7 = 5 $8 = 6 $9 = 7 $10 = 8 $11 = 9 $12 = 10 # Пробуем условие (gdb) if $i >= 10 p $i else p 0 end $13 = 10
Примеры бессмысленные, поскольку в них не участвую команды отладки, такие как: step (s), next (n), continue (c). Но если их таки задействовать, можно разрабатывать довольно витиеватые отладочные алгоритмы.
Надо отметить, что с выбором имён переменных надо быть осторожным: GDB экспортирует имена аппаратных регистров в виде $<reg> переменных. Вот как можно просмотреть информацию о регистрах:
(gdb) info registers r0 0x200005cc 536872396 r1 0x5e7 1511 r2 0x9ae 2478 r3 0x200007e8 536872936 r4 0x800c3b4 134267828 r5 0x8000151 134218065 r6 0x20001e5a 536878682 r7 0x20001ef8 536878840 r8 0x20001e58 536878680 r9 0x20001e34 536878644 r10 0x20001e5a 536878682 r11 0x2 2 r12 0xffffffff -1 sp 0x20002000 0x20002000 lr 0xffffffff -1 pc 0x800a548 0x800a548 <reset_handler> xPSR 0x1000000 16777216
В сущности же наши переменные $<var> и есть регистры GDB, только не аппаратные, а виртуальные. Отладчик и сам всякий раз при вычислении печатаемых значений создаёт виртуальные регистры под номерами $1 ... $N.
Условная остановка
Команда break (b) одна из наиболее полезных и часто используемых при отладке. Она расставляет точки останова в нашем коде, позволяя контролировать состояние программы в них. Иногда возникают ситуации, что нам необходимо отладить горячий код, поставив точку останова в таком месте программы, которое выполняется очень часто. Так отлаживать программу было бы сущим кошмаром, если бы у команды break не было возможности условной остановки. Обычно у нас имеются догадки относительно того, при каких условиях исследуемая проблема имеет место быть. А значит мы можем попросить отладчик останавливать выполнение только при достижении этих условий.
Предположим, что странности в поведении некого итеративного кода начинаются только с тысячной итерации:
(gdb) break step_func if iteration > 1000
Теперь только когда счётчик итераций перевалит за тысячу, произойдёт остановка, и мы сможем исследовать состояние нашей программы. В условии можно задействовать любые переменные, встречающиеся в коде, а также переменные, созданные командой set.
Точки останова по шаблону
Точки останова можно расставить сразу на целую группу функций, используя регулярные выражения в качестве шаблонов. Это особенно полезно в объектно-ориентированном программировании для отладки некоторого класса типов, когда с ходу локализовать проблему не получается. Для использования регулярных выражений существует специальная форма команды break с префиксом r. Я разрабатывал токенайзер, вытаскивающий распознаваемые токены из потока. Каждый токен разбирается соответствующей функцией, начинающейся на tokenize_ и оканцивающейся на _token. Вот как можно поставить точки останова на все подобные функции:
(gdb) rbreak tokenize_.*_token Breakpoint 1 at 0x400a4f: file tokenizer.c, line 178. static void tokenize_false_token(tokenize_runtime *); Breakpoint 2 at 0x400a12: file tokenizer.c, line 168. static void tokenize_true_token(tokenize_runtime *); Breakpoint 3 at 0x4009d5: file tokenizer.c, line 158. static void tokenize_null_token(tokenize_runtime *); ...
Маска .* совпадает с любым количеством любых символов в том числе и с отсутствием каких-либо символов, поэтому надо быть осторожным и в некоторых случаях использовать менее фривольные шаблоны. Отладчик находит все функции, имена которых совпадают с регулярным выражением, и ставит точки останова на них.
Автовыполнение команд
Итак, продолжим исследовать нашу гипотетическую программу. На этот раз выяснилось, что некие переменные меняются странным образом на протяжении многих итераций, и переменных этих также довольно много. GDB позволяет назначить команды, которые выполняются всякий раз, когда происходит остановка. Воспользуемся этим, например так:
# Описываем набор команд для точки останова с номером 1 (gdb) command 1 Type commands for when breakpoint 1 is hit, one per line. End with a line saying just "end". >p var1 >p var2 >p varN >end
Теперь при каждой остановке мы будем видеть значения всех важных для нас переменных.
Примеры посложнее
А теперь усложним задачу. Предположим, что итеративная функция из первого примера не имеет счётчика итераций в программе, но нам необходимо исследовать значение некоторых переменных внутри неё с тысячной по тысяча сотую итерацию:
# Устанавливаем виртуальный счётчик в 0 (gdb) set $i=0 # Устанавливаем точку останова где-то внутри функции (gdb) b main.c:233 if $i >= 1000 && $i < 1100 # Добавляем команды вывода значений переменных (gdb) commands 1 Type commands for when breakpoint 1 is hit, one per line. End with a line saying just "end". >p var1 >p var2 >p varN >set $i=$i+1 >end # Продолжаем выполнение программы (gdb) c
Логгирование вывода в файл
Иногда нам необходимо собрать значения переменных в процессе работы программы для последующего анализа. В простейшем случае можно перенаправить вывод GDB в файл, но это не всегда удобно. Например, когда мы хотим выборочно сохранить некоторые значения. В GDB поддерживается логгирование. Рассмотрим на практическом примере, как оно устроено:
#!/usr/bin/env arm-none-eabi-gdb --nh --nx -x adc.gdb --batch-silent # Отлаживаем прошивку под ARM Cortex-M # (в linux такой шэбанг работать не будет, # поэтому запускаем вручную) # Подключаемся к серверу OpenOCD target remote localhost:3333 # Загружаем прошивку с отладочной информацией file firmware.elf # Выключаем логгирование set logging off # Перенаправляем лог в файл set logging file adc.dat # Запрещаем перезапись файла # Данные будут добавляться в конце set logging overwrite off # Включаем перенаправление в файл # чтобы не нагружать попусту stdout set logging redirect on # Делаем программный сброс цели monitor reset halt # Устанавливаем точку останова в функции # преобразования измеренных значений АЦП b adc_convert # Устанавливаем счётчик set $i=100 # Делаем цикл до нуля while $i>0 # Уменьшаем значение счётчика set $i=$i-1 # Продолжаем выполнение до точки останова c # Устанавливаем начальный индекс элемента массива set $n=0 # Проходим до конца массива while $n<sizeof(adc_buffer)/sizeof(adc_buffer[0]) # Делаем ссылку на текущий элемент set $e=adc_buffer[$n] # Увеличиваем индекс на единицу set $n=$n+1 # Включаем логгирование set logging on # Выводим значения АЦП буффера printf "%d %d %d %d %d %d\n", $e.Tmcu, $e.Vref, $e.Tback, $e.Vref1, $e.Tfront, $e.Vref2 # Выключаем логгирование set logging off end end EOF
Здесь засветилась ещё одна приятная команда printf, это привычный программистам на C форматированный вывод. Мы использовали здесь эту функцию, чтобы сформировать данные, пригодные для построения графиков средствами GNUPlot.
Другие возможности GDB
Осталось ещё немного важных вещей, которые все обязаны знать.
Запуск отладки программы с аргументами:
sh:~$ gdb --args program --arg1=val1 --arg2
Удобная, надо сказать, особенность.
Вывод значений
Команда print (p) может отображать числовые значения в разных форматах:
# Напечатаем число в двоичном представлении (gdb) p/t 10 $1 = 1010 # Выведем в восьмеричной системе счисления (gdb) p/o 10 $2 = 012 # Ну и конечно можно вывести в шестнадцатеричной (gdb) p/x 10 $3 = 0xa
Вот весь список возможных представлений:
- o(octal)
- x(hex)
- d(decimal)
- u(unsigned decimal)
- t(binary)
- f(float)
- a(address)
- i(instruction)
- c(char)
- s(string)
- z(hex, zero padded on the left).
Команда print удобна для просмотра как атомарных значений, так и массивов и структур, поскольку она печатает значение так, как оно могло бы быть проинициализировано в исходном коде. Но эта команда подходит не для всего, ведь зачастую нам нужно просмотреть содержимое произвольного участка памяти. Для решения этой задачи существует команда x. Эта команда помимо формата представления принимает размер ячейки и общее число ячеек, содержимое которых необходимо показать:
# Отобразим одно слово (32 бита) в 16-ричном виде (gdb) x/1wx 0x08000000 0x8000000 <vector_table>: 0x20002000 # Отобразим три слова с указанного адреса (gdb) x/3wx 0x08000000 0x8000000 <vector_table>: 0x20002000 0x0800a549 0x0800a547 # Отобразим одно слово в двоичном представлении (gdb) x/1wt 0x08000000 0x8000000 <vector_table>: 00100000000000000010000000000000 # Отобразим один байт в двоичном представлении (gdb) x/1bt 0x08000000 0x8000000 <vector_table>: 00000000
Вот полный список поддерживаемых аргументов размера:
- b(byte)
- h(halfword)
- w(word)
- g(giant, 8 bytes)
Надо отметить, что команда x в отличии от команды p запоминает последние использовавшиеся размер ячейки и формат представления, но не количество ячеек. Это удобно при работе с отладчиком из командной строки. Если не указать число выводимых элементов, то будет выведен один.
Заключение
На этом, пожалуй, всё. Подробности об этих и других возможностях хорошо описаны в документации. Удачной отладки.

Отправить комментарий