DEV Community

Cover image for Como crear bytecode en python
Aitor
Aitor

Posted on • Edited on

Como crear bytecode en python

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.
Alt Text

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)

Enter fullscreen mode Exit fullscreen mode

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
>>>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    )
Enter fullscreen mode Exit fullscreen mode

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 |)
Enter fullscreen mode Exit fullscreen mode

"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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Esto es todo por ahora, despues voy a subír otro tutorial explicando las closures.

Fuentes

Top comments (0)