注意:此问题仅供参考。我很想知道这样做有多深入到Python内部。
不久前,在某个问题的内部开始了一个讨论,该问题涉及传递给print语句的字符串是否可以在调用to之后/期间进行修改print。例如,考虑以下功能:
print
def print_something(): print('This cat was scared.')
现在,当print运行时,到终端的输出应显示:
This dog was scared.
请注意,单词“ cat”已被单词“ dog”代替。某处某种方式能够修改这些内部缓冲区以更改打印的内容。假设这是在没有原始代码作者明确许可的情况下完成的(因此,黑客/劫持)。
这个评论从智者@abarnert,尤其让我思考:
有两种方法可以做到这一点,但是它们都很丑陋,绝不应该这样做。最丑陋的方法是code用一个带有不同co_consts 列表的对象替换 函数内部的对象。接下来可能是进入C API以访问str的内部缓冲区。[…]
code
co_consts
因此,看起来这实际上是可能的。
这是我解决此问题的幼稚方式:
>>> import inspect >>> exec(inspect.getsource(print_something).replace('cat', 'dog')) >>> print_something() This dog was scared.
当然,这exec很糟糕,但这并不能真正回答问题,因为 在 print调用 when / after之后 ,它实际上并未进行任何修改。
exec
正如@abarnert解释的那样,它将如何进行?
首先,实际上存在的黑客方式要少得多。我们要做的就是更改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。
sys.stdout
同样,这个exec … getsource …想法也没有错。好吧,这当然有 很多 问题,但比这里的要少…
exec … getsource …
但是,如果您确实想修改函数对象的代码常量,则可以这样做。
如果您真的想真正使用代码对象,则应该使用bytecode(完成时)或byteplay(直到那时,或者对于较旧的Python版本)之类的库,而不是手动执行。即使对于这种琐碎的事情,CodeType初始化器还是很痛苦的。如果您确实需要做一些固定的工作lnotab,那么只有疯子才会手动进行。
bytecode
byteplay
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()
破解代码对象可能会出什么问题?大多数情况下,segfaultsRuntimeError会耗尽整个堆栈,更正常RuntimeError的segfault会被处理,或者垃圾值可能只会引发aTypeError或AttributeError当您尝试使用它们时。例如,尝试创建一个代码对象,该对象仅带有一个RETURN_VALUE在堆栈上没有任何内容的字节码b'S\0'(3.6+b'S'之前的字节码),或者在字节码中有一个空元组(co_consts当LOAD_CONST 0字节码中有a时),或者varnames递减1,因此最高的LOAD_FAST实际上加载了一个freevar / cellvar单元格。为了获得真正的乐趣,如果您lnotab弄错了足够多的代码,则仅当在调试器中运行时,您的代码才会出现段错误。
RuntimeError
TypeError
AttributeError
RETURN_VALUE
b'S\0'
b'S'
LOAD_CONST 0
varnames
LOAD_FAST
使用bytecode或byteplay不会保护您免受所有这些问题的困扰,但是它们确实具有一些基本的健全性检查,并且好的助手可以让您执行诸如插入代码块之类的事情,并让其担心更新所有偏移量和标签,以便您能够弄错了,依此类推。(此外,它们使您不必键入该可笑的6行构造函数,也不必调试由此产生的愚蠢的错字。)
现在进入第二。
我提到代码对象是不可变的。当然,const是一个元组,因此我们不能直接更改它。const元组中的东西是一个字符串,我们也不能直接更改它。这就是为什么我必须构建一个新的字符串来构建一个新的元组来构建一个新的代码对象的原因。
但是,如果您可以直接更改字符串怎么办?
好吧,在足够深入的内容下,所有内容都只是指向某些C数据的指针,对吗?如果使用的是CPython,则有一个C API可以访问对象,并且可以使用ctypes它从Python本身内部访问该API,这是一个很糟糕的想法,他们将它们放在pythonapistdlib的ctypes模块中。:)您需要了解的最重要的技巧id(x)是实际指向x内存的指针(作为int)。
ctypes
pythonapi
id(x)
x
int
不幸的是,字符串的C API无法让我们安全地获取已经冻结的字符串的内部存储。因此,请放心,我们只需要读取头文件并自己找到该存储即可。
如果您使用的是CPython 3.4-3.7(旧版本有所不同,谁知道未来),那么将使用紧凑的ASCII格式存储由纯ASCII组成的模块中的字符串文字。提早结束,并且ASCII字节的缓冲区立即在内存中。如果您在字符串或某些非文字字符串中输入非ASCII字符,这将中断(可能在段错误中),但是您可以阅读其他4种方法来访问不同类型字符串的缓冲区。
为了使事情变得简单一些,我在superhackyinternalsGitHub上使用了该项目。(这是故意不可以点子安装的,因为除了试验本地的解释器之类的东西之外,您实际上不应该使用此工具。)
superhackyinternals
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。而且,通过更改2to的值来猜测可以破坏什么,容易得多1,对吗?实际上,忘记想象,让我们开始(superhackyinternals再次使用类型):
str
2
1
>>> 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循环中将数字用于某物,而股票解释器不是吗?