Innanzitutto, c'è in realtà un modo molto meno confuso. Tutto quello che vogliamo fare è cambiare ciò che print
stampa, giusto?
_print = print
def print(*args, **kw):
args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg
for arg in args)
_print(*args, **kw)
Oppure, allo stesso modo, puoi monkeypatch sys.stdout
invece di print
.
Inoltre, nulla di sbagliato exec … getsource …
nell'idea. Beh, ovviamente c'è molto di sbagliato in questo, ma meno di quello che segue qui ...
Ma se si desidera modificare le costanti di codice dell'oggetto funzione, possiamo farlo.
Se vuoi davvero giocare con oggetti di codice per davvero, dovresti usare una libreria come bytecode
(quando è finita) o byteplay
(fino ad allora, o per le versioni precedenti di Python) invece di farlo manualmente. Anche per qualcosa di così banale, l' CodeType
inizializzatore è un dolore; se hai davvero bisogno di fare cose come sistemare lnotab
, solo un pazzo lo farebbe manualmente.
Inoltre, è ovvio che non tutte le implementazioni di Python utilizzano oggetti di codice in stile CPython. Questo codice funzionerà in CPython 3.7 e probabilmente tutte le versioni tornano almeno alla 2.2 con alcune piccole modifiche (e non le cose di hacking del codice, ma cose come le espressioni del generatore), ma non funzioneranno con nessuna versione di IronPython.
import types
def print_function():
print ("This cat was scared.")
def main():
# A function object is a wrapper around a code object, with
# a bit of extra stuff like default values and closure cells.
# See inspect module docs for more details.
co = print_function.__code__
# A code object is a wrapper around a string of bytecode, with a
# whole bunch of extra stuff, including a list of constants used
# by that bytecode. Again see inspect module docs. Anyway, inside
# the bytecode for string (which you can read by typing
# dis.dis(string) in your REPL), there's going to be an
# instruction like LOAD_CONST 1 to load the string literal onto
# the stack to pass to the print function, and that works by just
# reading co.co_consts[1]. So, that's what we want to change.
consts = tuple(c.replace("cat", "dog") if isinstance(c, str) else c
for c in co.co_consts)
# Unfortunately, code objects are immutable, so we have to create
# a new one, copying over everything except for co_consts, which
# we'll replace. And the initializer has a zillion parameters.
# Try help(types.CodeType) at the REPL to see the whole list.
co = types.CodeType(
co.co_argcount, co.co_kwonlyargcount, co.co_nlocals,
co.co_stacksize, co.co_flags, co.co_code,
consts, co.co_names, co.co_varnames, co.co_filename,
co.co_name, co.co_firstlineno, co.co_lnotab,
co.co_freevars, co.co_cellvars)
print_function.__code__ = co
print_function()
main()
Cosa potrebbe andare storto con l'hacking di oggetti in codice? Principalmente solo segfault, RuntimeError
che consumano l'intero stack, più normali RuntimeError
che possono essere gestiti, o valori di immondizia che probabilmente aumenteranno solo TypeError
o AttributeError
quando si tenta di usarli. Ad esempio, prova a creare un oggetto codice con solo un RETURN_VALUE
senza nulla nello stack (bytecode b'S\0'
per 3.6+, b'S'
prima), o con una tupla vuota per co_consts
quando c'è un LOAD_CONST 0
nel bytecode o con varnames
decrementato di 1 in modo che il più alto LOAD_FAST
carichi effettivamente un freevar / cellvar cell. Per un vero divertimento, se lnotab
sbagli abbastanza, il tuo codice segfault solo quando eseguito nel debugger.
Utilizzando bytecode
o byteplay
non ti proteggeranno da tutti questi problemi, ma hanno alcuni controlli di integrità di base e simpatici aiutanti che ti consentono di fare cose come inserire un pezzo di codice e lasciarti preoccupare di aggiornare tutti gli offset e le etichette in modo da poter ' non sbagliare, e così via. (Inoltre, ti impediscono di dover digitare quel ridicolo costruttore di 6 righe e di dover eseguire il debug degli errori di battitura che derivano dal farlo.)
Passiamo ora al n. 2.
Ho detto che gli oggetti di codice sono immutabili. E ovviamente i contro sono una tupla, quindi non possiamo cambiarlo direttamente. E la cosa nella tupla const è una stringa, che anche noi non possiamo cambiare direttamente. Ecco perché ho dovuto creare una nuova stringa per creare una nuova tupla per creare un nuovo oggetto codice.
E se potessi cambiare direttamente una stringa?
Bene, abbastanza in profondità sotto le coperte, tutto è solo un puntatore ad alcuni dati C, giusto? Se stai usando CPython, c'è un'API C per accedere agli oggetti e puoi usare ctypes
per accedere pythonapi
a quell'API dall'interno di Python stesso, il che è un'idea così terribile che si inseriscono nel ctypes
modulo di stdlib . :) Il trucco più importante che devi sapere è che id(x)
è il puntatore reale x
in memoria (come int
).
Sfortunatamente, l'API C per le stringhe non ci consente di accedere in modo sicuro alla memoria interna di una stringa già bloccata. Quindi avvitatelo in sicurezza, leggiamo solo i file di intestazione e troviamo da soli l'archiviazione.
Se stai usando CPython 3.4 - 3.7 (è diverso per le versioni precedenti e chissà per il futuro), una stringa letterale da un modulo fatto di puro ASCII verrà memorizzata usando il formato compatto ASCII, il che significa che la struttura termina presto e il buffer di byte ASCII segue immediatamente in memoria. Questo si interromperà (come nel caso probabilmente del segfault) se si inserisce un carattere non ASCII nella stringa o determinati tipi di stringhe non letterali, ma è possibile leggere gli altri 4 modi per accedere al buffer per diversi tipi di stringhe.
Per semplificare leggermente le cose, sto usando il superhackyinternals
progetto su GitHub. (Non è intenzionalmente installabile tramite pip perché in realtà non dovresti usarlo se non per sperimentare la tua build locale dell'interprete e simili.)
import ctypes
import internals # https://github.com/abarnert/superhackyinternals/blob/master/internals.py
def print_function():
print ("This cat was scared.")
def main():
for c in print_function.__code__.co_consts:
if isinstance(c, str):
idx = c.find('cat')
if idx != -1:
# Too much to explain here; just guess and learn to
# love the segfaults...
p = internals.PyUnicodeObject.from_address(id(c))
assert p.compact and p.ascii
addr = id(c) + internals.PyUnicodeObject.utf8_length.offset
buf = (ctypes.c_int8 * 3).from_address(addr + idx)
buf[:3] = b'dog'
print_function()
main()
Se vuoi giocare con queste cose, int
è molto più semplice sotto le coperte di str
. Ed è molto più facile indovinare cosa puoi rompere cambiando il valore di 2
a 1
, giusto? In realtà, dimentica di immaginare, facciamolo (usando di superhackyinternals
nuovo i tipi ):
>>> n = 2
>>> pn = PyLongObject.from_address(id(n))
>>> pn.ob_digit[0]
2
>>> pn.ob_digit[0] = 1
>>> 2
1
>>> n * 3
3
>>> i = 10
>>> while i < 40:
... i *= 2
... print(i)
10
10
10
... fingi che la casella del codice abbia una barra di scorrimento di lunghezza infinita.
Ho provato la stessa cosa in IPython e la prima volta che ho provato a valutare 2
al prompt, è andato in una specie di loop infinito ininterrotto. Presumibilmente sta usando il numero 2
per qualcosa nel suo ciclo REPL, mentre l'interprete di borsa non lo è?
42
per23
rispetto perché è una cattiva idea di modificare il valore di"My name is Y"
a"My name is X"
.