Немного мыслей о GDB/MI

GDB/MI выводит информацию в весьма структурированном виде. Например, информация от фреймах, возвращаемая командой -stack-list-frames, выглядит так:

(gdb)
-stack-list-frames
^done,stack=[frame={level="0",addr="0x0804868f",func="hello",file="hello.c",fullname="/home/sphinx/projects/gsoc/hello.c",line="6"},frame={level="1",addr="0xb7faa900",func="start_thread",from="/lib/libpthread.so.0"}]

Выглядит хорошо! Как с этим работать?

Регвырам бой!

На данный момент в gdb-mi.el используются для разбора подобных ответов используются регулярные выражения. Вот, например, выражения для обработки приведённого выше вывода команды -stack-list-frames:

(defconst gdb-stack-list-frames-regexp
"level=\"\\(.*?\\)\",addr=\"\\(.*?\\)\",func=\"\\(.*?\\)\",\
\\(?:file=\".*?\",fullname=\"\\(.*?\\)\",line=\"\\(.*?\\)\"\\|\
from=\"\\(.*?\\)\"\\)")

На ответы GDB/MI натравливается re-search-forward, после чего из них извлекаются с помощью match-string нужные значения.

Вроде бы работает, но основанный на таком подходе код:

  • слишком быстро ломается при малейших изменениях выходного формата GDB/MI, а попытки захватить с его помощью как можно больше частных случаях приводят к излишним усложнениям;

  • поддерживается без удовольствия;

  • что самое главное, ошибочен идейно, потому что использование регулярных выражений снижает весь выигрыш от общения с отладчиком при помощи машинного интерфейса.

Язык GDB/MI в общем случае не является регулярным, поэтому все методы разбора сообщений на нём с помощью регвыров априори обречены быть если не провальными, то недостаточно общными как минимум.

По степени мудатства разбирать ответы GDB/MI с помощью регвыров — примерно то же самое, что обрабатывать XML теми же регвырами вместо XSLT.

Итак, ситуация в коде, которым я занимаюсь, требует перемен в лучшую сторону.

Выше!

Оценив синтаксис ответов GDB/MI, я пришёл к выводу, что (с небольшими исключениями) он хорошо ложится на следующую модель:

  • рассматриваются объекты, в которых инкапсулированы пары поле=значение;

  • значениями могут быть (строковые) константы, списки или другие объекты;

  • списки представляют собой неупорядоченные наборы других списков, констант или объектов.

Такая модель легко представляется простой рекурсивной списочной структурой данных в виде вложенных друг в друга ассоциативных и не очень списков.

Остаётся только обернуть весь вывод GDB/MI в фигурные скобочки, тогда на Лисп он переводится так (тут в качестве примера рассматривается ответ на запрос -thread-info):

было:

{threads=[{id="1", target-id="LWP18334", frame={level="0",
addr="0x08048b9a", func="mult_matrices_mt", args=[{name="m1",
value="0x804ba30"}, {name="m2", value="0x804ba30"}], file="test.c",
fullname="/home/sphinx/projects/gsoc/test.c", line="142"},
state="stopped"}], current-thread-id="1"}

стало:

((threads . (((id . "1")
              (target-id . "LWP18334")
              (frame . ((level . "0")
                        (addr . "0x08048b9a")
                        (func . "mult_matrices_mt")
                        (args . (((name . "m1")
                                  (value . "0x804ba30"))
                                 ((name . "m2")
                                  (value . "0x804ba30"))))
                        (file . "test.c")
                        (fullname . "/home/sphinx/projects/gsoc/test.c")
                        (line . "142")))
              (state . "stopped"))))
 (current-thread-id . "1"))

Стало очевидно, что нужно переводить строковые ответы GDB/MI в структуры данных более высокого уровня и работать уже с ними.

Я посоветовался в рассылке GDB, и авторы нескольких морд к GDB (CDT для Eclipse, Qt Creator) ожидаемо ответили, что работают с информацией от MI на объектной основе.

Использование JSON

Я уже было решил, что мне нужно писать какой-то парсер для MI, чтобы он мне это всё по круглым скобкам распихал.

Когда я первый раз поработал с GDB/MI, я подумал: «как же это похоже на JSON». Действительно, если не рассматривать один поистине уродский случай в синтаксисе GDB/MI, остаётся только обернуть имена полей в символы "", да знаки равенства заменить двоеточиями:

(defun json-partial-output ()
  (with-current-buffer (gdb-get-buffer-create 'gdb-partial-output-buffer)
    (goto-char (point-min))
    (insert "{")
    (replace-regexp "\\([[:alpha:]-_]+\\)=" "\"\\1\":")
    (goto-char (point-max))
    (insert "}")
    (goto-char (point-min))
    (let ((json-array-type 'list))
      (json-read))))

Ну да, двоеточия опять регекспом добавляю :) Это слабость в моём коде, но ей управлять проще.

Попробовал работать так — стало гораздо удобнее. Написал пару обёрток для того, чтобы можно было быстро обращаться к элементам рекурсивных списков и с их помощью новый код для буфера с информацией о потоках обогатился подобными конструкциями:

(insert (fadr-format "~.id (~.target-id) ~.state\tin ~.frame.func" thread))

Здесь thread — ассоциативный список представленной выше структуры. Функция fadr-format у меня просто заменяет в своём первом аргументе подстроки типа

~.frame.func

на результат вызова

(cdr (assoc 'func (cdr (assoc 'frame thread))))

то есть я просто обращаюсь к вложенным членам структуры thread:

thread → frame → func

Так что вместо ~.frame.func у меня в строке появляется значение соответствующего элемента структуры (конкретно здесь — имя функции, в которой находится поток).

Если бы GDB для вывода использовал JSON, всё было бы вообще хорошо. Разработчики морд могли бы использовать более распространённые и имеющии более широкое распространение парсеры JSON вместо поддержки своих собственных (очень похожих) функций.

Для этого не понадобится переписывать много кода, поскольку изменения в GDB требуются чисто косметические. С другой стороны, всегда можно чуточку поправить вывод GDB/MI уже в своей программе и работать тем же JSON-парсером.

Что пакостно

Вот неприятное место в синтаксисе MI, о котором я упоминал выше:

result ==>
    variable "=" value

tuple ==>
    "{}" | "{" result ( "," result )* "}"

list ==>
    "[]" | "[" value ( "," value )* "]" | "[" result ( "," result )* "]"

Тут получается, что и кортежи (tuple), и списки (list) могут содержать в себе разделённые запятыми пары поле=значение, что немного портит всю картину. Получается, что кортежи по смыслу являются подмножеством списков, а потому их наличие в синтаксисе как отдельной сущности избыточно. Было бы хорошо, если списки могли содержать просто набор значений:

list ==>
    "[]" | "[" value ( "," value )* "]"

То есть чтобы было спискам списково, а кортежам кортежево.

Тогда для использования описанной выше модели не было бы вообще никаких препятствий.

Сейчас некоторые команды нагло (и совершенно без всякой на то причины) пользуются данным недостатком синтаксиса; таковы, например -stack-list-frames, -break-info):

Ошмёток вывода -break-info:

body=[bkpt={number="1", … },bkpt={number="2", …}]

Ну нафига тут эти bkpt=? Должно быть так:

body=[{number="1", … },{number="2", …}]

А всё от недостатка планирования (из сообщения в рассылке):

I wonder why was GDB/MI syntax designed this way.

What makes you think it was designed? GDB/MI is not result of a design session, but of somewhat long evolution, so some early decisions are prominent.

git