Primeiro, há realmente uma maneira muito menos invasiva. Tudo o que queremos fazer é mudar o que é print
impresso, certo?
_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, da mesma forma, você pode monkeypatch em sys.stdout
vez de print
.
Além disso, nada de errado com a exec … getsource …
ideia. Bem, é claro que há muito errado com isso, mas menos do que se segue aqui…
Mas se você deseja modificar as constantes de código do objeto de função, podemos fazer isso.
Se você realmente quiser brincar com objetos de código de verdade, use uma biblioteca como bytecode
(quando terminar) ou byteplay
(até então, ou para versões mais antigas do Python) em vez de fazê-lo manualmente. Mesmo para algo tão trivial, o CodeType
inicializador é uma dor; se você realmente precisa fazer coisas como consertar lnotab
, apenas um lunático faria isso manualmente.
Além disso, é evidente que nem todas as implementações do Python usam objetos de código no estilo CPython. Esse código funcionará no CPython 3.7 e, provavelmente, todas as versões voltarão para pelo menos 2.2 com algumas pequenas alterações (e não as coisas de hackers de código, mas coisas como expressões de gerador), mas não funcionará com nenhuma versão do 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()
O que poderia dar errado ao hackear objetos de código? Principalmente apenas segfaults, RuntimeError
s que consomem toda a pilha, RuntimeError
s mais normais que podem ser manipulados ou valores de lixo que provavelmente apenas aumentam um TypeError
ou AttributeError
quando você tenta usá-los. Por exemplo, tente criar um objeto de código com apenas a RETURN_VALUE
com nada na pilha (bytecode b'S\0'
para 3.6+ ou b'S'
anterior) ou com uma tupla vazia para co_consts
quando houver um LOAD_CONST 0
no bytecode ou com varnames
decrementado por 1 para que o mais alto LOAD_FAST
realmente carregue um freevar / célula cellvar. Para se divertir de verdade, se você lnotab
errar o suficiente, seu código será apenas falhado quando executado no depurador.
Usar bytecode
ou byteplay
não protegê-lo de todos esses problemas, mas eles têm algumas verificações básicas de integridade e bons ajudantes que permitem que você insira um pedaço de código e se preocupe em atualizar todos os desvios e etiquetas para que você possa ' Não entendi errado, e assim por diante. (Além disso, eles impedem que você precise digitar o ridículo construtor de 6 linhas e depurar os erros de bobagem que surgem com isso.)
Agora vamos para o # 2.
Mencionei que os objetos de código são imutáveis. E é claro que os consts são uma tupla, então não podemos mudar isso diretamente. E a coisa na tupla constante é uma string, que também não podemos mudar diretamente. É por isso que eu tive que criar uma nova string para criar uma nova tupla para criar um novo objeto de código.
Mas e se você pudesse alterar uma string diretamente?
Bem, profundo o suficiente debaixo das cobertas, tudo é apenas um ponteiro para alguns dados C, certo? Se você estiver usando o CPython, existe uma API C para acessar os objetos , e você pode usá-la ctypes
para acessar a API a partir do próprio Python, o que é uma péssima idéia que eles colocam pythonapi
ali no ctypes
módulo do stdlib . :) O truque mais importante que você precisa saber é que id(x)
é o ponteiro real para a x
memória (como um int
).
Infelizmente, a API C para seqüências de caracteres não nos permite acessar com segurança o armazenamento interno de uma sequência já congelada. Portanto, com segurança, vamos apenas ler os arquivos de cabeçalho e encontrar esse armazenamento.
Se você estiver usando o CPython 3.4 - 3.7 (é diferente para versões mais antigas e quem sabe o futuro), uma string literal de um módulo feito de ASCII puro será armazenada usando o formato ASCII compacto, que significa a estrutura termina cedo e o buffer de bytes ASCII segue imediatamente na memória. Isso quebrará (como provavelmente no segfault) se você colocar um caractere não ASCII na string ou certos tipos de strings não literais, mas você poderá ler as outras 4 maneiras de acessar o buffer para diferentes tipos de strings.
Para tornar as coisas um pouco mais fáceis, estou usando o superhackyinternals
projeto no meu GitHub. (Não é intencionalmente instalável pelo pip, porque você realmente não deveria usá-lo, exceto para experimentar a compilação local do intérprete e assim por diante.)
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 você quiser brincar com essas coisas, int
é muito mais simples do que isso str
. E é muito mais fácil adivinhar o que você pode quebrar alterando o valor de 2
para 1
, certo? Na verdade, esqueça de imaginar, vamos fazê-lo (usando os tipos de superhackyinternals
novo):
>>> 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
… Finja que a caixa de código tem uma barra de rolagem de comprimento infinito.
Tentei a mesma coisa no IPython e, na primeira vez em que tentei avaliar 2
no prompt, ele entrou em algum tipo de loop infinito ininterrupto. Presumivelmente, ele está usando o número 2
para algo em seu loop REPL, enquanto o intérprete de ações não está?
42
para do23
que por que é uma má idéia alterar o valor de"My name is Y"
para"My name is X"
.