gdb-дуэль — списки, деревья и хэш таблицы против командной строки

Habrahabr 1

Первый раз я увидел команду duel в gdb на каком-то древнем IRIX-е, лет пятнадцать назад. Это была невероятно крутая штука для просмотра разных связанных списков, массивов структур, и прочих подобных конструкций. Помечтал, мол, если б в Линуксе такая была, и забыл. Лет десять назад вспомнил, погуглил — оказалось, что DUEL, это вообще-то патч 93-го года для gdb 4.6, а вовсе не что-то уникальное в IRIX. Только автор по идейным соображениям выпустил его как public domain, а gdb-шники были тоже идейные и хотели GPL, так что попасть в upstream этому патчу не грозило. Я портировал его на тогдашний gdb 6.6.2, отдал в gentoo и до выхода 7-го gdb наслаждался жизнью. Потом duel из gentoo выкинули, портировать на новый gdb было сложно, никто не взялся. А недавно я его попробовал оживить. Только вместо патча (надо собирать вместе с gdb из исходников, использует всякие внутренние gdb-шные функции) я его написал с нуля на питоне. Теперь Duel.py (так называется новая реализация Duel-а) грузится в gdb на лету, и, надеюсь, Python API не будет меняться от версии к версии так, как недокументированные gdb-шные потроха. Итак, встречайте: DUEL — высокоуровневый язык анализа данных для gdb.

Примеры

Сразу, чтоб показать, на что он способен:
(gdb) dl table_list-->next_local->table_name
tables->table_name = 0x7fffc40126b8 "t2"
tables->next_local->table_name = 0x7fffc4012d18 "t1"
tables-->next_local[[2]]->table_name = 0x7fffc4013388 "t1"

Это из отладки MariaDB. Команда проходит односвязный список структур TABLE_LIST и для каждого элемента списка выводит TABLE_LIST::table_name.
(gdb) dl longopts[0..].name @0
longopts[0].name = "help"
longopts[1].name = "allow-suspicious-udfs"
longopts[2].name = "ansi"
<... cut ...>
longopts[403].name = "session_track_schema"
longopts[404].name = "session_track_transaction_info"
longopts[405].name = "session_track_state_change"

Оттуда же (я вырезал адреса, чтоб не захламлять текст). Есть массив структур, задающий опции командной строки. Команда выводит только имена опций, проходя весь массив до name == 0. А можно просто посчитать, сколько их:
(gdb) dl #/longopts[0..].name @0
#/longopts[0..].name@0 = 406

Основная идея

Duel построен на том, что выражение может возвращать много значений. Например,
(gdb) dl 1..4
1 = 1
2 = 2
3 = 3
4 = 4

или вот
(gdb) dl my_long_options[1..4].(name,def_value)
my_long_options[1].(name) = "allow-suspicious-udfs"
my_long_options[1].(def_value) = 0
my_long_options[2].(name) = "ansi"
my_long_options[2].(def_value) = 0
my_long_options[3].(name) = "autocommit"
my_long_options[3].(def_value) = 1
my_long_options[4].(name) = "bind-address"
my_long_options[4].(def_value) = 0

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

Операторы

Синтаксис похож на С, и C-шные операторы работают, как обычно. Гораздо интереснее, новые, специфичные для DUEL, операторы. Рассмотрим самые полезные из них:

Диапазон и перечисление, .. и ,

Выше я приводил пример обоих операторов. Это знакомые конструкции, они есть и в других языках. При этом в диапазоне можно опустить один из концов. Если указать только конец диапазона, например, ..20, то диапазон начнется с нуля и в нем будет 20 значений, так же, как если бы было написано 0..19. Если же указать только начало, то получится открытый диапазон! Чтобы duel не продолжал генерировать числа до тепловой смерти вселенной (или до переполнения счетчика, смотря что случится раньше), вместе с открытым диапазоном обычно используют оператор остановки по условию, @.

Остановка по условию, @

В выражении x@y, выражение x будет генерировать значения до тех пор, пока y ложно. Например,
(gdb) dl arr[0..]@(count > 10)

И duel будет выводить элементы массива arr[] до тех пор, пока arr[i].count будет не больше десяти.

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

(gdb) dl str[0..]@0

вернет все символы строки, вплоть до '\0'. Более практичный пример — вывести все опции командной строки из argv:
(gdb) dl argv[0..]@0
argv[0] = "./mysqld"
argv[1] = "--log-output=file"
argv[2] = "--gdb"
argv[3] = "--core-file"

Хотя тот же эффект достигается и

(gdb) dl argv[..argc]
argv[0] = "./mysqld"
argv[1] = "--log-output=file"
argv[2] = "--gdb"
argv[3] = "--core-file"

Перейти по указателю, -->

Генератор a-->b порождает множество значений a, a->b, a->b->b, и так далее, пока не уткнется в NULL. Я уже приводил пример, как таким образом можно пройтись по односвязному списку. Но это точно так же работает и для деревьев, например:
(gdb) dl tree-->(left,right)->info

Вычисляющие скобки {}

Фигурные скобки работают как обычные круглые, но они дополнительно заменяют выражение его значением при выводе. Проще показать на примере:
(gdb) dl i:=5
i = 5
(gdb) dl i+6
i+6 = 11
(gdb) dl {i}+6
5+6 = 11
(gdb) dl {i+6}
11 = 11

Это, в основном, нужно для массивов:
(gdb) dl if (my_long_options[i:=1..20].name[0] == 'd') my_long_options[i].name
if(my_long_options[i].name[0] == 'd') my_long_options[i].name = "debug-abort-slave-event-count"
if(my_long_options[i].name[0] == 'd') my_long_options[i].name = "debug-assert-on-error"
if(my_long_options[i].name[0] == 'd') my_long_options[i].name = "debug-assert-if-crashed-table"
if(my_long_options[i].name[0] == 'd') my_long_options[i].name = "debug-disconnect-slave-event-count"
if(my_long_options[i].name[0] == 'd') my_long_options[i].name = "debug-exit-info"
(gdb) dl if (my_long_options[i:=1..20].name[0] == 'd') my_long_options[{i}].name 
if(my_long_options[i].name[0] == 'd') my_long_options[16].name = "debug-abort-slave-event-count"
if(my_long_options[i].name[0] == 'd') my_long_options[17].name = "debug-assert-on-error"
if(my_long_options[i].name[0] == 'd') my_long_options[18].name = "debug-assert-if-crashed-table"
if(my_long_options[i].name[0] == 'd') my_long_options[19].name = "debug-disconnect-slave-event-count"
if(my_long_options[i].name[0] == 'd') my_long_options[20].name = "debug-exit-info"

Тут фигурные скобки сразу показывают, какие элементы массива удовлетворяют условию.

Фильтры <? >? <=? >=? ==? !=?

Эти вариации на тему операторов сравнения фактически работают как фильтры. То есть в x !=? y, из множества значений x выбираются только те, которые не равны y. Выше был пример с условным оператором if. С фильтром такой же результат получается проще:
(gdb) dl my_long_options[1..20].(name[0] ==? 'd' => name)
my_long_options[16].(name) = "debug-abort-slave-event-count"
my_long_options[17].(name) = "debug-assert-on-error"
my_long_options[18].(name) = "debug-assert-if-crashed-table"
my_long_options[19].(name) = "debug-disconnect-slave-event-count"
my_long_options[20].(name) = "debug-exit-info"

Еще операторы

Кроме того есть алиасы, групповые операторы, условный оператор (if) и прочие редко нужные штуки.

Advanced

Вот еще несколько примеров из документации.

Пройтись по циклическому списку (начинаем с head и идем по указателям, пока опять не дойдем до head):

(gdb) head-->(next!=?head)

Найти второй положительный элемент в массиве x[]. Оператор [[ ]] выбирает элементы из последовательности:

(gdb) dl (x[0..] >? 0)[[2]]

Найти последний элемент в односвязном списке. Тут используется унарный оператор #/, который возвращает количество элементов в последовательности, и оператор выбора [[ ]]:

(gdb) head-->next[[#/head-->next - 1]]

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

(gdb) dl x[i:=..100] >? x[i+1]

Еще раз, самое главное

А самое главное вот что — duel в gdb работает (опять). Он дико удобен при отладке чего-то, сложнее чем hello world. Для нормального использования практически достаточно четырех конструкций — две точки, запятая, длинная стрелка --> и @0.

Взять можно у меня в репозитории: github.com/vuvova/gdb-tools Disclaimer (отмазка): хотя сам DUEL — весьма почтенный и проверенный временем интерпретатор, Duel.py совсем новый, наверняка есть баги.