Gdb довольно печатает с python рекурсивную структуру

Я не очень хорошо знаком с Python, и я просто открываю возможности сценариев для python для GDB; мотивировка моего вопроса заключается в усилении печати значений GDB внутри монитора MELT, который позже будет подключен к GCC MELT. Но вот более простой вариант.

Моя система - Linux/Debian/Sid/x86-64. компилятор GCC - 4.8.2; отладчик GDB - 7.6.2; его питон составляет 3,3

Я хочу отлаживать программу C с типом "дискриминационный союз":

// file tiny.c in the public domain by Basile Starynkevitch
// compile with gcc -g3 -Wall -std=c99 tiny.c -o tiny
// debug with gdb tiny
// under gdb: python tiny-gdb.py
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef union my_un myval_t;
enum tag_en {
  tag_none,
  tag_int,
  tag_string,
  tag_sequence
};
struct boxint_st;
struct boxstring_st;
struct boxsequence_st;
union my_un {
  void* ptr;
  enum tag_en *ptag;
  struct boxint_st *pint;
  struct boxstring_st *pstr;
  struct boxsequence_st *pseq;
};

struct boxint_st {
  enum tag_en tag;      // for tag_int
  int ival;
};
struct boxstring_st {
  enum tag_en tag;      // for tag_string
  char strval[];        // a zero-terminated C string 
};
struct boxsequence_st {
  enum tag_en tag;      // for tag_sequence
  unsigned slen;
  myval_t valtab[];     // of length slen
};


int main (int argc, char **argv) {
  printf ("start %s, argc=%d", argv[0], argc);
  struct boxint_st *iv42 = malloc (sizeof (struct boxint_st));
  iv42->tag = tag_int;
  iv42->ival = 42;
  struct boxstring_st *istrhello =
    malloc (sizeof (struct boxstring_st) + sizeof ("hello") + 1);
  istrhello->tag = tag_string;
  strcpy (istrhello->strval, "hello");
  struct boxsequence_st *iseq3 =
    malloc (sizeof (struct boxsequence_st) + 3 * sizeof (myval_t));
  iseq3->tag = tag_sequence;
  iseq3->slen = 3;
  iseq3->valtab[0] = (myval_t)iv42;
  iseq3->valtab[1] = (myval_t)istrhello;
  iseq3->valtab[2] = (myval_t)NULL;
  printf ("before %s:%d gdb print iseq3\n", __FILE__, __LINE__);
}

Вот мой файл Python для чтения под gdb

 # file tiny-gdb.py in the public domain by Basile Starynkevitch
 ## see also tiny.c file
class my_val_Printer:
    """pretty prints a my_val"""
    def __init__ (self, val):
        self.val = val
    def to_string (self):
        outs = "[email protected]" + self.val['ptr']
        mytag = self.val['ptag'].dereference();
        if (mytag):
            outs = outs + mytag.to_string()
    def display_hint (self):
        return 'my_val'

def my_val_lookup(val):
    lookup = val.type.tag
    if (lookup == None):
        return None
    if lookup == "my_val":
        return my_val_Printer(val)
    return None

Я придерживаюсь следующих основных вопросов.

  • Как установить мой красивый принтер в python под GDB? (В документации я вижу несколько способов, и я не могу выбрать подходящую).
  • Как обеспечить, чтобы GDB печатал как union my_un, так и синоним typedef-ed myval_t таким же образом.
  • Как должен красивый принтер обнаруживать NULL-указатели?
  • Как мой симпатичный принтер рекурсирует для struct boxsequence_st? Это означает, что указатель не равен нулю, а затем разыменовывает его ptag, сравнивая этот тег с tag_sequence, довольно печатая член гибкого массива valtab.
  • Как избежать слишком глубокого перебора печатной печати?

Ответ 1

У меня недостаточно опыта работы с apd gdb Python, чтобы называть это ответом; Я рассматриваю это только некоторые исследовательские заметки от другого разработчика. Мой код, приведенный ниже, довольно груб и уродлив. Однако это работает с gdb-7.4 и python-2.7.3. Пример прогона отладки:

$ gcc -Wall -g3 tiny.c -o tiny
$ gdb tiny
(gdb) b 58
(gdb) run
(gdb) print iseq3
$1 = (struct boxsequence_st *) 0x602050
(gdb) print iv42
$2 = (struct boxint_st *) 0x602010
(gdb) print istrhello
$3 = (struct boxstring_st *) 0x602030

Все вышесказанное является стандартным довольно печальным выходом - мои рассуждения состоят в том, что я часто хочу видеть, что представляют собой указатели, поэтому я не хотел их переопределять. Однако, для привязки указателей используется симплексный указатель, показанный ниже:

(gdb) print *iseq3
$4 = (struct boxsequence_st)(3) = {(struct boxint_st)42, (struct boxstring_st)"hello"(5), NULL}
(gdb) print *iv42
$5 = (struct boxint_st)42
(gdb) print *istrhello
$6 = (struct boxstring_st)"hello"(5)
(gdb) set print array
(gdb) print *iseq3
$7 = (struct boxsequence_st)(3) = {
  (struct boxint_st)42,
  (struct boxstring_st)"hello"(5),
  NULL
}
(gdb) info auto-load
Loaded  Script                                                                 
Yes     /home/.../tiny-gdb.py

В последней строке показано, что при отладке tiny, tiny-gdb.py в том же каталоге автоматически загружается (хотя вы можете отключить это, я считаю, что это поведение по умолчанию).

Файл tiny-gdb.py, используемый выше:

def deref(reference):
    target = reference.dereference()
    if str(target.address) == '0x0':
        return 'NULL'
    else:
        return target

class cstringprinter:
    def __init__(self, value, maxlen=4096):
        try:
            ends = gdb.selected_inferior().search_memory(value.address, maxlen, b'\0')
            if ends is not None:
                maxlen = ends - int(str(value.address), 16)
                self.size = str(maxlen)
            else:
                self.size = '%s+' % str(maxlen)
            self.data = bytearray(gdb.selected_inferior().read_memory(value.address, maxlen))
        except:
            self.data = None
    def to_string(self):
        if self.data is None:
            return 'NULL'
        else:
            return '\"%s\"(%s)' % (str(self.data).encode('string_escape').replace('"', '\\"').replace("'", "\\\\'"), self.size)

class boxintprinter:
    def __init__(self, value):
        self.value = value.cast(gdb.lookup_type('struct boxint_st'))
    def to_string(self):
        return '(struct boxint_st)%s' % str(self.value['ival'])

class boxstringprinter:
    def __init__(self, value):
        self.value = value.cast(gdb.lookup_type('struct boxstring_st'))
    def to_string(self):
        return '(struct boxstring_st)%s' % (self.value['strval'])

class boxsequenceprinter:
    def __init__(self, value):
        self.value = value.cast(gdb.lookup_type('struct boxsequence_st'))
    def display_hint(self):
        return 'array'
    def to_string(self):
        return '(struct boxsequence_st)(%s)' % str(self.value['slen'])
    def children(self):
        value = self.value
        tag = str(value['tag'])
        count = int(str(value['slen']))
        result = []
        if tag == 'tag_none':
            for i in xrange(0, count):
                result.append( ( '#%d' % i, deref(value['valtab'][i]['ptag']) ))
        elif tag == 'tag_int':
            for i in xrange(0, count):
                result.append( ( '#%d' % i, deref(value['valtab'][i]['pint']) ))
        elif tag == 'tag_string':
            for i in xrange(0, count):
                result.append( ( '#%d' % i, deref(value['valtab'][i]['pstr']) ))
        elif tag == 'tag_sequence':
            for i in xrange(0, count):
                result.append( ( '#%d' % i, deref(value['valtab'][i]['pseq']) ))
        return result

def typefilter(value):
    "Pick a pretty-printer for 'value'."
    typename = str(value.type.strip_typedefs().unqualified())

    if typename == 'char []':
        return cstringprinter(value)

    if (typename == 'struct boxint_st' or
        typename == 'struct boxstring_st' or
        typename == 'struct boxsequence_st'):
        tag = str(value['tag'])
        if tag == 'tag_int':
            return boxintprinter(value)
        if tag == 'tag_string':
            return boxstringprinter(value)
        if tag == 'tag_sequence':
            return boxsequenceprinter(value)

    return None

gdb.pretty_printers.append(typefilter)

Мотивы выбора следующие:

  • Как установить красивые принтеры в gdb?

    В этом вопросе есть две части: где установить файлы Python и как подключить симпатичные принтеры к gdb.

    Поскольку выбор симпатичного принтера не может полагаться только на выведенный тип, но должен заглянуть в фактические поля данных, вы не можете использовать функции регулярного выражения. Вместо этого я решил добавить свою собственную функцию выбора красивого принтера typefilter() в список глобальных симпатичных принтеров, как описано в документации, Я не реализовал функцию enable/disable, потому что считаю, что проще просто загрузить/не загружать соответствующий Python script.

    (typefilter() вызывается один раз для каждой ссылки на переменные, если какой-либо другой симпатичный принтер уже не принял его.)

    Проблема размещения файлов является более сложной. Для приложений-принтеров, ориентированных на приложения, размещение их в одном файле Python script звучит разумно, но для библиотеки некоторые расщепления, похоже, в порядке. Документация рекомендует упаковывать функции в модуль Python, так что простой python import module позволяет использовать довольно-принтер. К счастью, упаковка Python довольно проста. Если вы были в import gdb вверху и сохраните его до /usr/lib/pythonX.Y/tiny.py, где X.Y - используемая версия python, вам нужно запустить python import tiny в gdb, чтобы включить симпатичный принтер.

    Конечно, правильно packaging красивый принтер - очень хорошая идея, особенно если вы собираетесь его распространять, но это в значительной степени сводится к добавлению некоторых переменных и так далее к началу script, предполагая, что вы храните его как один файл. Для более сложных довольно-принтеров использование макета каталога может быть хорошей идеей.

  • Если у вас есть значение val, то val.type - объект gdb.Type, описывающий его тип; преобразование его в строку дает имя, читаемое человеком.

    val.type.strip_typedefs() дает фактический тип, если все typedefs лишены. Я даже добавил .unqualified(), так что все const/volatile/etc. типы классификаторов удаляются.

  • Обнаружение указателя NULL немного сложнее.

    Лучший способ, которым я нашел, - проверить стробированный член .address целевого объекта gdb.Value и посмотреть, есть ли он "0x0".

    Чтобы облегчить жизнь, я смог написать простую функцию deref(), которая пытается разыменовать указатель. Если цель указывает на (void *) 0, она возвращает строку "NULL", в противном случае она возвращает целевой объект gdb.Value.

    Способ, которым я пользуюсь deref(), основан на том факте, что "array" типа pretty-printers дает список из 2-х кортежей, где первый элемент - это строка имени, а второй элемент - либо gdb.Value объект или строку. Этот список возвращается с помощью метода children() объекта с красивым принтером.

  • Обработка типов с дискриминационным объединением была бы намного проще, если бы у вас был отдельный тип для общего объекта. То есть, если у вас

    struct box_st {
        enum tag_en tag;
    };
    

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

    Как и в tiny.c, типы struct box*_st могут использоваться взаимозаменяемо. (Или, более конкретно, мы не можем полагаться на определенное значение тега, основанное только на типе.)

    Случай последовательности на самом деле довольно прост, потому что valtab[] можно рассматривать как просто массив указателей void. Тег последовательности используется для выбора правильного члена объединения. Фактически, если valtab [] был просто массивом указателей void, то gdb.Value.cast(gdb.lookup_type()) или gdb.Value.reinterpret_cast (gdb.lookup_type()) может использоваться для изменения каждого типа указателя по мере необходимости, так же, как и для типов в штучной упаковке.

  • Пределы рекурсии?

    Вы можете использовать оператор @ в команде print, чтобы указать, сколько элементов напечатано, но это не помогает при вложенности.

    Если вы добавите iseq3->valtab[2] = (myval_t)iseq3; в tiny.c, вы получите бесконечно рекурсивную последовательность. gdb действительно печатает его, особенно с set print array, но он не замечает и не заботится о рекурсии.

На мой взгляд, вы можете написать команду gdb в дополнение к довольно принтеру для глубоко вложенных или рекурсивных структур данных. Во время моего тестирования я написал команду, которая использует Graphviz для рисования двоичных древовидных структур непосредственно изнутри gdb; Я абсолютно уверен, что это простое выведение текста.

Добавлено: Если вы сохраните следующее как /usr/lib/pythonX.Y/tree.py:

import subprocess
import gdb

def pretty(value, field, otherwise=''):
    try:
        if str(value[field].type) == 'char []':
            data = str(gdb.selected_inferior().read_memory(value[field].address, 64))
            try:
                size = data.index("\0")
                return '\\"%s\\"' % data[0:size].encode('string_escape').replace('"', '\\"').replace("'", "\\'")
            except:
                return '\\"%s\\"..' % data.encode('string_escape').replace('"', '\\"').replace("'", "\\'")
        else:
            return str(value[field])
    except:
        return otherwise

class tee:
    def __init__(self, cmd, filename):
        self.file = open(filename, 'wb')
        gdb.write("Saving DOT to '%s'.\n" % filename)
        self.cmd = cmd
    def __del__(self):
        if self.file is not None:
            self.file.flush()
            self.file.close()
            self.file = None
    def __call__(self, arg):
        self.cmd(arg)
        if self.file is not None:
            self.file.write(arg)

def do_dot(value, output, visited, source, leg, label, left, right):
    if value.type.code != gdb.TYPE_CODE_PTR:
        return
    target = value.dereference()

    target_addr = int(str(target.address), 16)
    if target_addr == 0:
        return

    if target_addr in visited:
        if source is not None:
            path='%s.%s' % (source, target_addr)
            if path not in visited:
                visited.add(path)
                output('\t"%s" -> "%s" [ taillabel="%s" ];\n' % (source, target_addr, leg))
        return

    visited.add(target_addr)

    if source is not None:
        path='%s.%s' % (source, target_addr)
        if path not in visited:
            visited.add(path)
            output('\t"%s" -> "%s" [ taillabel="%s" ];\n' % (source, target_addr, leg))

    if label is None:
        output('\t"%s" [ label="%s" ];\n' % (target_addr, target_addr))
    elif "," in label:
        lab = ''
        for one in label.split(","):
            cur = pretty(target, one, '')
            if len(cur) > 0:
                if len(lab) > 0:
                    lab = '|'.join((lab,cur))
                else:
                    lab = cur
        output('\t"%s" [ shape=record, label="{%s}" ];\n' % (target_addr, lab))
    else:
        output('\t"%s" [ label="%s" ];\n' % (target_addr, pretty(target, label, target_addr)))

    if left is not None:
        try:
            target_left = target[left]
            do_dot(target_left, output, visited, target_addr, left, label, left, right)
        except:
            pass

    if right is not None:
        try:
            target_right = target[right]
            do_dot(target_right, output, visited, target_addr, right, label, left, right)
        except:
            pass

class Tree(gdb.Command):

    def __init__(self):
        super(Tree, self).__init__('tree', gdb.COMMAND_DATA, gdb.COMPLETE_SYMBOL, False)

    def do_invoke(self, name, filename, left, right, label, cmd, arg):
        try:
            node = gdb.selected_frame().read_var(name)
        except:
            gdb.write('No symbol "%s" in current context.\n' % str(name))
            return
        if len(arg) < 1:
            cmdlist = [ cmd ]
        else:
            cmdlist = [ cmd, arg ]
        sub = subprocess.Popen(cmdlist, bufsize=16384, stdin=subprocess.PIPE, stdout=None, stderr=None)
        if filename is None:
            output = sub.stdin.write
        else:
            output = tee(sub.stdin.write, filename)
        output('digraph {\n')
        output('\ttitle = "%s";\n' % name)
        if len(label) < 1: label = None
        if len(left)  < 1: left  = None
        if len(right) < 1: right = None
        visited = set((0,))
        do_dot(node, output, visited, None, None, label, left, right)
        output('}\n')
        sub.communicate()
        sub.wait()

    def help(self):
        gdb.write('Usage: tree [OPTIONS] variable\n')
        gdb.write('Options:\n')
        gdb.write('   left=name          Name member pointing to left child\n')
        gdb.write('   right=name         Name right child pointer\n')
        gdb.write('   label=name[,name]  Define node fields\n')
        gdb.write('   cmd=dot arg=-Tx11  Specify the command (and one option)\n')
        gdb.write('   dot=filename.dot   Save .dot to a file\n')
        gdb.write('Suggestions:\n')
        gdb.write('   tree cmd=neato variable\n')

    def invoke(self, argument, from_tty):
        args = argument.split()
        if len(args) < 1:
            self.help()
            return
        num = 0
        cfg = { 'left':'left', 'right':'right', 'label':'value', 'cmd':'dot', 'arg':'-Tx11', 'dot':None }
        for arg in args[0:]:
            if '=' in arg:
                key, val = arg.split('=', 1)
                cfg[key] = val
            else:
                num += 1
                self.do_invoke(arg, cfg['dot'], cfg['left'], cfg['right'], cfg['label'], cfg['cmd'], cfg['arg'])
        if num < 1:
            self.help()

Tree()

вы можете использовать его в gdb:

(gdb) python import tree
(gdb) tree
Usage: tree [OPTIONS] variable
Options:
   left=name          Name member pointing to left child
   right=name         Name right child pointer
   label=name[,name]  Define node fields
   cmd=dot arg=-Tx11  Specify the command (and one option)
   dot=filename.dot   Save .dot to a file
Suggestions:
   tree cmd=neato variable

Если у вас есть, например,

struct node {
    struct node *le;
    struct node *gt;
    long         key;
    char         val[];
}

struct node *sometree;

и у вас есть X11 (локальное или удаленное) соединение и установлен Graphviz, вы можете использовать

(gdb) tree left=le right=gt label=key,val sometree

чтобы просмотреть древовидную структуру. Поскольку он сохраняет список уже посещенных узлов (как набор Python), он не становится в курсе рекурсивных структур.

Я, вероятно, должен был очистить мои фрагменты Python перед публикацией, но неважно. Пожалуйста, рассмотрите эти только исходные версии тестирования; Используйте на свой риск.:)