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