Страницы

Поиск по вопросам

понедельник, 1 октября 2018 г.

Какова структура файла байткода CPython?

Пытаюсь разобраться в структуре скомпилированного байт-кода 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 - строка "" (зачем?) 01 ... - дальше не знаю что
Помогите восполнить пробелы. Если бы мне надо было просто узнать значение какой-нибудь константы, то мне бы имеющихся знаний хватило. Но я пытаюсь написать интерпретатор байт-кода 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('', file) source_bytes = loader.get_data(file) Исходники преобразовываются в AST - Абстрактное Синтаксическое Дерево. Невозможно тут написать, какой путь проходит исходник через череду всяких функций и проверок, но вкратце: вызывается функция compile
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"byte1_big = b">c" byte2_big = b">h" byte4_big = b">I"
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 уже написан, остается только его внимательно прочитать.

Комментариев нет:

Отправить комментарий