Пытаюсь разобраться в структуре скомпилированного байт-кода CPython.
Допустим, у меня есть файл foo.py следующего содержания:
def hello(name):
print("Hello, %s" % name)
Скомпилированный __pycache__\foo.cpython-35.pyc выглядит так:
Дальше что я понял:
16 0d 0d 0a - это магическое число
06 e2 7f 57 - дата последнего изменения
31 00 00 00 - размер файла (должен быть - хотя в файле 216 байт, так что я не знаю, что это за размер на самом деле)
следующие 22 байта (e3 00 00 00 ... 00 00 00 73) - не знаю для чего
10 00 00 00 - видимо размер кода модуля (но тогда получается что предыдущий символ должен быть типом, а это 73 - код строкового типа)
64 00 00, 64 01 00 - LOAD_CONST(0); LOAD_CONST(1),
где константа 0 - code object функции, константа 1 - её имя ("hello")
84 00 00 - MAKE_FUNCTION(0), только я так и не понял, зачем нужен аргумент этого опкода
5a 00 00 - STORE_NAME(0), где имя 0 - имя функции ("hello")
64 02 00 53 - LOAD_CONST(2); RETURN_VALUE, где константа 2 - None (правда не совсем понятно зачем модулю возвращать что-то)
29 03 - кортеж констант модуля: (, "hello", None)
следующие 18 байт (74 00 00 64 ... 64 00 00 53) - код функции hello (тот который hello.__code__.co_code)
29 02 - кортеж констант функции: (None, "Hello, %s")
4e - тип константы NONE
7a 09 - тип строковой константы (SHORT_ASCII) и длина строки "Hello, %s" (9 символов)
48 65 6c 6c 6f 2c 20 25 73 - строка "Hello, %s"
29 01 - кортеж имён функции: ("print",)
da 05 - здесь вроде должен быть один из строковых типов, но вместо него несуществующий тип da; 05 - длина строка
70 72 69 6e 74 - имя функции print
29 01 - кортеж имён локальных переменных: ("name",)
da 04 - опять неизвестный тип da и длина строки 04
6e 61 6d 65 - имя переменной name
a9 00 72 03 00 00 00 - не понял
fa 06 - ещё один непонятный тип fa и длина строки 06
66 6f 6f 2e 70 79 - имя файла модуля (foo.py)
da 05 - непонятный тип da и длина строки 05
68 65 6c 6c 6f - имя функции hello (правда нет объявления кортежа имён модуля или вроде того)
следующие 17 байт (01 00 00 00 ... 00 00 00 4e) - не понял
29 01 - какой-то кортеж длиной в один элемент
72 ... - константа типа ref (что это за тип?)
da 08 - снова загадочный типа da и длина строки 08
3c 6d 6f 64 75 6c 65 3e - строка "
Помогите восполнить пробелы. Если бы мне надо было просто узнать значение какой-нибудь константы, то мне бы имеющихся знаний хватило. Но я пытаюсь написать интерпретатор байт-кода Python, так что мне надо полностью разобраться со структурой .pyc-файлов.
Ответ
История длинная - можно поставить чайку. Также у меня нет возможности копировать все из документации в ответ, так что документацию придется читать самому. Также не будет экспериментов с dis - предполагается, что автор вопроса уже наигрался с ним и решил копнуть глубже.
Вопрос объемный, но так как он пользуется популярностью, я немного коснусь этой темы для одной выбранной реализации и одной выбранной версии - CPython 3.6.1 - эта версия, потому что она последняя и ответ устареет позднее, и потому что в вопросе явно указана версия 3.5.
В общем-то в последних версиях довольно легко разобраться, что к чему - нет никаких сложностей, которые могут сорвать вам колпак. Исходной точкой будет модуль py_compile. Почему с нее, а не с C исходников? - потому что вот что говорит документация о нем: Этот модуль предоставляет функцию для генерации байт-кода из исходников. То, что доктор прописал.
Также заранее напишем тестовый пример, который и будет подвергнут разбору и определим его в отдельный файл test.py
import time
def my_gen():
for i in range(10):
time.sleep(1)
yield i**2
if __name__ == "__main__":
for elem in my_gen():
print(elem)
Теперь в основном файле мы можем сгенерировать байткод вот так:
import py_compile
py_compile.compile("test.py", "test.pyc")
Расширение *.pyc - всего лишь расширение, соглашение о наименовании и может быть каким угодно, главное - это содержимое.
Запустить можно как обычно - python test.pyc
Теперь самое время обратить внимание на функцию, которая и сгенерировала байткод. Исходники вы можете посмотреть самостоятельно, я приведу краткую выдержку:
Первое, что там происходит - это определение пути и имени для выходного байткода. Мы указали это явно. Если нет, то путь определяется согласно PEP 3147/PEP 488 и еще и от версии зависит.
Исходник в виде текста преобразуется в байты. За это отвечает еще модуль - importlib, а точнее класс importlib.machinery.SourceFileLoader. Внезапно, весь код, который там выполняется - это
with _io.FileIO(path, 'r') as file:
return file.read()
то есть исходник просто читается. В нашем коде это выглядит как:
loader = importlib.machinery.SourceFileLoader('
code1 = compile(source_bytes, file, mode='exec', dont_inherit=True)
code = loader.source_to_code(source_bytes, file)
print(code, type(code))
print(code == code1)
Выше в коде я копировал их исходников py_compile (loader.source_to_code) и написал сам с использованием функции compile - результат в простом случае одинаков. Результатом явился экземпляр класса из модуля code
Наконец, самое главное - преобразование AST в байткод. В py_compile это делается так:
source_stats = loader.path_stats(file)
bytecode = importlib._bootstrap_external._code_to_bytecode(
code, source_stats['mtime'], source_stats['size'])
sourcestats выглядит как-то так: {'mtime': 1493229661.7715623, 'size': 180}. mtime - это время изменения исходника, выраженное в каком-то особоточном timestamp'е. Пригодится еще далее.
Содержание code_to_bytecode
data = bytearray(MAGIC_NUMBER)
data.extend(_w_long(mtime))
data.extend(_w_long(source_size))
data.extend(marshal.dumps(code))
Я был удивлен, но само преобразование в байткод занимает в общем-то 4 строчки. MAGIC_NUMBER - волшебное словцо, новое для каждой новой версии. Для 3.6.1 (Python 3.6rc1) - это (3379).to_bytes(2, 'little') + b'
'. Список всех волшебных словец можно найти в файле importlib/_bootstrap_external.py. Обратите внимание, что помимо числа туда еще засовывается перевод каретки. Функция _w_long - это (int(x) & 0xFFFFFFFF).to_bytes(4, 'little'). - можно проверить HEX редактором фактическое содержание .pyc файла - все совпадает. То есть сам код начинается после 2 + 2 + 4 + 4 байтов. Это все мелочи, настоящая цель - модель marshal и функция dumps. marshal - это модуль, содержащий функции, которые могут читать и писать значение переменных в бинарном формате. Формате специфичен для Python, но независим от архитектуры конечной машины
Дальше, к сожалению, на питоне ехать не получиться, придется идти в исходники исходников: можно проследить за судьбой объекта в marshal.dumps, но в конечном итоге объект бьется на байты вот этой функцией. Она длинная, но относительно простая. Маршал может много всяких объектов засунуть в файл, но нас интересует объект типа code (его ему передали) - для этого есть специальная проверка на то, не является ли переданный объект кодом. Как видно из этой функции, безусловно пишется много какой информации - тип операции (op_code), аргументы, значения аргументов самые простые из этого списка. Что конкретно означают эти переменные, можно посмотреть (там комментов навалили сполна) в определении структуры PyCodeObject в файле code.h
Еще, в общем-то, не все. Все только начинается. Мы можем посмотреть, ЧТО КОНКРЕТНО, по байтам, пишется в pyc файл и не придется лазить в C. Дело в том, что все нужные аттрибуты есть у питоновского объекта code. Я расположил аргументы в том порядке, в каком они пишутся в pyc:
args = ['co_argcount', 'co_kwonlyargcount', 'co_nlocals', 'co_stacksize', 'co_flags', 'co_code', 'co_consts',
'co_names', 'co_varnames', 'co_freevars', 'co_cellvars', 'co_filename', 'co_name', 'co_firstlineno',
'co_lnotab']
for arg in args:
print(arg, getattr(code, arg, None))
Распечатав на экране значение аргументов, а также посчитав смещения (1 вызов w_long пишет 4 байта, W_TYPE пишет 1 байт) вы можете с легкостью, с помощью HEX редактора посмотреть что и куда пишется. Проанализировать co_code вы уже можете с помощью dis - в новых версиях у него появился козырный метод get_instructions
for instruction in dis.get_instructions(code):
print(instruction)
В нем также есть и байтовое смещение, и код операнда и все, что может понадобиться.
Для закрепления прочитанного можно своими руками полностью раскукожить pyc файл. Не буду писать весь парсер - дюже затратно, но из того, что написано уже можно написать парсер полностью:
from io import BytesIO
import struct
import datetime
byte1_little = b"
INTEGER_TYPE = int.from_bytes(b'i', 'little')
CODE_TYPE = int.from_bytes(b'c', 'little')
SHORT_TUPLE_TYPE = int.from_bytes(b')', 'little')
NONE_TYPE = int.from_bytes(b'N', 'little')
def parse_string(raw_bytes):
# Наш рулевой тут - https://github.com/python/cpython/blob/3.6/Python/marshal.c#L427
# Байтик на тип, четверочку на размер
SIZE = int.from_bytes(raw_bytes.read(4), 'little')
print("BYTES TO READ: ", SIZE)
s = raw_bytes.read(SIZE)
return s
def parse_code(raw_bytes):
CO_ARGCOUNT = struct.unpack(byte4_little, raw_bytes.read(4))[0]
co_kwonlyargcount = struct.unpack(byte4_little, raw_bytes.read(4))[0]
CO_NLOCALS = struct.unpack(byte4_little, raw_bytes.read(4))[0]
CO_STACKSIZE = struct.unpack(byte4_little, raw_bytes.read(4))[0]
CO_FLAGS = struct.unpack(byte4_little, raw_bytes.read(4))[0]
print("CODE STATS:", CO_ARGCOUNT, co_kwonlyargcount, CO_NLOCALS, CO_STACKSIZE, CO_FLAGS)
CODE_TYPE = raw_bytes.read(1)
# Тип строка
assert CODE_TYPE == b's'
CODE_ITSELF = parse_string(raw_bytes)
return CODE_ITSELF
def parse_long(raw_bytes):
VALUE = int.from_bytes(raw_bytes.read(4), 'little')
print("LONG VALUE:", VALUE)
return VALUE
def parse_none(raw_bytes):
# Уже отпрасили
return None
def parse_tuple(raw_bytes):
# Размер у маленького - 1 байт, а не 4
# https://github.com/python/cpython/blob/3.6/Python/marshal.c#L471
SIZE = int.from_bytes(raw_bytes.read(1), 'little')
print("TUPLE LEN:", SIZE)
# Следующий тип - SHORT_TUPLE. Также по аналогии
for index in range(SIZE):
NEXT_TYPE = int.from_bytes(raw_bytes.read(1), 'little')
if NEXT_TYPE == INTEGER_TYPE or NEXT_TYPE == INTEGER_TYPE | 128:
print("LONG TYPE")
parse_long(raw_bytes)
# None 1 байт
elif NEXT_TYPE == NONE_TYPE:
print("NONE TYPE")
parse_none(raw_bytes)
# code
elif NEXT_TYPE == CODE_TYPE or NEXT_TYPE == CODE_TYPE | 128:
print("CODE TYPE")
parse_code(raw_bytes)
elif NEXT_TYPE == SHORT_TUPLE_TYPE:
parse_tuple(raw_bytes)
with open("test.pyc", "rb") as f:
raw_bytes = BytesIO(f.read())
MAGIC_NUMBER = struct.unpack(byte2_little, raw_bytes.read(2))[0]
assert MAGIC_NUMBER == 3379
# пропускаем
raw_bytes.read(2)
TIMESTAMP = struct.unpack(byte4_little, raw_bytes.read(4))[0]
print(datetime.datetime.fromtimestamp(TIMESTAMP))
SIZE = struct.unpack(byte4_little, raw_bytes.read(4))[0]
print(SIZE)
OBJ_TYPE = int.from_bytes(raw_bytes.read(1), 'little')
# Объект типа TYPE_CODE - см. https://github.com/python/cpython/blob/3.6/Python/marshal.c#L46
# Флаг 128 выставляется в функции https://github.com/python/cpython/blob/3.6/Python/marshal.c#L288
assert OBJ_TYPE == CODE_TYPE | 128
parse_code(raw_bytes)
NEXT_TYPE = int.from_bytes(raw_bytes.read(1), 'little')
# Small tuple
if NEXT_TYPE == SHORT_TUPLE_TYPE:
parse_tuple(raw_bytes)
Код несколько неполный, обрабатывается только несколько типов, но, надеюсь, сама идея понятна - этот код - зеркальное отражение функции dumps - только там использовался w_object (write object) и свои записывалки для простых типов, вам же нужно r_object (read object) и читалки для простых. r_object уже написан, остается только его внимательно прочитать.
Комментариев нет:
Отправить комментарий