Ответ 1
Во-первых, на самом деле гораздо менее хакерский путь. Все, что мы хотим сделать, это изменить то, что print
отпечатки, не так ли?
_print = print
def print(*args, **kw):
args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg
for arg in args)
_print(*args, **kw)
Или, аналогично, вы можете monkeypatch sys.stdout
вместо print
.
Кроме того, ничего плохого в exec … getsource …
idea. Ну, конечно, там много не так, но меньше того, что следует здесь...
Но если вы хотите изменить константы кода объекта функции, мы можем это сделать.
Если вы действительно хотите поиграть с объектами кода для реального, вы должны использовать библиотеку, такую как bytecode
(когда он закончен) или byteplay
(до тех пор или для более старых версий Python), вместо того, чтобы делать это вручную. Даже для чего-то такого тривиального, инициализатор CodeType
- это боль; если вам действительно нужно делать что-то вроде исправления lnotab
, только сумасшедший будет делать это вручную.
Кроме того, само собой разумеется, что не все реализации Python используют объекты кода типа CPython. Этот код будет работать в CPython 3.7, и, вероятно, все версии вернутся, по меньшей мере, к 2.2 с небольшими изменениями (а не с хакерами кода, но такими, как генераторные выражения), но это не сработает ни с одной версией 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 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 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()
Что может пойти не так, как взломать объекты кода? В основном просто segfaults, RuntimeError
которые съедают весь стек, более нормальное RuntimeError
которое можно обрабатывать, или значения мусора, которые, вероятно, просто поднимут TypeError
или AttributeError
когда вы попытаетесь их использовать. Например, попробуйте создать объект кода с помощью только RETURN_VALUE
с ничем в стеке (байт-код b'S\0'
для 3. 6+, b'S'
before) или с пустым кортежем для co_consts
когда LOAD_CONST 0
в байт-коде, или с varnames
уменьшенными на 1, поэтому самый высокий LOAD_FAST
фактически загружает ячейку freevar/cellvar. Для какой-то настоящей забавы, если вы неправильно используете lnotab
, ваш код будет только segfault при запуске в отладчике.
Использование bytecode
или byteplay
не защитит вас от всех этих проблем, но у них есть некоторые основные проверки на здравомыслие и хорошие помощники, которые позволяют вам делать такие вещи, как вставлять кусок кода и не беспокоиться об обновлении всех смещений и меток, чтобы вы не может ошибиться и так далее. (Кроме того, они не позволяют вам вводить этот нелепый 6-строчный конструктор и должны отлаживать глупые опечатки, которые исходят от этого).
Теперь на # 2.
Я упомянул, что объекты кода неизменяемы. И, конечно, константы - кортеж, поэтому мы не можем напрямую это изменить. И вещь в кортете const - это строка, которую мы также не можем изменить напрямую. Вот почему мне пришлось построить новую строку для создания нового кортежа для создания нового кодового объекта.
Но что, если вы можете напрямую изменить строку?
Ну, достаточно глубоко под обложками, все просто указатель на некоторые данные С, не так ли? Если вы используете CPython, там есть API C для доступа к объектам, и вы можете использовать ctypes
для доступа к этому API из самого Python, что является такой ужасной идеей, что они помещают pythonapi
прямо в модуль stdlib ctypes
. :) Самый важный трюк, который вам нужно знать, это то, что id(x)
является фактическим указателем на x
в памяти (как int
).
К сожалению, C API для строк не позволит нам безопасно получить во внутреннем хранилище уже замороженной строки. Поэтому будьте осторожны, давайте просто прочитайте файлы заголовков и найдите это хранилище.
Если вы используете CPython 3.4 - 3.7 (он отличается для более старых версий и кто знает в будущем), строковый литерал из модуля, который сделан из чистого ASCII, будет храниться с использованием компактного формата ASCII, что означает, что структура заканчивается раньше, и буфер ASCII-байтов следует сразу в памяти. Это сломается (как, вероятно, segfault), если вы поместите символ не ASCII в строку или некоторые типы нелиберальных строк, но вы можете прочитать другие 4 способа доступа к буферу для разных типов строк.
Чтобы сделать вещи немного проще, я использую проект superhackyinternals
у своего GitHub. (Он намеренно не устанавливается в pp-installable, потому что вы действительно не должны использовать это, кроме как экспериментировать с вашей локальной сборкой интерпретатора и т.п.).
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()
Если вы хотите играть с этим материалом, int
намного проще под обложками, чем str
. И гораздо проще угадать, что вы можете сломать, изменив значение 2
на 1
, верно? Собственно, забудьте представить себе, давайте просто сделаем это (снова используя типы superhackyinternals
):
>>> 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
... притворяйтесь, что в кодовом окне есть полоса прокрутки бесконечной длины.
Я пробовал то же самое в IPython, и в первый раз, когда я попытался оценить 2
в подсказке, он попал в какой-то непрерывный бесконечный цикл. Предположительно, он использует номер 2
для чего-то в своем REPL-цикле, в то время как интерпретатор запаса нет?