首先,实际上没有那么多hacky方式。我们要做的就是更改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()
入侵代码对象可能会出什么问题?大多数情况下,segfaults RuntimeError
会耗尽整个堆栈,更正常RuntimeError
的segfault 会被处理,或者垃圾值可能只会引发a TypeError
或AttributeError
当您尝试使用它们时。例如,尝试创建一个代码对象,该对象只带有一个RETURN_VALUE
在堆栈上没有任何内容的字节码b'S\0'
(3.6以上的字节码,b'S'
之前),或者一个空元组(表示字节码中是否co_consts
有a LOAD_CONST 0
,或者varnames
减1,因此最高的字节LOAD_FAST
实际上加载了一个freevar) / cellvar单元格。为了获得一些真正的乐趣,如果您lnotab
弄错了太多,那么只有在调试器中运行代码时,您的代码才会出现段错误。
使用bytecode
或byteplay
不会保护您免受所有这些问题的影响,但是它们确实具有一些基本的健全性检查,并且好的助手可以让您执行诸如插入代码块之类的事情,并使其担心更新所有偏移量和标签,以便您能够弄错了,等等。(此外,它们使您不必键入该可笑的6行构造函数,也不必调试由此产生的愚蠢的错字。)
现在进入第二。
我提到代码对象是不可变的。当然,const是一个元组,因此我们不能直接更改它。const元组中的东西是一个字符串,我们也不能直接更改它。这就是为什么我必须构建一个新的字符串来构建一个新的元组来构建一个新的代码对象的原因。
但是,如果您可以直接更改字符串怎么办?
好吧,在足够深入的内容下,所有内容都只是指向某些C数据的指针,对吗?如果您使用的是CPython,则有一个C API可以访问对象,并且可以使用ctypes
它从Python本身内部访问该API,这是一个很糟糕的想法,他们将它们放在pythonapi
stdlib的ctypes
模块中。:)您需要了解的最重要的技巧id(x)
是实际指向x
内存的指针(作为int
)。
不幸的是,用于字符串的C API不能让我们安全地获取已经冻结的字符串的内部存储。因此,请放心,我们只需要阅读头文件并自己找到该存储即可。
如果您使用的是CPython 3.4-3.7(旧版本有所不同,谁知道未来),那么将使用紧凑ASCII格式存储由纯ASCII组成的模块中的字符串文字。提早结束,并且ASCII字节的缓冲区立即在内存中。如果您在字符串或某些非文字字符串中输入非ASCII字符,这将中断(可能在段错误中),但是您可以阅读其他4种方式来访问不同类型字符串的缓冲区。
为了使事情变得简单一些,我在superhackyinternals
GitHub上使用了该项目。(这是有意不可pip安装的,因为您真的不应该使用它,除非尝试在本地构建解释器等。)
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
to 的值来猜测可以破坏什么,容易得多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"
。