Como crear bytecode en python
Aitor
Posted on November 19, 2020
Introducción
Este es un tutorial de como crear bytecode pelado, o bytecode crudo en python, primero había arrancado a hacer este tutorial en github.io, pero después un grupo de pandilleros virtuales me golpearon y me hicieron dar cuenta de que no tenia sentido utilizar eso teniendo esta maravilla.
Pre-requisitos
- Conocimientos básicos de Python
- Saber que es un objeto de bytes (bytes object)
- Conocer el concepto de stack
¿Qué es Python?
Python es un lenguaje de programación interpretado multiparadigma, soporta polimorfismo, programación orientada a objetos (POO/OOP) y programación imperativa.
¿Como funciona?
Python como ya se nombró, es un lenguaje interpretado, esto quiere decir que pasa a través de un interpretador que conecta lo que la computadora va a hacer, con lo que une escriba. Python no genera un código de máquina como generaría un programa en C o C++, sino que funciona más o menos como Java, tiene una maquina virtual que interpreta bytecode. Este intérprete por defecto es CPython es el que se encarga de ejecutar el bytecode en tu computadora. Acá no vamos a utilizar compiladores, si no que vamos a manejar implementaciones del lenguaje, básicamente intérpretes que justamente, interpretan el código escrito luego de traducirlo a bytecode. Existe una amplia variedad de estos, e.g IronPython (Implementación en C#), Jython (Implementación hecha a puro Java), Micropython (Version hecha en C y optimizada para ejecutarse en microcontroladores).
Acá hay un esquema de como Python funciona y los pasos que el intérprete toma para llegar a ejecutar el código que vos escribiste.
Como crear bytecode UTILIZABLE
Bueno, tenemos dos cosas, primero, bytecode pelado, es decír, bytes en hexadecimal representando opcodes y parámetros, y en segundo lugar, tenemos CodeType
, un tipo de datos en Python que nos sirve para crear ByteCode que SÍ SIRVA. Igualmente para armar, hay que saber como se desarma, vamos a utilizar el módulo dis para desarmar nuestra función, este módulo es utiliza para des-ensamblar funciones, archivos y código.
import dis
def suma(x, y):
return x+y
dis.dis(suma)
La salida de ese retazo de código es la siguiente
1. 4 0 LOAD_FAST 0(x)
2. 2 LOAD_FAST 1(y)
3. 4 BINARY_ADD
4. 6 RETURN_VALUE
>>>
Como podemos ver, todo eso es bytecode, ahora la explicación.
Como habrán observado, enumeré las lineas que hay en la salida con el fin de hacer mas fácil esta explicación.
Cada instrucción en Python tiene un OPCODE (Código de operación) específico, en este caso usamos 3, LOAD_FAST BINARY_ADD RETURN_VALUE
, vamos a explicar que hace cada uno.
- LOAD_FAST : Carga una variable a la cima del stack (Top Of Stack).
- BINARY_ADD : Suma los dos valores que hay en la cima del stack y los devuelve a la cima del stack.
- RETURN_VALUE : Devuelve el valor que esté en TOS.
Bueno, ahora que explicamos los opcodes, podemos darnos una idea de como funciona internamente nuestro código, pero aún hay dudas, dudas molestas pero necesarias, como por ejemplo esta "¿Qué es el 4 en la parte izquierda, el 4 que esta al inicio de la primer linea?", "¿Qué son los números a la izquierda de los OPCODES?"¿Por qué aparece un 0 a la derecha de LOAD_FAST?" "¿Y el 1?", "¿No querríamos cargar x
e y
para sumarlos en vez de 0 y 1?".
Bueno, voy a responder en orden.
- El 4 es la línea donde comienza el bytecode des-ensamblado.
- Estos números representan el offset de los bytes.
- El 0 y el 1 corresponden a un índice, ya que las variables del código, son almacenadas en una lista (array), los 0 y 1 representan el índice, no obstante, el módulo dis nos dice que variable es a la derecha de este número (de ahí el
0 (x)
y 1(y)
).*
¿Cómo re-creamos nuestra función para hacerla bytecode?
Bueno, lo primero que hacemos es importar CodeType
y FunctionType
(Para pasarlo a función) desde el módulo types
import dis
from types import CodeType, FunctionType
def suma(x, y):
return x+y
Luego de esto, vamos a crear nuestro código objeto
import dis
from types import CodeType, FunctionType
def suma(x, y):
return x+y
# Esto lo voy a explicar despues, son flags
CO_OPTIMIZED = 0x0001
CO_NEWLOCALS = 0x0002
CO_NOFREE = 0x0002
mi_codigo = CodeType(
2, #argcount
0, #kwonlyargcount
2, #nlocals
2, #stacksize
(CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE), #flags
bytes([124, 0, 124, 1, 23, 0, 83, 0]), #codestring
(0,), #constantes
(), #nombres de las constantes o globales (names)
('x','y',), #nombres de variables (varnames)
'blog_sin_nombre', #filename
'suma_crafteada', #name (nombre del codigo/funcion)
9, #Firstlineno (Primer linea donde aparece este cod.)
b'', #lnotab
(), #freevars
() ,#freecellvars
)
Bueno bueno... Muchas cosas nuevas, vamos a explicar los argumentos a ver.
CodeType: argcount, kwonlyargcount, nlocals, stacksize, flags, codestring, constants, names, varnames, filename, name, firstlineno, lnotab, freevars, freecellvars
Argumento | Descripción |
---|---|
argcount | Cantidad de argumentos |
kwonlyargcount | Cantidad de keyword arguments |
nlocals | Numero de variables locales (En este caso 2, x e y) |
stacksize | Máximo tamaño en bytes que va a tener el stack (En este caso 2 porque x+y requiere dos espacios en el stack frame) |
flags | Las banderas son lo que determinan algunas condiciones del bytecode, podes guiarte por esta referencia. Nos vamos a adentrar en flags en un tutorial mas avanzado. |
codestring | Esto es una lista(array) de bytes conteniendo lasequencia en cuestión, en el 124 significa LOAD_FAST, 23 BINARY_ADD y 83 RETURN_VALUE |
constants | Una tupla con el valor de las constantes (Como numeros enteros, False, True, funciones built-in...) |
names | Una tupla conteniendo el nombre de las constantes respectivamente |
varnames | Nombre de variables locales |
filename | Esta string representa el nombre del archivo, cuando no se usa este valor puede ser cualquier string |
name | Nombre del code object o la función |
firstlineno | Representa la primer línea en la que se ejecuta el código, relevante si importamos un archivo, de otra manera puede ser cualquier numero entero |
lnotab | Esto es un mapeo entre los offsets del bytecode object y el offset de las lineas, si no te interesa poner información de las líneas, podes usar b''
|
freevars | Estas variables las voy a explicar en un tutorial avanzado, se utiliza en closures |
cellvars | Estas variables son definidas dentro de una closure |
Unas ultimas dos cosas para remarcar antes de pasar a FunctionType
, la primera es que los 0 que le siguen a los opcodes e.g [124, 0, ...] son el argumento, y la segunda es que cada bytecode puede variar de versión en versión, para saber u orientarte sobre el codestring, podes utilizar el siguiente snippet
def suma(x,y):
return x+y
suma.__code__.co_code
# Salida esperada en Python 3.7.9 (La versión que yo uso)
# b'|\x00|\x01\x17\x00S\x00'
# Los bytes los interpreta como characters, probablemente para que sea mas legible. (Si ponemos chr(124) nos va a imprmír el carácter |)
"Crafteando" la función
Vamos a utilizar FunctionType ahora.
FunctionType: code, globals, name, argdefs, closure
Argumento | Descripción |
---|---|
code | Código objeto (osea, CodeType) |
globals | Un diccionario conteniendo las globales del siguiente modo `{ "Nombre": ValorNombre}` de ese modo, Nombre pasa a ser un identificador, y luego se accede a el como si fuese una variable |
name (Opcional) | Sobreescribe el valor que tiene el código objeto) |
argdefs (Opcional) | Una tupla que especifica el valor de los argumentos por defecto |
closure (Opcional) | Una tupla que suple los lazos para las freevars |
Bueno, una vez esto claro, ahora solo nos quedaría agregar una FunctionType con nuestro código objeto (mi_codigo
) y llamarla.
import dis
from types import CodeType, FunctionType
def suma(x, y):
return x+y
Luego de esto, vamos a crear nuestro código objeto
import dis
from types import CodeType, FunctionType
def suma(x, y):
return x+y
# Esto lo voy a explicar despues, son flags
CO_OPTIMIZED = 0x0001
CO_NEWLOCALS = 0x0002
CO_NOFREE = 0x0002
mi_codigo = CodeType(
2, #argcount
0, #kwonlyargcount
2, #nlocals
2, #stacksize
(CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE), #flags
bytes([124, 0, 124, 1, 23, 0, 83, 0]), #codestring
(0,), #constantes
(), #nombres de las constantes o globales (names)
('x','y',), #nombres de variables (varnames)
'blog_sin_nombre', #filename
'suma_crafteada', #name (nombre del codigo/funcion)
9, #Firstlineno (Primer linea donde aparece este cod.)
b'', #lnotab
(), #freevars
() ,#freecellvars
)
_suma = FunctionType(mi_codigo, {})
resultado = _suma(213,3)
print(resultado)
# Salida esperada
# 216
Esto es todo por ahora, despues voy a subír otro tutorial explicando las closures.
Fuentes
Posted on November 19, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.