Страницы

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

Показаны сообщения с ярлыком байткод. Показать все сообщения
Показаны сообщения с ярлыком байткод. Показать все сообщения

четверг, 28 ноября 2019 г.

Расшифровать инфу из байтов

#шифрование #байткод


У меня есть 100 байт.
Вот они:

65 5A 44 5B 58 44 43 8A 4B 5A 4A 5B 41 4F 58 8A 42 4D 8A 40 4A 46 47 55 84
8A 68 8A 47 4F 45 5A 42 58 55 4D 4A 58 4F 41 56 47 51 5F 8A 40 8A 46 44 4E
4F 8A 45 44 5B 4F 41 4F 47 42 55 5F 8A 46 44 4C 4F 58 8A 5B 41 59 4C 42 58 
56 8A 48 8A 40 4A 5D 4F 5B 58 48 4F 8A 59 40 5A 4A 52 4F 47 42 55 84 A7 A0


Теперь они и у вас есть.
В них написан какой-то осмысленный текст. Предположительно русский (выдернуто из
игры). Байты как я понял перевернуты. Что это за кодировка такая, что там написано
и, чёрт возьми, кто тот герой, что сможет объяснить как их правильно конвертнуть в
этом случае?
    


Ответы

Ответ 1



Браслет из камня. В непритязательных к моде поселениях может служить в качестве украшения. Вы были абсолютно правы, текст, явно из игры :) Шифр элементарный. С каждый байтом строки в кодировке windows (1251) произведен XOR с 0xAA. Догадаться было не сложно, по частоте символов мы определили, что чаще всего встречается 8A, предположили, что это пробел. Далее я обратил внимание, что это практически единственный байт, который больше 0x80, остальные байты гораздо меньше. А как известно, у пробела самый маленький код 0x20, латинские символы начинаются с 0x41, а русские так вообще во второй половине таблицы, после 0x80. Получается все крупные числа превратились в мелкие, а маленькое число наоборот в большое - это явный признак операции XOR, тем более она обычно и используется в криптографии т.к. легко обратима, повторением операции. 0x8A xor 0x20 = 0xAA - вот наш ключ. Сделал маленькую прогу, которая считала ваши шестнадцеричиные коды символов, сделала из них числа и провела xor 0xAA. Текст перед нами. На perl моя программа выглядела так: #!/usr/bin/perl $a=<>; // Читаем строку из входного файла @b=split(/ /,$a); // Делаем массив hex кодов, они через проблел foreach $a (@b) // бежим по массиву { $n=hex($a) ^ 0xAA; // Делаем нормальное число из шестнадцетиричного и XOR 0xAA print(chr($n)); // Печатаем символ с этим кодом }

среда, 27 ноября 2019 г.

Зачем java-программисту знать как работает сборщик мусора и как устроен байт-код?

#java #jvm #сборщик_мусора #байткод


Все вокруг это говорят. Но как это применяется на практике? GC ведь и был придуман,
что бы облегчить программисту работу с памятью, что бы программист мог абстрагироваться
от этого. А нам часто говорят, надо понять, как работает GC. Практическое применение
байт-кода только одно вижу - это если человек пишет свой язык, на платформе JVM. А
еще это где-нибудь применяется на практике?
    


Ответы

Ответ 1



У сборщика мусора есть разные стратегии и реализации: Serial GC, Parallel GC, CMS GC, G1. Каждый из них обладает набором своих параметров, регулируя которые можно добиться тонкой настройки JVM под конкретное приложение (в зависимости от задач можно увеличить пропускную способность, уменьшить STW-задержки, уменьшить потребляемые ресурсы, грамотно выделить объемы памяти под молодое/старое поколения и т.п.). Естественно знать эти нюансы не обязательно и можно просто пользоваться стандартными настройками JVM... До тех пор пока ваше приложение не начинает пожирать всю доступную оперативную память или появляются задержки в его работе. Вот тогда-то эти знания и пригодятся. На мой взгляд, хороший программист тем и отличается, что он обладает широким кругозором и знает как работает тот инструмент, которым он пользуется.

Ответ 2



На практике когда вы используете sun.misc.Unsafe или сталкиваетесь с SoftReference, WeakReference, PhantomReference, или ваша система тормозит ввиду большого количества объектов и соответственно, частых вызовов GC, неплохо понимать как вся эта кухня работает изнутри. Тогда вы сможете решить возникшие проблемы несколько быстрее. Байт код вещь ещё более низкоуровневая, опять же, нужно понимать как работают определенные структуры кода, какой низкоуровневый код могут сгенерировать. Сразу вспомнился забавный синтетический пример: https://habrahabr.ru/post/245333/ Любой разработчик, который понимает что происходит на более низком уровне, будет писать более эффективный код. Начиная от элементарного, что спрашивается и освещается везде и всеми, вроде принципов работы коллекций, заканчивая работой с памятью на самом низком уровне процессорного кэша. А сборщик мусора - не тот инструмент, который всё сделает за вас, это лишь ваш небольшой помощник.

Ответ 3



Знания как работает сборщик мусора и как устроен байт-код, а также как устроена JVM в целом дают возможность кодить более эффективно. Особенно это важно на проектах где необходимо выжать максимум из JVM, так как ресурсы могут быть ограничены.

среда, 3 октября 2018 г.

Расшифровать инфу из байтов

У меня есть 100 байт. Вот они:
65 5A 44 5B 58 44 43 8A 4B 5A 4A 5B 41 4F 58 8A 42 4D 8A 40 4A 46 47 55 84 8A 68 8A 47 4F 45 5A 42 58 55 4D 4A 58 4F 41 56 47 51 5F 8A 40 8A 46 44 4E 4F 8A 45 44 5B 4F 41 4F 47 42 55 5F 8A 46 44 4C 4F 58 8A 5B 41 59 4C 42 58 56 8A 48 8A 40 4A 5D 4F 5B 58 48 4F 8A 59 40 5A 4A 52 4F 47 42 55 84 A7 A0
Теперь они и у вас есть. В них написан какой-то осмысленный текст. Предположительно русский (выдернуто из игры). Байты как я понял перевернуты. Что это за кодировка такая, что там написано и, чёрт возьми, кто тот герой, что сможет объяснить как их правильно конвертнуть в этом случае?


Ответ

Браслет из камня. В непритязательных к моде поселениях может служить в качестве украшения.
Вы были абсолютно правы, текст, явно из игры :)
Шифр элементарный. С каждый байтом строки в кодировке windows (1251) произведен XOR с 0xAA
Догадаться было не сложно, по частоте символов мы определили, что чаще всего встречается 8A, предположили, что это пробел. Далее я обратил внимание, что это практически единственный байт, который больше 0x80, остальные байты гораздо меньше. А как известно, у пробела самый маленький код 0x20, латинские символы начинаются с 0x41, а русские так вообще во второй половине таблицы, после 0x80. Получается все крупные числа превратились в мелкие, а маленькое число наоборот в большое - это явный признак операции XOR, тем более она обычно и используется в криптографии т.к. легко обратима, повторением операции. 0x8A xor 0x20 = 0xAA - вот наш ключ. Сделал маленькую прогу, которая считала ваши шестнадцеричиные коды символов, сделала из них числа и провела xor 0xAA. Текст перед нами.
На perl моя программа выглядела так:
#!/usr/bin/perl $a=<>; // Читаем строку из входного файла @b=split(/ /,$a); // Делаем массив hex кодов, они через проблел foreach $a (@b) // бежим по массиву { $n=hex($a) ^ 0xAA; // Делаем нормальное число из шестнадцетиричного и XOR 0xAA print(chr($n)); // Печатаем символ с этим кодом }

вторник, 2 октября 2018 г.

Как объяснить, что такое байткод?

Пытался объяснить начинающим инженерам, что такое байткод (в рамках пояснения принципов выполнения Java программ). Так вот, я пытаюсь стандартно объяснить, мол это что то между компиляцией и интерпретацией, типа каждая команда джавовского байт кода, это как несколько ассемблеровских комманд, но они смотрят на меня и говорят - не понятноооо! Они не программисты, не знают что такое ни компиляция, ни интерпретация, ни ассемблер, это я попытался на пальцах им объяснить, и вроде до них что-то дошло, а как можно на пальцах используя жизненные примеры объяснит принцип работы байткода?


Ответ

А нужно объяснять очень просто. Используя их положение. Для начала показываем этим инженерам программу на Java. Например, классический HelloWorld. И спрашиваем - понятно ли? Скорее всего они скажут нет. Объясняем, что и специальной программе, которая исполняет жава код, тоже не понятно. Для этого нужно "разобрать по косточкам". Теперь делаем "псевдотрасляцию" - как для машинистки (секретарши). Для HelloWorld'а она будет такая. настроить окружение (в коде этого нет, но это автоматом) - приготовить бумагу, проверить катриджи. взять из памяти строку "привет мир". нижимая кнопки, побуквенно ввести сроку (здесь появился цикл:) ). почистить все за собой и отнести бумагу заказчику. Формально - это и есть простой байткод. Только это человеческий байткод. А если в нем стандартизировать все операции и занумеровать, то все может быть сведено к набору чисел. Теперь к реальному байткоду перейти просто. Легко будет объяснить и переносимость. Если человек (секретарша) выучит все коды операций, то она сможет выполнить любую работу, главное, что бы была последовательность кодов. А инженеры могут попробовать спаять-сконструировать устройство, которое будет это исполнять.

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

Какова структура файла байткода 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 уже написан, остается только его внимательно прочитать.