Premièrement, il existe en fait une manière beaucoup moins piratée. Tout ce que nous voulons faire, c'est changer les print
impressions, non?
_print = print
def print(*args, **kw):
args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg
for arg in args)
_print(*args, **kw)
Ou, de même, vous pouvez monkeypatch sys.stdout
au lieu de print
.
De plus, rien de mal avec l' exec … getsource …
idée. Eh bien, bien sûr, il y a beaucoup de mal à cela, mais moins que ce qui suit ici ...
Mais si vous souhaitez modifier les constantes de code de l'objet fonction, nous pouvons le faire.
Si vous voulez vraiment jouer avec des objets de code pour de vrai, vous devriez utiliser une bibliothèque comme bytecode
(quand c'est fini) ou byteplay
(jusque-là, ou pour les anciennes versions de Python) au lieu de le faire manuellement. Même pour quelque chose d'aussi trivial, l' CodeType
initialiseur est une douleur; si vous avez réellement besoin de faire des choses comme réparer lnotab
, seul un fou le ferait manuellement.
De plus, il va sans dire que toutes les implémentations Python n'utilisent pas des objets de code de style CPython. Ce code fonctionnera dans CPython 3.7, et probablement toutes les versions remontant à au moins 2.2 avec quelques modifications mineures (et non les trucs de piratage de code, mais des choses comme les expressions de générateur), mais il ne fonctionnera avec aucune version d'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()
Qu'est-ce qui pourrait mal tourner avec le piratage des objets de code? Surtout juste des segfaults, des RuntimeError
s qui consomment toute la pile, des RuntimeError
s plus normales qui peuvent être gérées, ou des valeurs de garbage qui ne feront probablement que lever un TypeError
ou AttributeError
lorsque vous essayez de les utiliser. Pour des exemples, essayez de créer un objet de code avec juste un RETURN_VALUE
sans rien sur la pile (bytecode b'S\0'
pour 3.6+, b'S'
avant), ou avec un tuple vide pour co_consts
quand il y a un LOAD_CONST 0
dans le bytecode, ou avec varnames
décrémenté de 1 pour que le plus élevé LOAD_FAST
charge réellement une freevar / cellvar cellule. Pour vous amuser vraiment, si vous vous lnotab
trompez suffisamment, votre code ne fera que segfault lorsqu'il sera exécuté dans le débogueur.
Utiliser bytecode
ou byteplay
ne vous protégera pas de tous ces problèmes, mais ils ont quelques vérifications de base de la cohérence et de bons assistants qui vous permettent de faire des choses comme insérer un morceau de code et le laisser s'inquiéter de la mise à jour de tous les décalages et étiquettes afin que vous puissiez ' t se tromper, et ainsi de suite. (De plus, ils vous évitent d'avoir à taper dans ce ridicule constructeur de 6 lignes et de déboguer les fautes de frappe qui en découlent.)
Passons maintenant au n ° 2.
J'ai mentionné que les objets de code sont immuables. Et bien sûr, les consts sont un tuple, donc nous ne pouvons pas changer cela directement. Et la chose dans le tuple const est une chaîne, que nous ne pouvons pas non plus changer directement. C'est pourquoi j'ai dû créer une nouvelle chaîne pour créer un nouveau tuple afin de créer un nouvel objet de code.
Mais que se passerait-il si vous pouviez changer une chaîne directement?
Eh bien, assez profondément sous les couvertures, tout n'est qu'un pointeur vers des données C, non? Si vous utilisez CPython, il existe une API C pour accéder aux objets , et vous pouvez l'utiliser ctypes
pour accéder à cette API à partir de Python lui-même, ce qui est une idée tellement terrible qu'ils ont mis un pythonapi
droit là dans le ctypes
module de stdlib . :) L'astuce la plus importante que vous devez savoir est que id(x)
c'est le pointeur vers x
en mémoire (en tant que int
).
Malheureusement, l'API C pour les chaînes ne nous permettra pas d'accéder en toute sécurité au stockage interne d'une chaîne déjà figée. Alors vissez en toute sécurité, lisons simplement les fichiers d'en-tête et trouvons ce stockage nous-mêmes.
Si vous utilisez CPython 3.4 - 3.7 (c'est différent pour les anciennes versions, et qui sait pour le futur), une chaîne littérale d'un module en ASCII pur sera stockée en utilisant le format ASCII compact, ce qui signifie que la structure se termine tôt et le tampon d'octets ASCII suit immédiatement en mémoire. Cela cassera (comme probablement dans segfault) si vous mettez un caractère non-ASCII dans la chaîne, ou certains types de chaînes non littérales, mais vous pouvez lire les 4 autres façons d'accéder au tampon pour différents types de chaînes.
Pour rendre les choses un peu plus faciles, j'utilise le superhackyinternals
projet depuis mon GitHub. (Ce n'est intentionnellement pas installable par pip car vous ne devriez vraiment pas l'utiliser sauf pour expérimenter votre version locale de l'interpréteur et autres.)
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()
Si vous voulez jouer avec ce truc, int
c'est beaucoup plus simple sous les couvertures que str
. Et il est beaucoup plus facile de deviner ce que vous pouvez briser en changeant la valeur de 2
à 1
, non? En fait, oubliez d'imaginer, faisons-le simplement (en utilisant à superhackyinternals
nouveau les types de ):
>>> 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
… Prétendez que la zone de code a une barre de défilement infinie.
J'ai essayé la même chose dans IPython, et la première fois que j'ai essayé d'évaluer 2
à l'invite, cela s'est passé dans une sorte de boucle infinie ininterrompue. Il utilise probablement le nombre 2
pour quelque chose dans sa boucle REPL, contrairement à l'interpréteur de stock?
42
pour23
que pourquoi il est une mauvaise idée de changer la valeur"My name is Y"
à"My name is X"
.