modificando el bytecode de python

Me preguntaba cómo modificar el código de bytes y luego volver a comstackr ese código para poder usarlo en Python como una función. He estado intentando:

a = """ def fact(): a = 8 a = 0 """ c = compile(a, '', 'exec') w = c.co_consts[0].co_code dis(w) 

que descomstack a:

  0 LOAD_CONST 1 (1) 3 STORE_FAST 1 (1) 6 LOAD_CONST 2 (2) 9 STORE_FAST 1 (1) 12 LOAD_CONST 0 (0) 15 RETURN_VALUE 

Suponiendo que quiero deshacerme de las líneas 0 y 3, llamo:

 x = c.co_consts[0].co_code[6:16] dis(x) 

lo que resulta en :

  0 LOAD_CONST 2 (2) 3 STORE_FAST 1 (1) 6 LOAD_CONST 0 (0) 9 RETURN_VALUE 

mi problema es qué hacer con x , si bash exec x obtengo una ‘cadena esperada sin nullbytes y obtengo lo mismo para exec w , al intentar comstackr x resulta en: compile () cadena esperada sin bytes nulos.

No estoy seguro de cuál es la mejor manera de proceder, excepto que quizás deba crear algún tipo de objeto de código, pero no estoy seguro de cómo, pero supongo que debe ser posible, también conocido como byteplay, python assemblers et al

Estoy usando python 2.7.10, pero me gustaría que fuera compatible con el futuro (por ejemplo, python 3) si es posible.

Actualización: Por diversos motivos, comencé a escribir un ensamblador de la versión de Cross-Python. Ver https://github.com/rocky/python-xasm Todavía está en su versión beta muy temprana.

Por lo que sé, no hay un ensamblador Python actualmente mantenido . Bytecode Disassembler de PEAK se desarrolló para Python 2.6 y luego se modificó para que sea compatible con Python 2.7.

Está muy bien de la documentación . Pero se basa en otras bibliotecas PEAK que pueden ser problemáticas.

Repasaré todo el ejemplo para darte una idea de lo que tendrías que hacer. No es bonito, pero entonces deberías esperar eso.

Básicamente, después de modificar el bytecode, debe crear un nuevo types.CodeType objeto de types.CodeType . Necesitas uno nuevo porque muchos de los objetos en el tipo de código, por una buena razón, no puedes cambiarlos. Por ejemplo, el intérprete puede tener algunos de estos valores de objetos en caché.

Después de crear el código, puede usar esto en funciones que usan un tipo de código que se puede usar en exec o eval .

O puede escribir esto en un archivo de código de bytes. Por desgracia, el formato del código ha cambiado entre Python 2 y Python 3. Y, por cierto, también lo ha hecho la optimización y los códigos de bytes. De hecho, en Python 3.6 serán códigos de palabra , no códigos de bytes.

Así que esto es lo que tendrías que hacer para tu ejemplo:

 a = """ def fact(): a = 8 a = 0 return a """ c = compile(a, '', 'exec') fn_code = c.co_consts[0] # Pick up the function code from the main code from dis import dis dis(fn_code) print("=" * 30) x = fn_code.co_code[6:16] # modify bytecode import types opt_fn_code = types.CodeType(fn_code.co_argcount, # c.co_kwonlyargcount, Add this in Python3 fn_code.co_nlocals, fn_code.co_stacksize, fn_code.co_flags, x, # fn_code.co_code: this you changed fn_code.co_consts, fn_code.co_names, fn_code.co_varnames, fn_code.co_filename, fn_code.co_name, fn_code.co_firstlineno, fn_code.co_lnotab, # In general, You should adjust this fn_code.co_freevars, fn_code.co_cellvars) dis(opt_fn_code) print("=" * 30) print("Result is", eval(opt_fn_code)) # Now let's change the value of what's returned co_consts = list(opt_fn_code.co_consts) co_consts[-1] = 10 opt_fn_code = types.CodeType(fn_code.co_argcount, # c.co_kwonlyargcount, Add this in Python3 fn_code.co_nlocals, fn_code.co_stacksize, fn_code.co_flags, x, # fn_code.co_code: this you changed tuple(co_consts), # this is now changed too fn_code.co_names, fn_code.co_varnames, fn_code.co_filename, fn_code.co_name, fn_code.co_firstlineno, fn_code.co_lnotab, # In general, You should adjust this fn_code.co_freevars, fn_code.co_cellvars) dis(opt_fn_code) print("=" * 30) print("Result is now", eval(opt_fn_code)) 

Cuando corrí esto aquí es lo que obtuve:

  3 0 LOAD_CONST 1 (8) 3 STORE_FAST 0 (a) 4 6 LOAD_CONST 2 (0) 9 STORE_FAST 0 (a) 5 12 LOAD_FAST 0 (a) 15 RETURN_VALUE ============================== 3 0 LOAD_CONST 2 (0) 3 STORE_FAST 0 (a) 4 6 LOAD_FAST 0 (a) 9 RETURN_VALUE ============================== ('Result is', 0) 3 0 LOAD_CONST 2 (10) 3 STORE_FAST 0 (a) 4 6 LOAD_FAST 0 (a) 9 RETURN_VALUE ============================== ('Result is now', 10) 

Observe que los números de línea no han cambiado, aunque eliminé en el código un par de líneas. Eso es porque no actualicé fn_code.co_lnotab .

Si desea escribir ahora un archivo de código de bytes de Python desde este. Esto es lo que harías:

 co_consts = list(c.co_consts) co_consts[0] = opt_fn_code c1 = types.CodeType(c.co_argcount, # c.co_kwonlyargcount, Add this in Python3 c.co_nlocals, c.co_stacksize, c.co_flags, c.co_code, tuple(co_consts), c.co_names, c.co_varnames, c.co_filename, c.co_name, c.co_firstlineno, c.co_lnotab, # In general, You should adjust this c.co_freevars, c.co_cellvars) from struct import pack with open('/tmp/testing.pyc', 'w') as fp: fp.write(pack('Hcc', 62211, '\r', '\n')) # Python 2.7 magic number import time fp.write(pack('I', int(time.time()))) # In Python 3 you need to write out the size mod 2**32 here import marshal fp.write(marshal.dumps(c1)) 

Para simplificar la escritura del código de bytes repetitivo anterior, he agregado una rutina a xdis llamada write_python_file () .

Ahora para comprobar los resultados:

 $ uncompyle6 /tmp/testing.pyc # uncompyle6 version 2.9.2 # Python bytecode 2.7 (62211) # Disassembled from: Python 2.7.12 (default, Jul 26 2016, 22:53:31) # [GCC 5.4.0 20160609] # Embedded file name:  # Compiled at: 2016-10-18 05:52:13 def fact(): a = 0 # okay decompiling /tmp/testing.pyc $ pydisasm /tmp/testing.pyc # pydisasm version 3.1.0 # Python bytecode 2.7 (62211) disassembled from Python 2.7 # Timestamp in code: 2016-10-18 05:52:13 # Method Name:  # Filename:  # Argument count: 0 # Number of locals: 0 # Stack size: 1 # Flags: 0x00000040 (NOFREE) # Constants: # 0: ", line 2> # 1: None # Names: # 0: fact 2 0 LOAD_CONST 0 (", line 2>) 3 MAKE_FUNCTION 0 6 STORE_NAME 0 (fact) 9 LOAD_CONST 1 (None) 12 RETURN_VALUE # Method Name: fact # Filename:  # Argument count: 0 # Number of locals: 1 # Stack size: 1 # Flags: 0x00000043 (NOFREE | NEWLOCALS | OPTIMIZED) # Constants: # 0: None # 1: 8 # 2: 10 # Local variables: # 0: a 3 0 LOAD_CONST 2 (10) 3 STORE_FAST 0 (a) 4 6 LOAD_CONST 0 (None) 9 RETURN_VALUE $ 

Un enfoque alternativo para la optimización es optimizar en el nivel de árbol de syntax abstracta (AST). No sé cómo generar un archivo de código de bytes de un AST. Así que supongo que escribes esto de vuelta como fuente de Python, si eso es posible.

Sin embargo, tenga en cuenta que algunos tipos de optimización, como la eliminación de la recursión de la cola, pueden dejar el código de bytes en una forma que no se pueda transformar de una manera verdaderamente fiel al código fuente. Vea mi charla de pycon2018 columbia relámpago para un video que hice que termina la recursión de la cola en el código de bytes para tener una idea de lo que estoy hablando aquí.