Документация Python неофициальный перевод
Содержание страницы

Отладка расширений C API и внутренностей CPython с помощью GDBDebugging C API extensions and CPython Internals with GDB

Этот документ описывает, как расширение GDB для Python, python-gdb.py, может использоваться с отладчиком GDB для отладки расширений CPython и самого интерпретатора CPython.

При отладке низкоуровневых проблем, таких как крахи или взаимоблокировки, низкоуровневый отладчик, например GDB, полезен для диагностики и исправления проблемы. По умолчанию GDB (или любой его фронтенд) не поддерживает высокоуровневую информацию, специфичную для интерпретатора CPython.

Расширение python-gdb.py добавляет информацию об интерпретаторе CPython в GDB. Расширение помогает интроспектировать стек текущих выполняемых функций Python. При наличии объекта Python, представленного указателем PyObject*, расширение показывает тип и значение объекта.

Разработчики, работающие над расширениями CPython или экспериментирующие с частями CPython, написанными на C, могут использовать этот документ, чтобы узнать, как использовать расширение python-gdb.py с GDB.

Примечание

Этот документ предполагает, что вы знакомы с основами GDB и CPython C API. В нем собраны рекомендации из devguide и Python wiki.

Предварительные требованияPrerequisites

Вам потребуется:

  • GDB версии 7 или новее. (Для более ранних версий GDB см. Misc/gdbinit в исходниках CPython. Обратите внимание, что этот файл будет удалён в Python 3.12.)

  • Отладочная информация, совместимая с GDB, для Python и любых расширений, которые вы отлаживаете.

  • Расширение python-gdb.py.

Расширение собирается вместе с Python, но может распространяться отдельно или не распространяться вовсе. Ниже приведены советы для нескольких распространенных систем в качестве примеров. Обратите внимание: даже если инструкции соответствуют вашей системе, они могут быть устаревшими.

Настройка для сборки Python из исходного кодаSetup with Python built from source

При сборке CPython из исходного кода отладочная информация должна быть доступна, и сборка должна добавить файл python-gdb.py в корневой каталог вашего репозитория.

Чтобы активировать поддержку, необходимо добавить каталог, содержащий python-gdb.py, в «auto-load-safe-path» GDB. Если вы этого не сделали, последние версии GDB выведут предупреждение с инструкциями, как это сделать.

Примечание

Если вы не видите инструкций для вашей версии GDB, добавьте это в ваш конфигурационный файл (~/.gdbinit или ~/.config/gdb/gdbinit):

add-auto-load-safe-path /path/to/cpython

Также можно добавить несколько путей, разделенных :.

Настройка для Python из дистрибутива LinuxSetup for Python from a Linux distro

Большинство систем Linux предоставляют отладочную информацию для системного Python в пакете с именем python-debuginfo, python-dbg или подобном. Например:

  • Fedora:

    sudo dnf install gdb
    sudo dnf debuginfo-install python3
    
  • Ubuntu:

    sudo apt install gdb python3-dbg
    

В некоторых современных системах Linux GDB может автоматически загружать отладочные символы с помощью debuginfod. Однако это не установит расширение python-gdb.py; обычно требуется установить пакет с отладочной информацией отдельно.

Использование отладочной сборки и режима разработкиUsing the Debug build and Development mode

Для упрощения отладки может быть полезно:

  • Используйте отладочную сборку Python. (При сборке из исходного кода используйте configure --with-pydebug. В дистрибутивах Linux установите и запустите пакет наподобие python-debug или python-dbg, если они доступны.)

  • Используйте режим разработки во время выполнения (-X dev).

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

Использование расширения python-gdbUsing the python-gdb extension

После загрузки расширения оно предоставляет две основные возможности: форматировщики вывода (pretty printers) для значений Python и дополнительные команды.

Форматировщики вывода (pretty printers)Pretty-printers

Вот как выглядит обратная трассировка GDB (сокращённая) при включённом расширении:

#0  0x000000000041a6b1 in PyObject_Malloc (nbytes=Cannot access memory at address 0x7fffff7fefe8
) at Objects/obmalloc.c:748
#1  0x000000000041b7c0 in _PyObject_DebugMallocApi (id=111 'o', nbytes=24) at Objects/obmalloc.c:1445
#2  0x000000000041b717 in _PyObject_DebugMalloc (nbytes=24) at Objects/obmalloc.c:1412
#3  0x000000000044060a in _PyUnicode_New (length=11) at Objects/unicodeobject.c:346
#4  0x00000000004466aa in PyUnicodeUCS2_DecodeUTF8Stateful (s=0x5c2b8d "__lltrace__", size=11, errors=0x0, consumed=
    0x0) at Objects/unicodeobject.c:2531
#5  0x0000000000446647 in PyUnicodeUCS2_DecodeUTF8 (s=0x5c2b8d "__lltrace__", size=11, errors=0x0)
    at Objects/unicodeobject.c:2495
#6  0x0000000000440d1b in PyUnicodeUCS2_FromStringAndSize (u=0x5c2b8d "__lltrace__", size=11)
    at Objects/unicodeobject.c:551
#7  0x0000000000440d94 in PyUnicodeUCS2_FromString (u=0x5c2b8d "__lltrace__") at Objects/unicodeobject.c:569
#8  0x0000000000584abd in PyDict_GetItemString (v=
    {'Yuck': <type at remote 0xad4730>, '__builtins__': <module at remote 0x7ffff7fd5ee8>, '__file__': 'Lib/test/crashers/nasty_eq_vs_dict.py', '__package__': None, 'y': <Yuck(i=0) at remote 0xaacd80>, 'dict': {0: 0, 1: 1, 2: 2, 3: 3}, '__cached__': None, '__name__': '__main__', 'z': <Yuck(i=0) at remote 0xaace60>, '__doc__': None}, key=
    0x5c2b8d "__lltrace__") at Objects/dictobject.c:2171

Обратите внимание, как аргумент-словарь PyDict_GetItemString отображается как его repr(), а не как непрозрачный указатель PyObject *.

Расширение работает за счёт предоставления собственной процедуры печати для значений типа PyObject *. Если нужно получить доступ к низкоуровневым деталям объекта, приведите значение к указателю подходящего типа. Например:

(gdb) p globals
$1 = {'__builtins__': <module at remote 0x7ffff7fb1868>, '__name__':
'__main__', 'ctypes': <module at remote 0x7ffff7f14360>, '__doc__': None,
'__package__': None}

(gdb) p *(PyDictObject*)globals
$2 = {ob_refcnt = 3, ob_type = 0x3dbdf85820, ma_fill = 5, ma_used = 5,
ma_mask = 7, ma_table = 0x63d0f8, ma_lookup = 0x3dbdc7ea70
<lookdict_string>, ma_smalltable = {{me_hash = 7065186196740147912,
me_key = '__builtins__', me_value = <module at remote 0x7ffff7fb1868>},
{me_hash = -368181376027291943, me_key = '__name__',
me_value ='__main__'}, {me_hash = 0, me_key = 0x0, me_value = 0x0},
{me_hash = 0, me_key = 0x0, me_value = 0x0},
{me_hash = -9177857982131165996, me_key = 'ctypes',
me_value = <module at remote 0x7ffff7f14360>},
{me_hash = -8518757509529533123, me_key = '__doc__', me_value = None},
{me_hash = 0, me_key = 0x0, me_value = 0x0}, {
  me_hash = 6614918939584953775, me_key = '__package__', me_value = None}}}

Обратите внимание: pretty printers не вызывают repr(). Для базовых типов они стараются точно воспроизвести его результат.

Область, которая может сбивать с толку: пользовательский форматировщик для некоторых типов очень похож на встроенный форматировщик GDB для стандартных типов. Например, pretty-printer для Python int (PyLongObject*) выдаёт представление, которое невозможно отличить от представления обычного машинного целого числа:

(gdb) p some_machine_integer
$3 = 42

(gdb) p some_python_integer
$4 = 42

Внутреннюю структуру можно увидеть, приведя к PyLongObject*:

(gdb) p (PyLongObject)some_python_integer $5 = {ob_base = {ob_base = {ob_refcnt = 8, ob_type = 0x3dad39f5e0}, ob_size = 1}, ob_digit = {42}}

Подобная путаница может возникнуть с типом str, когда вывод очень похож на встроенный форматировщик GDB для char *:

(gdb) p ptr_to_python_str
$6 = '__builtins__'

Pretty-printer для экземпляров str по умолчанию использует одинарные кавычки (как и repr Python для строк), тогда как стандартный форматировщик для значений char * использует двойные кавычки и содержит шестнадцатеричный адрес:

(gdb) p ptr_to_char_star
$7 = 0x6d72c0 "hello world"

Опять же, детали реализации можно увидеть, приведя к PyUnicodeObject*:

(gdb) p *(PyUnicodeObject*)$6
$8 = {ob_base = {ob_refcnt = 33, ob_type = 0x3dad3a95a0}, length = 12,
str = 0x7ffff2128500, hash = 7065186196740147912, state = 1, defenc = 0x0}

py-list

Расширение добавляет команду py-list, которая выводит исходный код Python (если есть) для текущего фрейма в выбранном потоке. Текущая строка помечается символом “>”:

(gdb) py-list
 901        if options.profile:
 902            options.profile = False
 903            profile_me()
 904            return
 905
>906        u = UI()
 907        if not u.quit:
 908            try:
 909                gtk.main()
 910            except KeyboardInterrupt:
 911                # properly quit on a keyboard interrupt...

Используйте py-list START для вывода начиная с другой строки в исходном коде Python, а py-list START,END для вывода определённого диапазона строк в исходном коде Python.

py-up и py-downpy-up and py-down

Команды py-up и py-down аналогичны обычным командам GDB up и down, но пытаются перемещаться на уровне фреймов CPython, а не C-фреймов.

GDB не всегда может прочитать соответствующую информацию о фрейме, в зависимости от уровня оптимизации, с которым был скомпилирован CPython. Внутренне эти команды ищут C-фреймы, в которых выполняется функция вычисления фреймов по умолчанию (то есть основной цикл интерпретатора байткода внутри CPython) и находят значение соответствующего PyFrameObject *.

Они выводят номер фрейма (на уровне C) в пределах потока.

Например:

(gdb) py-up
#37 Frame 0x9420b04, for file /usr/lib/python2.6/site-packages/
gnome_sudoku/main.py, line 906, in start_game ()
    u = UI()
(gdb) py-up
#40 Frame 0x948e82c, for file /usr/lib/python2.6/site-packages/
gnome_sudoku/gnome_sudoku.py, line 22, in start_game(main=<module at remote 0xb771b7f4>)
    main.start_game()
(gdb) py-up
Unable to find an older python frame

итак, мы на вершине стека Python.

Номера фреймов соответствуют тем, что выводятся стандартной командой GDB backtrace. Команда пропускает C-фреймы, в которых не выполняется код Python.

Спускаемся обратно:

(gdb) py-down
#37 Frame 0x9420b04, for file /usr/lib/python2.6/site-packages/gnome_sudoku/main.py, line 906, in start_game ()
    u = UI()
(gdb) py-down
#34 (unable to read python frame information)
(gdb) py-down
#23 (unable to read python frame information)
(gdb) py-down
#19 (unable to read python frame information)
(gdb) py-down
#14 Frame 0x99262ac, for file /usr/lib/python2.6/site-packages/gnome_sudoku/game_selector.py, line 201, in run_swallowed_dialog (self=<NewOrSavedGameSelector(new_game_model=<gtk.ListStore at remote 0x98fab44>, puzzle=None, saved_games=[{'gsd.auto_fills': 0, 'tracking': {}, 'trackers': {}, 'notes': [], 'saved_at': 1270084485, 'game': '7 8 0 0 0 0 0 5 6 0 0 9 0 8 0 1 0 0 0 4 6 0 0 0 0 7 0 6 5 0 0 0 4 7 9 2 0 0 0 9 0 1 0 0 0 3 9 7 6 0 0 0 1 8 0 6 0 0 0 0 2 8 0 0 0 5 0 4 0 6 0 0 2 1 0 0 0 0 0 4 5\n7 8 0 0 0 0 0 5 6 0 0 9 0 8 0 1 0 0 0 4 6 0 0 0 0 7 0 6 5 1 8 3 4 7 9 2 0 0 0 9 0 1 0 0 0 3 9 7 6 0 0 0 1 8 0 6 0 0 0 0 2 8 0 0 0 5 0 4 0 6 0 0 2 1 0 0 0 0 0 4 5', 'gsd.impossible_hints': 0, 'timer.__absolute_start_time__': <float at remote 0x984b474>, 'gsd.hints': 0, 'timer.active_time': <float at remote 0x984b494>, 'timer.total_time': <float at remote 0x984b464>}], dialog=<gtk.Dialog at remote 0x98faaa4>, saved_game_model=<gtk.ListStore at remote 0x98fad24>, sudoku_maker=<SudokuMaker(terminated=False, played=[], batch_siz...(truncated)
            swallower.run_dialog(self.dialog)
(gdb) py-down
#11 Frame 0x9aead74, for file /usr/lib/python2.6/site-packages/gnome_sudoku/dialog_swallower.py, line 48, in run_dialog (self=<SwappableArea(running=<gtk.Dialog at remote 0x98faaa4>, main_page=0) at remote 0x98fa6e4>, d=<gtk.Dialog at remote 0x98faaa4>)
            gtk.main()
(gdb) py-down
#8 (unable to read python frame information)
(gdb) py-down
Unable to find a newer python frame

и мы на дне стека Python.

py-bt

Команда py-bt пытается показать обратную трассировку на уровне Python текущего потока.

Например:

(gdb) py-bt
#8 (unable to read python frame information)
#11 Frame 0x9aead74, for file /usr/lib/python2.6/site-packages/gnome_sudoku/dialog_swallower.py, line 48, in run_dialog (self=<SwappableArea(running=<gtk.Dialog at remote 0x98faaa4>, main_page=0) at remote 0x98fa6e4>, d=<gtk.Dialog at remote 0x98faaa4>)
            gtk.main()
#14 Frame 0x99262ac, for file /usr/lib/python2.6/site-packages/gnome_sudoku/game_selector.py, line 201, in run_swallowed_dialog (self=<NewOrSavedGameSelector(new_game_model=<gtk.ListStore at remote 0x98fab44>, puzzle=None, saved_games=[{'gsd.auto_fills': 0, 'tracking': {}, 'trackers': {}, 'notes': [], 'saved_at': 1270084485, 'game': '7 8 0 0 0 0 0 5 6 0 0 9 0 8 0 1 0 0 0 4 6 0 0 0 0 7 0 6 5 0 0 0 4 7 9 2 0 0 0 9 0 1 0 0 0 3 9 7 6 0 0 0 1 8 0 6 0 0 0 0 2 8 0 0 0 5 0 4 0 6 0 0 2 1 0 0 0 0 0 4 5\n7 8 0 0 0 0 0 5 6 0 0 9 0 8 0 1 0 0 0 4 6 0 0 0 0 7 0 6 5 1 8 3 4 7 9 2 0 0 0 9 0 1 0 0 0 3 9 7 6 0 0 0 1 8 0 6 0 0 0 0 2 8 0 0 0 5 0 4 0 6 0 0 2 1 0 0 0 0 0 4 5', 'gsd.impossible_hints': 0, 'timer.__absolute_start_time__': <float at remote 0x984b474>, 'gsd.hints': 0, 'timer.active_time': <float at remote 0x984b494>, 'timer.total_time': <float at remote 0x984b464>}], dialog=<gtk.Dialog at remote 0x98faaa4>, saved_game_model=<gtk.ListStore at remote 0x98fad24>, sudoku_maker=<SudokuMaker(terminated=False, played=[], batch_siz...(truncated)
            swallower.run_dialog(self.dialog)
#19 (unable to read python frame information)
#23 (unable to read python frame information)
#34 (unable to read python frame information)
#37 Frame 0x9420b04, for file /usr/lib/python2.6/site-packages/gnome_sudoku/main.py, line 906, in start_game ()
    u = UI()
#40 Frame 0x948e82c, for file /usr/lib/python2.6/site-packages/gnome_sudoku/gnome_sudoku.py, line 22, in start_game (main=<module at remote 0xb771b7f4>)
    main.start_game()

Номера фреймов соответствуют тем, что выводятся стандартной командой GDB backtrace.

py-print

Команда py-print ищет имя Python и пытается его вывести. Поиск производится сначала в локальных переменных текущего потока, затем в глобальных, и наконец во встроенных (builtins):

(gdb) py-print self
local 'self' = <SwappableArea(running=<gtk.Dialog at remote 0x98faaa4>,
main_page=0) at remote 0x98fa6e4>
(gdb) py-print __name__
global '__name__' = 'gnome_sudoku.dialog_swallower'
(gdb) py-print len
builtin 'len' = <built-in function len>
(gdb) py-print scarlet_pimpernel
'scarlet_pimpernel' not found

py-locals

Команда py-locals ищет все локальные переменные Python в текущем Python-фрейме в выбранном потоке и выводит их представления:

(gdb) py-locals
self = <SwappableArea(running=<gtk.Dialog at remote 0x98faaa4>,
main_page=0) at remote 0x98fa6e4>
d = <gtk.Dialog at remote 0x98faaa4>

Использование с командами GDBUse with GDB commands

Команды расширения дополняют встроенные команды GDB. Например, можно использовать номера фреймов из вывода py-bt с командой frame, чтобы перейти к определенному фрейму в выбранном потоке, вот так:

(gdb) py-bt
(output snipped)
#68 Frame 0xaa4560, for file Lib/test/regrtest.py, line 1548, in <module> ()
        main()
(gdb) frame 68
#68 0x00000000004cd1e6 in PyEval_EvalFrameEx (f=Frame 0xaa4560, for file Lib/test/regrtest.py, line 1548, in <module> (), throwflag=0) at Python/ceval.c:2665
2665                            x = call_function(&sp, oparg);
(gdb) py-list
1543        # Run the tests in a context manager that temporary changes the CWD to a
1544        # temporary and writable directory. If it's not possible to create or
1545        # change the CWD, the original CWD will be used. The original CWD is
1546        # available from test_support.SAVEDCWD.
1547        with test_support.temp_cwd(TESTCWD, quiet=True):
>1548            main()

Команда info threads выводит список потоков внутри процесса, а командой thread можно выбрать другой поток:

(gdb) info threads
  105 Thread 0x7fffefa18710 (LWP 10260)  sem_wait () at ../nptl/sysdeps/unix/sysv/linux/x86_64/sem_wait.S:86
  104 Thread 0x7fffdf5fe710 (LWP 10259)  sem_wait () at ../nptl/sysdeps/unix/sysv/linux/x86_64/sem_wait.S:86
* 1 Thread 0x7ffff7fe2700 (LWP 10145)  0x00000038e46d73e3 in select () at ../sysdeps/unix/syscall-template.S:82

Можно использовать thread apply all COMMAND (или сокращённо t a a COMMAND) для запуска команды на всех потоках. Вместе с py-bt это позволяет увидеть, чем занят каждый поток на уровне Python:

(gdb) t a a py-bt

Thread 105 (Thread 0x7fffefa18710 (LWP 10260)):
#5 Frame 0x7fffd00019d0, for file /home/david/coding/python-svn/Lib/threading.py, line 155, in _acquire_restore (self=<_RLock(_Verbose__verbose=False, _RLock__owner=140737354016512, _RLock__block=<thread.lock at remote 0x858770>, _RLock__count=1) at remote 0xd7ff40>, count_owner=(1, 140737213728528), count=1, owner=140737213728528)
        self.__block.acquire()
#8 Frame 0x7fffac001640, for file /home/david/coding/python-svn/Lib/threading.py, line 269, in wait (self=<_Condition(_Condition__lock=<_RLock(_Verbose__verbose=False, _RLock__owner=140737354016512, _RLock__block=<thread.lock at remote 0x858770>, _RLock__count=1) at remote 0xd7ff40>, acquire=<instancemethod at remote 0xd80260>, _is_owned=<instancemethod at remote 0xd80160>, _release_save=<instancemethod at remote 0xd803e0>, release=<instancemethod at remote 0xd802e0>, _acquire_restore=<instancemethod at remote 0xd7ee60>, _Verbose__verbose=False, _Condition__waiters=[]) at remote 0xd7fd10>, timeout=None, waiter=<thread.lock at remote 0x858a90>, saved_state=(1, 140737213728528))
            self._acquire_restore(saved_state)
#12 Frame 0x7fffb8001a10, for file /home/david/coding/python-svn/Lib/test/lock_tests.py, line 348, in f ()
            cond.wait()
#16 Frame 0x7fffb8001c40, for file /home/david/coding/python-svn/Lib/test/lock_tests.py, line 37, in task (tid=140737213728528)
                f()

Thread 104 (Thread 0x7fffdf5fe710 (LWP 10259)):
#5 Frame 0x7fffe4001580, for file /home/david/coding/python-svn/Lib/threading.py, line 155, in _acquire_restore (self=<_RLock(_Verbose__verbose=False, _RLock__owner=140737354016512, _RLock__block=<thread.lock at remote 0x858770>, _RLock__count=1) at remote 0xd7ff40>, count_owner=(1, 140736940992272), count=1, owner=140736940992272)
        self.__block.acquire()
#8 Frame 0x7fffc8002090, for file /home/david/coding/python-svn/Lib/threading.py, line 269, in wait (self=<_Condition(_Condition__lock=<_RLock(_Verbose__verbose=False, _RLock__owner=140737354016512, _RLock__block=<thread.lock at remote 0x858770>, _RLock__count=1) at remote 0xd7ff40>, acquire=<instancemethod at remote 0xd80260>, _is_owned=<instancemethod at remote 0xd80160>, _release_save=<instancemethod at remote 0xd803e0>, release=<instancemethod at remote 0xd802e0>, _acquire_restore=<instancemethod at remote 0xd7ee60>, _Verbose__verbose=False, _Condition__waiters=[]) at remote 0xd7fd10>, timeout=None, waiter=<thread.lock at remote 0x858860>, saved_state=(1, 140736940992272))
            self._acquire_restore(saved_state)
#12 Frame 0x7fffac001c90, for file /home/david/coding/python-svn/Lib/test/lock_tests.py, line 348, in f ()
            cond.wait()
#16 Frame 0x7fffac0011c0, for file /home/david/coding/python-svn/Lib/test/lock_tests.py, line 37, in task (tid=140736940992272)
                f()

Thread 1 (Thread 0x7ffff7fe2700 (LWP 10145)):
#5 Frame 0xcb5380, for file /home/david/coding/python-svn/Lib/test/lock_tests.py, line 16, in _wait ()
    time.sleep(0.01)
#8 Frame 0x7fffd00024a0, for file /home/david/coding/python-svn/Lib/test/lock_tests.py, line 378, in _check_notify (self=<ConditionTests(_testMethodName='test_notify', _resultForDoCleanups=<TestResult(_original_stdout=<cStringIO.StringO at remote 0xc191e0>, skipped=[], _mirrorOutput=False, testsRun=39, buffer=False, _original_stderr=<file at remote 0x7ffff7fc6340>, _stdout_buffer=<cStringIO.StringO at remote 0xc9c7f8>, _stderr_buffer=<cStringIO.StringO at remote 0xc9c790>, _moduleSetUpFailed=False, expectedFailures=[], errors=[], _previousTestClass=<type at remote 0x928310>, unexpectedSuccesses=[], failures=[], shouldStop=False, failfast=False) at remote 0xc185a0>, _threads=(0,), _cleanups=[], _type_equality_funcs={<type at remote 0x7eba00>: <instancemethod at remote 0xd750e0>, <type at remote 0x7e7820>: <instancemethod at remote 0xd75160>, <type at remote 0x7e30e0>: <instancemethod at remote 0xd75060>, <type at remote 0x7e7d20>: <instancemethod at remote 0xd751e0>, <type at remote 0x7f19e0...(truncated)
        _wait()