Если вы читали мои предыдущие сообщения, вы знаете, что мне нравится возиться с внутренними компонентами CPython, чтобы попытаться понять, как на самом деле работает python. Чтение исходного кода CPython помогает, но чтобы действительно понять, как работает python (или любой фрагмент кода), я считаю, что нужно пошагово выполнить код и понять поток управления. В этом посте я опишу процесс, которым я обычно следую, чтобы глубже изучить аспекты языка программирования python, которые меня интересуют. Если не указано иное, примеры и образцы кода в этом посте предполагают использование операционной системы типа unix.

Для начала нам нужно уметь скомпилировать и собрать интерпретатор CPython локально из исходного кода. Я знаю, это звучит пугающе, когда нам нужно скомпилировать и собрать программное обеспечение из исходного кода, но поверьте мне, этот процесс довольно прост для CPython и хорошо документирован здесь. Короче говоря, предполагая, что у нас есть базовый компилятор C и установленные необходимые системные библиотеки, следующие команды - это все, что нам нужно для получения локальной сборки интерпретатора CPython:

# locally clone the repo.
git clone https://github.com/python/cpython
# Navigate to the repo directory
cd cpython
# Switch to the version of python you want to work on
git checkout 3.6
# configure a debug build for CPython
./configure --with-pydebug
# build without echoing commands and use 2 cores
make -s -j2

После завершения сборки вы увидите файл python или python.exe (в зависимости от вашей ОС), созданный в вашем каталоге сборки. Вы должны иметь возможность запустить это на месте, набрав ./python или ./python.exe

Далее вам понадобится отладчик командной строки gdb. Доступны и другие графические отладчики, но при пошаговом выполнении кода C я считаю, что отладка из командной строки в стиле старой школы является наиболее простой и удобной.

После того, как у вас будет запущена локальная сборка интерпретатора CPython и установлен gdb, вы, наконец, можете перейти к самой интересной части: установке точек останова и пошаговому просмотру исходного кода CPython!

Когда я хочу изучить особенности языка программирования Python, я обычно работаю в обратном направлении от дизассемблированного байтового кода, связанного с фрагментом кода Python. Я выбираю код операции из дизассемблированного байтового кода и ищу реализацию этого кода операции в источнике C. После того, как я нашел реализацию, я установил точки останова, чтобы увидеть, как реализация на самом деле работает при запуске фрагмента кода Python. Чтобы дополнительно проиллюстрировать этот процесс, давайте рассмотрим конкретный пример того, как работает доступ к атрибутам в Python:

>>> import dis
>>> def foo():
...     a = 1
...     a.x
...
>>> dis.dis(foo)
  2           0 LOAD_CONST               1 (1)
              3 STORE_FAST               0 (a)
  3           6 LOAD_FAST                0 (a)
              9 LOAD_ATTR                0 (x)
             12 POP_TOP
             13 LOAD_CONST               0 (None)
             16 RETURN_VALUE

У нас есть очень простая функция foo. В этой функции мы объявляем переменную a, а затем пытаемся получить доступ к несуществующему свойству x для этой переменной (это, конечно, вызовет ошибку атрибута, но для этого упражнения это не имеет значения). Затем мы смотрим на дизассемблированный байт-код для foo, чтобы найти код операции, связанный с операцией доступа к атрибутам a.x. Неудивительно, что LOAD_ATTR кажется подходящим кандидатом для расследования.

Теперь нам нужно найти реализацию кода операции LOAD_ATTR в исходном коде CPython. Исходный файл C, содержащий реализации кода операции Python, - Python/ceval.c. Мы можем открыть этот файл в нашем любимом текстовом редакторе и просто выполнить поиск LOAD_ATTR с учетом регистра:

// Python/ceval.c
   2862 TARGET(LOAD_ATTR) {
   2863     PyObject *name = GETITEM(names, oparg);
   2864     PyObject *owner = TOP();
   2865     PyObject *res = PyObject_GetAttr(owner, name);
   2866     Py_DECREF(owner);
   2867     SET_TOP(res);
   2868     if (res == NULL)
   2869         goto error;
   2870     DISPATCH();
}

Мы заметим, что LOAD_ATTR находится внутри гигантского оператора switch внутри функции PyEval_EvalFrameEx. Эта функция является основным циклом интерпретации Python, в котором выполняется оценка всего кода операции.

Блок switch, содержащий регистр LOAD_ATTR, является нашей точкой входа в прохождение серии шагов, которые происходят, когда интерпретатор CPython выполняет поиск атрибутов объекта. Обратите внимание на номер строки (2863 в приведенном выше фрагменте кода), где начинается выполнение варианта LOAD_ATTR. Это строка, в которой нам нужно будет поставить нашу первую точку останова, как мы сейчас увидим.

Затем мы запускаем интерпретатор python, который мы только что создали из исходного кода, используя gdb. Нам нужно будет ввести команду run или просто r в приглашении gdb, чтобы начать выполнение python REPL. Попав в REPL, мы определяем описанную ранее функцию-образец foo для проверки доступа к атрибутам:

sabbas@sabbas-VirtualBox:~/Documents/pythondev/cpython
$ gdb python
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
(gdb) run
Starting program: /home/sabbas/Documents/pythondev/cpython/python 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Python 3.6.2+ (heads/3.6:cb7fdf6, Aug 23 2017, 22:24:16) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> 
>>> 
>>> def foo():
...     x = 1
...     x.a
... 
>>>

Прежде чем мы выполним foo, нам нужно войти в отладчик и установить точку останова в начале оператора case LOAD_ATTR opcode. Мы можем сделать это, выдав сигнал SIGTRAP с помощью команды kill или pkill в другой оболочке:

sabbas@sabbas-VirtualBox:~/Documents/pythondev/cpython
$ pkill python -SIGTRAP

GDB обрабатывает сигнал SIGTRAP и приостанавливает выполнение python REPL, тем самым давая нам возможность проверить текущее состояние программы и установить любые точки останова:

sabbas@sabbas-VirtualBox:~/Documents/pythondev/cpython
$ gdb python
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
(gdb) run
Starting program: /home/sabbas/Documents/pythondev/cpython/python 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Python 3.6.2+ (heads/3.6:cb7fdf6, Aug 23 2017, 22:24:16) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> 
>>> 
>>> def foo():
...     x = 1
...     x.a
... 
>>>
Program received signal SIGTRAP, Trace/breakpoint trap.
0x00007ffff71dd573 in __select_nocancel () at ../sysdeps/unix/syscall-template.S:84
84 ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) b Python/ceval.c:2855
Breakpoint 1 at 0x5418d7: file Python/ceval.c, line 2855.
(gdb)c
Continuing.
>>> 

Мы можем установить точку останова в начале оператора LOAD_ATTR opcode case, выполнив команду b Python/ceval.c:2855 в приглашении gdb, за которой следует команда c или continue, чтобы возобновить выполнение python REPL. Затем мы можем продолжить и выполнить функцию foo:

(gdb) b Python/ceval.c:2855
Breakpoint 1 at 0x5418d7: file Python/ceval.c, line 2855.
(gdb) c
Continuing.
>>> foo()
Breakpoint 1, _PyEval_EvalFrameDefault (f=<optimized out>, throwflag=<optimized out>) at Python/ceval.c:2855
2855             PyObject *name = GETITEM(names, oparg);
(gdb)

В тот момент, когда мы нажмем enter, мы увидим паузу интерпретатора CPython в только что установленной точке останова. С этого момента мы можем сделать ряд вещей для дальнейшего изучения. Например, помимо пошагового выполнения кода с помощью команд step или next gdb, чтобы увидеть, как разворачивается процесс доступа к атрибутам, мы можем подняться по стеку с помощью команды up, чтобы увидеть, какие строки кода были выполнены до того, как была достигнута точка останова. Мы также можем использовать команду backtrace, чтобы распечатать полный возврат стека. Мы действительно можем многое сделать с gdb, и я рекомендую вам поэкспериментировать с различными командами gdb.

Если вы хотели поиграть с внутренними компонентами CPython, но не знали, с чего и как начать, надеюсь, этот пост дал некоторое направление, с чего вы можете начать. Как только вы освоите это, навигация по внутренностям CPython с использованием gdb станет довольно увлекательным занятием.