Схема работы интерпретатора Python:
# пример для проведения декомпиляции
x = 30
y = 62
z = x + y
# python3.8 -m dis test.py
'''
1 0 LOAD_CONST 0 (30)
2 STORE_NAME 0 (x)
2 4 LOAD_CONST 1 (62)
6 STORE_NAME 1 (y)
3 8 LOAD_NAME 0 (x)
10 LOAD_NAME 1 (y)
12 BINARY_ADD
14 STORE_NAME 2 (z)
16 LOAD_CONST 2 (None)
18 RETURN_VALUE
'''
Когда интерпретатор СPython исполняет программу, он сначала ее транслирует в последовательность байткодовых инструкций.
Байткод — это промежуточный язык для виртуальной машины Python, который используется в качестве оптимизации производительности.
Байткод, который получается в результате этого шага компиляции, кэшируется на диске в файлах .pyc и .pyo, чтобы во второй раз исполнение того же самого файла Python проходило быстрее.
def greet(name):
return 'Привет, ' + name + '!'
greet('Гвидо')
'Привет, Гвидо!'
Каждая функция имеет атрибут __code__
, который мы можем использовать, чтобы получить инструкции виртуальной машины, константы и переменные, используемые нашей функцией greet
:
greet.__code__.co_code
b'd\x01|\x00\x17\x00d\x02\x17\x00S\x00'
greet.__code__.co_consts
# https://docs.python.org/3/library/inspect.html#types-and-members
(None, 'Привет, ', '!')
greet.__code__.co_varnames
('name',)
co_consts
содержит части строки приветствия, которую собирает наша функция. Константы и код хранятся отдельно, чтобы сэкономить пространство памяти.
Python хранит константы отдельно в поисковой таблице. Поток команд затем может ссылаться на константу по индексу в поисковой таблице.
Дизассемблер байткода Python располагается в модуле dis
(документация), который является составной частью стандартной библиотеки. Поэтому мы можем его просто импортировать и вызвать dis.dis()
с функцией greet
в качестве аргумента, чтобы получить более удобочитаемое представление о ее байткоде:
import dis
dis.dis(greet)
2 0 LOAD_CONST 1 ('Привет, ') 2 LOAD_FAST 0 (name) 4 BINARY_ADD 6 LOAD_CONST 2 ('!') 8 BINARY_ADD 10 RETURN_VALUE
Главное, что сделал дизассемблер, было разбиение потока команд и назначение каждому находящемуся в нем коду операции человекочитаемого имени, как, например, LOAD_CONST
.
Ссылки на константы и переменные теперь чередуются с байткодом и выведены полностью, чтобы уберечь нас от мозговой гимнастики относительно поиска по таблице co_const
или co_varnames
.
Глядя на человекочитаемые коды операций, мы начинаем понимать, как Python представляет и исполняет выражение 'Привет, ' + name + '!'
в исходной функции greet()
.
Сначала он извлекает константу в индексе 1 ('Привет, ')
и помещает ее в стек. Затем он загружает содержимое переменной name
и также помещает ее в стек.
Стек является структурой данных, которая используется в качестве внутренней рабочей памяти виртуальной машины. Существуют разные классы виртуальных машин, и один из них называется стековой машиной. Виртуальная машина Python является реализацией такой стековой машины.
Самое интересное относительно стека как абстрактной структуры данных состоит в том, что на минимальном уровне он поддерживает всего две операции: вталкивание (push
) и выталкивание (pop
).
Вталкивание добавляет значение на вершину стека, а выталкивание удаляет и возвращает самое верхнее значение. В отличие от массива, в стеке отсутствует способ получить доступ к элементам «ниже» верхнего уровня.
Давайте предположим, что вначале стек пустой. После того как первые два кода операции были исполнены, содержимое стека виртуальной машины будет выглядеть следующим образом (0 — это самый верхний элемент):
0: 'Гвидо' (содержимое "name")
1: 'Привет, '
Инструкция BINARY_ADD
выталкивает два строковых значения из стека, конкатенирует их, а затем вталкивает результат снова в стек:
0: 'Привет, Гвидо'
Затем идет еще одна инструкция LOAD_CONST
, которая помещает в стек строку с восклицательным знаком:
0: '!'
1: 'Привет, Гвидо'
Следующий код операции BINARY_ADD
снова объединяет два значения, чтобы сгенерировать заключительную приветственную строку:
0: 'Привет, Гвидо!'
Последняя байткодовая инструкция — RETURN_VALUE
, которая сообщает виртуальной машине следующее: то, что в настоящее время находится на вершине стека, является возвращаемым значением этой функции, и поэтому оно может быть передано источнику вызова.