注意:此问题仅供参考。我很想知道它可以深入 Python 的内部结构。
不久前,在某个 问题 中开始讨论是否可以在调用 print
之后/期间修改传递给 print 语句的字符串。例如,考虑以下功能:
def print_something():
print('This cat was scared.')
现在,当运行 print
时,终端的输出应该显示:
This dog was scared.
请注意,“猫”一词已被“狗”一词取代。某个地方的某些东西能够以某种方式修改这些内部缓冲区以更改打印的内容。假设这是在没有原始代码作者明确许可的情况下完成的(因此,黑客攻击/劫持)。
来自明智的@abarnert 的 评论 尤其让我思考:
有几种方法可以做到这一点,但它们都很丑陋,永远不应该这样做。最不丑陋的方法可能是用一个不同的
co_consts
列表替换函数内的code
对象。接下来可能是进入 C API 以访问 str 的内部缓冲区。 […]
所以,看起来这实际上是可能的。
这是我解决这个问题的天真方法:
>>> import inspect
>>> exec(inspect.getsource(print_something).replace('cat', 'dog'))
>>> print_something()
This dog was scared.
当然, exec
是不好的,但这并不能真正回答问题,因为它在调用 print
期间/之后 实际上并没有修改任何内容。
正如@abarnert 所解释的那样,它将如何完成?
原文由 cs95 发布,翻译遵循 CC BY-SA 4.0 许可协议
首先,实际上有一种更简单的方法。我们想要做的就是改变
print
打印的内容,对吗?或者,类似地,您可以使用 monkeypatch
sys.stdout
而不是print
。此外,
exec … getsource …
想法没有错。好吧,当然它有 很多 错误,但比这里接下来的要少……但是如果你确实想修改函数对象的代码常量,我们可以这样做。
如果你真的想真正地玩代码对象,你应该使用像
bytecode
(当它完成时)或byteplay
这样的库(直到那时,或者对于旧的 Python 版本)手动完成。即使对于这种微不足道的事情,CodeType
初始化器也是一种痛苦;如果你真的需要做一些像修复lnotab
这样的事情,只有疯子才会手动去做。此外,不用说,并非所有 Python 实现都使用 CPython 样式的代码对象。这段代码将在 CPython 3.7 中工作,并且可能所有版本至少回到 2.2 并进行一些小的更改(不是代码黑客的东西,而是生成器表达式之类的东西),但它不适用于任何版本的 IronPython。
破解代码对象会出什么问题?大多数只是段错误,
RuntimeError
s 吃掉整个堆栈,更正常的RuntimeError
可以处理的 s,或者可能只会引发的垃圾值TypeError
或AttributeError
当您尝试使用它们时。例如,尝试创建一个只有RETURN_VALUE
的代码对象,堆栈上没有任何内容(字节码b'S\0'
对于 3.6+,b'S'
),或者之前为空tuple forco_consts
when there’s aLOAD_CONST 0
in the bytecode, or withvarnames
decremented by 1 so the highestLOAD_FAST
actually loads a freevar/单元格单元格。为了一些真正的乐趣,如果你得到lnotab
足够错误,你的代码只会在调试器中运行时出现段错误。使用
bytecode
或byteplay
不会保护你免受所有这些问题的困扰,但它们确实有一些基本的健全性检查,以及可以让你做一些事情的好帮手,比如插入一段代码让它担心更新所有的偏移量和标签,这样你就不会弄错了,等等。 (另外,它们使您不必输入那个荒谬的 6 行构造函数,也不必调试由此产生的愚蠢拼写错误。)现在进入#2。
我提到代码对象是不可变的。当然,常量是一个元组,所以我们不能直接改变它。而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 种方法来访问不同类型字符串的缓冲区。
为了让事情稍微简单一些,我使用了我的 GitHub 上的
superhackyinternals
项目。 (它有意不能通过 pip 安装,因为你真的不应该使用它,除非你在本地构建解释器等进行试验。)如果你想玩这个东西,
int
在幕后比str
要简单得多。通过将 --- 的值更改为2
1
更容易猜出你能破解什么,对吧?实际上,忘记想象,让我们开始吧(再次使用superhackyinternals
中的类型):…假设代码框有一个无限长的滚动条。
我在 IPython 中尝试了同样的事情,我第一次尝试在提示符下计算
2
时,它进入了某种不可中断的无限循环。大概它在 REPL 循环中使用数字2
来表示某些内容,而股票解释器不是?