Первый раз я увидел команду 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 совсем новый, наверняка есть баги.
Поделиться с друзьями
-->

Комментарии (16)


  1. monah_tuk
    14.06.2017 15:50

    Отличная штука. Но меня больше волнует, что будет с отладкой кода со всякими корутинами… Уже даже реализация в clang есть.


    1. petropavel
      14.06.2017 16:31

      Ничего страшного не будет. Duel.py использует официальный gdb python API, то есть gdb.lookup_symbol() и все такое. Все проверки доступности переменных и правильности адресов — на стороне gdb. Если что, выдаст ошибку, мол invalid address или unknown variable. Так же, как если б этот адрес или переменную использовать в любой другой команде gdb, скажем в print.


  1. DustCn
    14.06.2017 15:55

    Классная штука! Обычно я пишу небольшой тест и работаю с задампленными данными. В интерактиве редко получается, в основном batch-mode. Но галочку поставил, никогда не знаешь где инструмент пригодится :)
    Кстати — если исходник скажем фортран, это будет работать?


    1. petropavel
      14.06.2017 16:33

      Должно, конечно (если gdb правильно фортрановские переменные и массивы в питон транслирует). Но синтаксис выражений останется С-шный :)


  1. boov
    14.06.2017 17:43

    Подскажите касательно gdb. Как с помощью него можно найти все адреса, содержащие некоторый magic. Удобно для поиска тэгированных объектов в дампах. В windbg это делается, например, так "s -d 0 L?-1 0xdeadbeef".


    1. petropavel
      14.06.2017 17:55

      Никогда не пользовался. Но есть же help:

      (gdb) h find
      Search memory for a sequence of bytes.
      Usage:
      find [/size-char] [/max-count] start-address, end-address, expr1 [, expr2 ...]
      find [/size-char] [/max-count] start-address, +length, expr1 [, expr2 ...]
      size-char is one of b,h,w,g for 8,16,32,64 bit values respectively,
      and if not specified the size is taken from the type of the expression
      in the current language.
      Note that this means for example that in the case of C-like languages
      a search for an untyped 0x42 will search for "(int) 0x42"
      which is typically four bytes.
      
      The address of the last match is stored as the value of "$_".
      Convenience variable "$numfound" is set to the number of matches.
      


    1. boov
      14.06.2017 19:49

      Да, find'ом пробовал, но он спотыкается на первом же inaccessible адресе. Думал, есть какое-то средство из коробки для такого действия. На данный момент я вижу только такой способ: получать список замапленных регионов и искать find'ом в каждом из них.


  1. aamonster
    14.06.2017 21:18

    Выглядит полезным. На lldb втянуть трудно?


    1. petropavel
      14.06.2017 21:35

      Честно говоря, не смотрел. В lldb тоже есть какой-то Python API, но на gdb-шный он, естественно, не похож. Тем не менее, основные вещи там, вроде, похожи — ну, есть symbols, у них есть values, и эти values поддерживают обычные питоновские операторы. То есть, кажется, что нетрудно. только заменить (а лучше абстрагировать и прислать мне pull request) несколько методов из gdb.* — такие как write, lookup_symbol, lookup_global_symbol, gdb.error (это исключение, что gdb бросает), ну, может, еще что-то. Немного.


  1. degs
    20.06.2017 17:37

    Очень полезная штука.
    Только почему у меня выдает: "'gdb.Value' object cannot be interpreted as an integer" на любой оператор… ?


    1. petropavel
      20.06.2017 17:48

      Совсем на любой?
      * пример, пожалуйста
      * какая версия duel-а?
      * какая версия gdb и питона?


      1. degs
        20.06.2017 17:55

        Да нет же) operator (..)
        (gdb) dl(1,5)
        (1) = 1
        (5) = 5
        (gdb) dl (1..5)
        'gdb.Value' object cannot be interpreted as an integer


        GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
        Python 2.7.6
        duel прямо с гитхаба, только что сгрузил


        1. petropavel
          20.06.2017 18:42

          Может быть слишком старый gdb. На 7.10 у меня все работало. Проверьте вот такое:

          (gdb) pi import sys
          (gdb) pi sys.version
          '2.7.12 (default, Dec 19 2016, 22:50:06) \n[GCC 4.9.3]'
          (gdb) pi dir(gdb.Value(1))
          [ ... '__int__', ... '__long__', ... ]
          (gdb) pi range(gdb.Value(1), gdb.Value(5))
          [1, 2, 3, 4]
          (gdb) 
          

          То есть в методах у gdb.Value должны быть __int__ и __long__. И range от gdb.Value должен работать. Если что-то будет не так, может быть я смогу вставить костыль.


          1. degs
            20.06.2017 18:52

            (gdb) pi import sys
            (gdb) pi sys.version
            '3.4.3 (default, Nov 17 2016, 01:12:14) \n[GCC 4.8.4]'
            (gdb) pi dir(gdb.Value(1))
            ['__abs__', '__add__', '__and__', '__bool__', '__call__', '__class__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__float__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__int__', '__invert__', '__le__', '__len__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__reduce__', '__reduce_ex__', '__repr__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__xor__', 'address', 'cast', 'dereference', 'dynamic_cast', 'dynamic_type', 'fetch_lazy', 'is_lazy', 'is_optimized_out', 'lazy_string', 'referenced_value', 'reinterpret_cast', 'string', 'type']
            (gdb) pi range(gdb.Value(1), gdb.Value(5))
            Python Exception <class 'TypeError'> 'gdb.Value' object cannot be interpreted as an integer:
            Error while executing Python code.
            (gdb)

            Я так понимаю зло в буковках 3.4.3? Можете что-нибудь посоветовать? Я с питоном нмкогда к сожалению дела не имел, а использовать ваш модуль очень хочется, причем на работе, где я над версиями не властен.


            1. petropavel
              20.06.2017 20:00

              Не знаю, то ли gdb-7.7.1, то ли python 3.4.3 — но кто-то из них виноват. Ошибка странная, потому что у gdb.Value есть метод __int__, так что as integer он точно может быть interpreted.

              В общем, я нашел такую же точно убунту, эффект повторился. Я пушнул костыль, скачайте свежую версию.


              1. degs
                20.06.2017 21:28

                Работает, спасибо. Буду пользоваться и вспоминать добрым словом.