python - 是否有可能 "hack"Python 的打印功能?

注意:此问题仅供引用。我很想知道如何深入了解 Python 的内部结构。

不久前,某家内部开始讨论question关于在调用 print 之后/期间是否可以修改传递给打印语句的字符串已经完成。例如,考虑函数:

def print_something():
    print('This cat was scared.')

现在,当 print运行,那么终端的输出应该显示:
This dog was scared.

注意“猫”这个词已经被“狗”这个词代替了。某处某处能够以某种方式修改这些内部缓冲区以更改打印的内容。假设这是在没有原始代码作者明确许可的情况下完成的(因此,黑客/劫持)。

此 comment特别是来自明智的@abernert,让我想到:

There are a couple of ways to do that, but they're all very ugly, and should never be done. The least ugly way is to probably replace the code object inside the function with one with a different co_consts list. Next is probably reaching into the C API to access the str's internal buffer. [...]



所以,看起来这实际上是可能的。

这是我解决这个问题的天真方法:
>>> import inspect
>>> exec(inspect.getsource(print_something).replace('cat', 'dog'))
>>> print_something()
This dog was scared.

当然,exec很糟糕,但这并不能真正回答问题,因为它实际上并没有在何时/之后修改任何内容 print被称为。

正如@abernert 所解释的那样,它将如何完成?

最佳答案

首先,实际上有一种不那么笨拙的方法。我们要做的就是改变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)

或者,类似地,您可以使用monkeypatch 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()

破解代码对象会出现什么问题?大多数只是段错误,RuntimeError s 吃掉整个堆栈,更正常 RuntimeError可以处理的 s,或者可能只会引发 TypeError 的垃圾值或 AttributeError当您尝试使用它们时。例如,尝试创建一个只有 RETURN_VALUE 的代码对象。堆栈上没有任何内容(字节码 b'S\0' 用于 3.6+,b'S' 之前),或带有空元组用于 co_consts当有 LOAD_CONST 0在字节码中,或使用 varnames减1所以最高LOAD_FAST实际上加载了一个 freevar/cellvar 单元。如果您收到 lnotab 以获得真正的乐趣,错了,您的代码只会在调试器中运行时出现段错误。

使用 bytecodebyteplay不会保护您免受所有这些问题的影响,但它们确实有一些基本的健全性检查和不错的助手,可让您执行诸如插入一大块代码并让它担心更新所有偏移量和标签之类的事情,因此您无法获得它错了,等等。 (另外,它们使您不必输入那个荒谬的 6 行构造函数,并且不必调试由此产生的愚蠢的拼写错误。)

现在转到#2。

我提到代码对象是不可变的。当然,常量是一个元组,所以我们不能直接改变它。而 const 元组中的东西是一个字符串,我们也不能直接改变它。这就是为什么我必须构建一个新字符串来构建一个新元组来构建一个新的代码对象。

但是如果你可以直接改变一个字符串呢?

好吧,隐藏的足够深,一切都只是指向某些 C 数据的指针,对吗?如果你使用 CPython,有 a C API to access the objects , 和 you can use ctypes to access that API from within Python itself, which is such a terrible idea that they put a pythonapi right there in the stdlib's ctypes module . :) 你需要知道的最重要的技巧是 id(x)是指向 x 的实际指针在内存中(作为 int )。

不幸的是,字符串的 C API 不会让我们安全地获得一个已经卡住的字符串的内部存储。所以安全拧紧,让我们只是read the header files并自己找到那个存储空间。

如果您使用的是 CPython 3.4 - 3.7(旧版本不同, future 谁知道),来自由纯 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 的值,可以更容易地猜测您可以破坏什么。至 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 循环中的某些内容,而股票解释器则不是?

关于python - 是否有可能 "hack"Python 的打印功能?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49271750/

相关文章:

android - adb 不显示 nexus 5 设备

python - 使用Python在SQLite中插入行后如何检索插入的ID?

linux - 选择 Linux I/O 调度程序

linux - 如何在不重新输入 SSH 密码的情况下执行 `git pull`?

python - 如何在不导入的情况下检查 Python 模块是否存在

python - bs4.FeatureNotFound : Couldn't find a tre

installation - 如何从命令行正确设置 CMAKE_INSTALL_PREFIX

python - numpy.where() 详细的分步说明/示例

linux - 什么包包括 AB Ubuntu 中的 Apache 服务器基准测试工具

python - pandas loc vs. iloc vs. at vs. iat?