Большинство основных языков, включая Java, .NET, такие как C #, Python, компилируются в промежуточный язык. У этих языков есть интерпретаторы (например, интерпретатор Python) или виртуальные машины (например, виртуальная машина Java), которые выполняют байт-коды. Эти байт-коды либо генерируются на лету (как в случае с Python), либо сохраняются в формате файла (формат файла класса Java в Java и CIL или Common Intermediate Language для .NET).

Изучение того, как читать байт-коды, может быть на удивление полезным, хотя лишь немногие разработчики когда-либо пробуют это делать (даже разработчики экспертного уровня). Основное преимущество - понимание того, как язык работает «изнутри», то есть как язык и его функции работают. В более практическом плане понимание байт-кодов может помочь вам отладить сложные сценарии, дать представление о проблемах производительности и помочь написать лучший код, который использует возможности языка.

В этом кратком сообщении блога давайте начнем с дизассемблирования байт-кода Python.

Что такое «разборка»? Это противоположность сборки - процесс преобразования кода сборки в машинный код (для реальной машины или виртуальной машины). Дизассемблирование будет означать обратное - процесс преобразования машинного кода в ассемблерный код.

Начнем с примера. Запустите свой интерпретатор Python (я использую Python 3.7 в этом сообщении в блоге) и попробуйте прочитать фактические байт-коды:

>>> def some_fun():
...     a = 10
...     b = 20
...     c = 30
...     return a * b + c
... 
>>> some_fun.__code__
<code object some_fun at 0x1075e9420, file "<stdin>", line 1>
>>> some_fun.__code__.co_code
b'd\x01}\x00d\x02}\x01d\x03}\x02|\x00|\x01\x14\x00|\x02\x17\x00S\x00'

Мы определили простую функцию с именем some_fun, которая оценивает выражение a * b + c и возвращает результат.

__Code__ показывает «объект кода, представляющий тело скомпилированной функции». Затем мы вызываем co_code, который является «строкой, представляющей последовательность инструкций байт-кода». Это определенно не читается человеком, и поэтому мы собираемся использовать модуль dis, чтобы увидеть версию байт-кода, читаемую человеком.

>>> import dis
>>> dis.dis(some_fun)
  2           0 LOAD_CONST               1 (10)
              2 STORE_FAST               1 (a)

  3           4 LOAD_CONST               2 (20)
              6 STORE_FAST               2 (b)

  4           8 LOAD_CONST               3 (30)
             10 STORE_FAST               3 (c)

  5          12 LOAD_FAST                1 (a)
             14 LOAD_FAST                2 (b)
             16 BINARY_MULTIPLY
             18 LOAD_FAST                3 (c)
             20 BINARY_ADD
             22 RETURN_VALUE
>>>

Сначала импортируйте модуль «dis». Выражение dis.dis (some_fun) разбирает some_fun и показывает байт-код. Давайте внимательнее посмотрим, что он показывает.

В первом столбце показаны номера строк исходного кода и соответствующие записи в дизассемблированном байт-коде. Второй столбец показывает индекс байт-кода. Третий столбец - это фактические байт-коды, такие как LOAD_CONST, BINARY_MULTIPLY и т. Д.

Python - это стековый язык. Чтобы понять этот промежуточный формат, подумайте о пост-исправлении эквивалента данного выражения «a * b + c» после исправления - это будет «a b * c +», и это то, что достигают следующие байт-коды:

             12 LOAD_FAST                1 (a)
             14 LOAD_FAST                2 (b)
             16 BINARY_MULTIPLY
             18 LOAD_FAST                3 (c)
             20 BINARY_ADD
             22 RETURN_VALUE

Байт-коды могут принимать аргументы. Например, LOAD_FAST принимает значение «a» (с индексом «1») в качестве аргумента. Сам байт-код - это один байт (поскольку это «байтовый» код, как указывает имя), а аргумент - 1 байт в этом случае, то есть всего 2 байта для «LOAD_FAST 1 (a)». Текущий индекс байт-кода равен 12, и добавление 2 дает следующий индекс байт-кода, начиная с 14, который равен «LOAD_FAST 2 (b)».

Все программы на Python конвертируются в байт-код. Глядя на байт-коды, можно «прояснить» то, как работают некоторые функции. Вот пример:

>>> def square_fun(a):
...     return a * a
... 
>>> dis.dis(square_fun)
  2           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                0 (a)
              4 BINARY_MULTIPLY
              6 RETURN_VALUE
>>> square_fun
<function square_fun at 0x1076d8598>
>>> square_lambda = lambda a: a * a 
>>> dis.dis(square_lambda)
  1           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                0 (a)
              4 BINARY_MULTIPLY
              6 RETURN_VALUE
>>> square_lambda
<function <lambda> at 0x1076141e0>

Этот пример короткий, приятный и передает мощный посыл: несмотря на различия в синтаксисе, функция и лямбда-код компилируются в один и тот же код! Как я и обещал ранее, теперь вы знаете, что я имел в виду под этим, может дать вам «понимание того, как язык работает« изнутри », то есть как язык и его функции работают!»

Так чего же вы ждете: просто запустите свой интерпретатор, поиграйте и исследуйте байт-коды Python - поверьте, это приключение, полное веселья!

Ссылки / Дополнительная литература:

  1. Модель данных Python (например, __code__): https://docs.python.org/3/reference/datamodel.html
  2. Список байт-кодов Python: https://docs.python.org/2/library/dis.html#bytecodes
  3. Дизассемблер для байт-кодов Python: https://docs.python.org/2/library/dis.html
  4. Понимание байт-кодов Python: https://www.synopsys.com/blogs/software-security/understanding-python-bytecode/
  5. Видео по байт-кодам: https://www.youtube.com/watch?v=cSSpnq362Bk и https://www.youtube.com/watch?v=GNPKBICTF2w

(Автор: Ганеш Самартям, соучредитель, KonfHub Technologies LLP)