首先,实际上存在的黑客方式要少得多。我们要做的就是更改
_print = printdef print(*args, **kw): args = (arg.replace('cat', 'dog') if isinstance(arg, str) else arg for arg in args) _print(*args, **kw)或者,类似地,您可以选择猴子补丁
sys.stdout而不是
同样,这个
exec … getsource …想法也没有错。好吧,这当然有 很多 问题,但比这里的要少…
但是,如果您确实想修改函数对象的代码常量,则可以这样做。
如果您真的想真正使用代码对象,则应该使用
bytepre(完成时)或
byteplay(直到那时,或者对于较旧的Python版本)之类的库,而不是手动执行。即使对于这种琐碎的事情,
CodeType初始化器还是很痛苦的。如果您确实需要做一些固定的工作
lnotab,那么只有疯子才会手动进行。
另外,不用说,并非所有的Python实现都使用CPython风格的代码对象。这段代码将在CPython
3.7中运行,并且可能所有版本至少可以回溯到2.2,并进行了一些细微的更改(不是代码黑客的东西,而是生成器表达式之类的东西),但不适用于任何版本的IronPython。
import typesdef print_function(): print ("This cat was scared.")def main(): # A function object is a wrapper around a pre object, with # a bit of extra stuff like default values and closure cells. # See inspect module docs for more details. co = print_function.__pre__ # A pre object is a wrapper around a string of bytepre, with a # whole bunch of extra stuff, including a list of constants used # by that bytepre. Again see inspect module docs. Anyway, inside # the bytepre 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, pre 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_pre, 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.__pre__ = co print_function()main()破解代码对象可能会出什么问题?大多数情况下,segfaults
RuntimeError会耗尽整个堆栈,更正常
RuntimeError的segfault会被处理,或者垃圾值可能只会引发a
TypeError或
AttributeError当您尝试使用它们时。例如,尝试创建一个代码对象,该对象仅带有一个
RETURN_VALUE在堆栈上没有任何内容的字节码
b'S '(3.6+
b'S'之前的字节码),或者在字节码中有一个空元组(
co_consts当
LOAD_CONST0字节码中有a时),或者
varnames递减1,因此最高的
LOAD_FAST实际上加载了一个freevar /
cellvar单元格。为了获得真正的乐趣,如果您
lnotab弄错了足够多的代码,则仅当在调试器中运行时,您的代码才会出现段错误。
使用
bytepre或
byteplay不会保护您免受所有这些问题的困扰,但是它们确实具有一些基本的健全性检查,并且好的助手可以让您执行诸如插入代码块之类的事情,并让其担心更新所有偏移量和标签,以便您能够弄错了,依此类推。(此外,它们使您不必键入该可笑的6行构造函数,也不必调试由此产生的愚蠢的错字。)
现在进入第二。
我提到代码对象是不可变的。当然,const是一个元组,因此我们不能直接更改它。const元组中的东西是一个字符串,我们也不能直接更改它。这就是为什么我必须构建一个新的字符串来构建一个新的元组来构建一个新的代码对象的原因。
但是,如果您可以直接更改字符串怎么办?
好吧,在足够深入的内容下,所有内容都只是指向某些C数据的指针,对吗?如果使用的是CPython,则有一个C
API可以访问对象,并且可以使用
ctypes它从Python本身内部访问该API,这是一个很糟糕的想法,他们将它们放在
pythonapistdlib的
ctypes模块中。:)您需要了解的最重要的技巧
id(x)是实际指向
x内存的指针(作为
int)。
不幸的是,字符串的C
API无法让我们安全地获取已经冻结的字符串的内部存储。因此,请放心,我们只需要读取头文件并自己找到该存储即可。
如果您使用的是CPython
3.4-3.7(旧版本有所不同,谁知道未来),那么将使用紧凑的ASCII格式存储由纯ASCII组成的模块中的字符串文字。提早结束,并且ASCII字节的缓冲区立即在内存中。如果您在字符串或某些非文字字符串中输入非ASCII字符,这将中断(可能在段错误中),但是您可以阅读其他4种方法来访问不同类型字符串的缓冲区。
为了使事情变得简单一些,我在
superhackyinternalsGitHub上使用了该项目。(这是故意不可以点子安装的,因为除了试验本地的解释器之类的东西之外,您实际上不应该使用此工具。)
import ctypesimport internals # https://github.com/abarnert/superhackyinternals/blob/master/internals.pydef print_function(): print ("This cat was scared.")def main(): for c in print_function.__pre__.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.PyUnipreObject.from_address(id(c)) assert p.compact and p.ascii addr = id(c) + internals.PyUnipreObject.utf8_length.offset buf = (ctypes.c_int8 * 3).from_address(addr + idx) buf[:3] = b'dog' print_function()main()如果您想玩这些东西,
int则比起隐藏起来要简单得多
str。而且,通过更改
2to的值来猜测可以破坏什么,容易得多
1,对吗?实际上,忘记想象,让我们开始(
superhackyinternals再次使用类型):
>>> n = 2>>> pn = PyLongObject.from_address(id(n))>>> pn.ob_digit[0]2>>> pn.ob_digit[0] = 1>>> 21>>> n * 33>>> i = 10>>> while i < 40:... i *= 2... print(i)101010
…假设代码框具有无限长的滚动条。
我在IPython中尝试过同样的事情,并且第一次尝试
2在提示符下进行评估,它陷入了某种不间断的无限循环。大概
2是在REPL循环中将数字用于某物,而股票解释器不是吗?



