По-перше, насправді існує набагато менш хакітний спосіб. Все, що ми хочемо зробити, - це змінити 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)
Або, аналогічно, ви можете sys.stdout
замість маніпулювання print
.
Також нічого поганого в exec … getsource …
ідеї. Ну, звичайно, тут багато не так, але менше, ніж тут випливає…
Але якщо ви хочете змінити константи коду об'єкта функції, ми можемо це зробити.
Якщо ви дійсно хочете пограти з кодовими об'єктами по-справжньому, вам слід використовувати бібліотеку на кшталт 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'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()
Що може піти не так у злому об'єктів коду? Здебільшого це лише segfault, RuntimeError
які з’їдають цілий стек, більш нормальні RuntimeError
s, з якими можна обробляти, або значення сміття, які, ймовірно, просто піднімають a TypeError
або AttributeError
при спробі їх використовувати. Наприклад, спробуйте створити об'єкт коду просто RETURN_VALUE
зі b'S\0'
значком на стеку (байт-код для 3.6+ b'S'
раніше) або з порожнім кортежем для того, co_consts
коли є LOAD_CONST 0
байт-код, або з varnames
зменшеним на 1, тому найвищий LOAD_FAST
насправді завантажує фрівар / клітина клітин. Для справжньої розваги, якщо ви отримаєте lnotab
досить неправильну, ваш код буде лише за замовчуванням під час запуску в налагоджувачі.
Використання bytecode
або byteplay
не захистить вас від усіх цих проблем, але у них є деякі основні перевірки на корисність та приємні помічники, які дозволяють вам робити такі речі, як вставити шматок коду і дозволяти турбуватися про оновлення всіх компенсацій та міток, щоб ви могли " t неправильно і так далі. (Крім того, вони не дозволяють вам набирати цей смішний 6-лінійний конструктор і не вимагати налагодження дурних друкарських помилок, які випливають із цього.)
Тепер до №2.
Я згадав, що об'єкти коду незмінні. І, звичайно, конкурси є кортежем, тому ми не можемо змінити це безпосередньо. А річ у const кортежі - це струна, яку ми також не можемо безпосередньо змінити. Ось чому мені довелося побудувати новий рядок, щоб побудувати новий кортеж, щоб побудувати новий об'єкт коду.
Але що робити, якщо ви могли змінити рядок безпосередньо?
Ну, досить глибоко під обкладинками, все лише вказівник на деякі дані С, правда? Якщо ви використовуєте CPython, для доступу до об'єктів є API C , і ви можете використовувати його ctypes
для доступу до цього API зсередини самого Python, що є такою жахливою ідеєю, що вони розміщують pythonapi
тут же ctypes
модуль stdlib . :) Найголовніший трюк, який потрібно знати, це те, що id(x)
є фактичним вказівником на x
пам'ять (як на int
).
На жаль, API C для рядків не дозволить нам спокійно потрапити на внутрішнє зберігання вже замороженої рядки. Тож накрутіть сміливо, давайте просто прочитаємо файли заголовків і самі знайдемо це сховище.
Якщо ви використовуєте CPython 3.4 - 3.7 (це відрізняється від старих версій, і хто знає на майбутнє), рядковий літерал з модуля, виготовлений із чистого ASCII, буде зберігатися у компактному форматі ASCII, що означає структуру закінчується рано, і в пам'яті негайно випливає буфер байтів ASCII. Це порушиться (як, напевно, у сегментах), якщо ви додасте символ не ASCII у рядок або певні види нелітеральних рядків, але ви можете прочитати інші 4 способи доступу до буфера для різних типів рядків.
Щоб зробити щось легше, я використовую superhackyinternals
проект від мого GitHub. .
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, тоді як перекладач запасів не є?
42
в23
ніж чому це погана ідея , щоб змінити значення"My name is Y"
для"My name is X"
.