代码之家  ›  专栏  ›  技术社区  ›  cs95 abhishek58g

有没有可能“破解”Python的打印功能?

  •  151
  • cs95 abhishek58g  · 技术社区  · 7 年前

    注:此问题仅供参考。我很感兴趣的是,看看Python的内部结构有多深。

    不久前,一场讨论在某 question 关于在调用之后/期间是否可以修改传递给print语句的字符串 print 已制作。例如,考虑函数:

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

    现在,什么时候 打印 则终端的输出应显示:

    This dog was scared.
    

    请注意,“cat”一词已被“dog”一词取代。不知何故,某个地方的某些东西能够修改这些内部缓冲区,以更改打印的内容。假设这是在没有原始代码作者明确许可的情况下完成的(因此,黑客/劫持)。

    comment 特别是从智者的角度,abarnert让我想到:

    有几种方法可以做到这一点,但它们都很难看 不应该这样做。最不丑陋的方法可能是更换 code 对象,其中一个具有不同的 co_consts 列表下一步可能是访问C API以访问str的 内部缓冲器。[…]

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

    以下是我处理这个问题的天真方式:

    >>> import inspect
    >>> exec(inspect.getsource(print_something).replace('cat', 'dog'))
    >>> print_something()
    This dog was scared.
    

    当然 exec 不好,但这并不能真正回答问题,因为它实际上并没有修改任何内容 何时/之后 打印 被调用。

    正如@abarnert所解释的那样,这将如何实现?

    4 回复  |  直到 7 年前
        1
  •  248
  •   Engineero    6 年前

    首先,实际上有一种更简单的方法。我们想做的就是改变 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 而不是 打印


    还有,没有什么问题 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 这会吃掉整堆,更正常 访问违例 或垃圾值,这些值可能只会引发 TypeError AttributeError 当你尝试使用它们时。例如,尝试使用 RETURN_VALUE 堆栈上没有任何内容(字节码 b'S\0' 对于3.6+, b'S' 之前),或具有空元组 co_consts 当有 LOAD_CONST 0 在字节码中,或使用 varnames 递减1,因此最高 LOAD_FAST 实际加载freevar/cellvar单元格。如果你得到 lnotab公司 如果错误足够大,代码只有在调试器中运行时才会出现segfault。

    使用 字节码 byteplay公司 不会保护您免受所有这些问题的影响,但它们确实有一些基本的健全性检查,还有一些很好的帮助程序,可以让您插入一段代码,让它担心更新所有偏移量和标签,这样您就不会出错,等等。(此外,它们还可以让您不必输入荒谬的6行构造函数,也不必调试由此产生的愚蠢的拼写错误。)


    现在转到#2。

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

    但如果可以直接更改字符串呢?

    好吧,在足够深的范围内,一切都只是指向一些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(旧版本不同,谁知道将来会怎样),那么来自纯ASCII模块的字符串文字将使用压缩ASCII格式存储,这意味着结构会提前结束,ASCII字节的缓冲区会立即在内存中跟随。如果在字符串中放入非ASCII字符或某些类型的非文字字符串,这将中断(可能是segfault),但您可以通过其他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()
    

    如果你想玩这个东西, 内景 隐藏起来要比 str 。通过更改 2 1 正当实际上,忘记想象吧,让我们来做吧(使用 超级ackyinternals 再次):

    >>> 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循环中的某些内容,而stock解释器不是?

        2
  •  38
  •   MSeifert    7 年前

    猴子补丁 print

    打印 是一个内置函数,因此它将使用 打印 中定义的函数 builtins 模块(或 __builtin__ 在Python 2中)。因此,每当您想要修改或更改内置函数的行为时,只需在该模块中重新分配名称即可。

    此过程称为 monkey-patching

    # Store the real print function in another variable otherwise
    # it will be inaccessible after being modified.
    _print = print  
    
    # Actual implementation of the new print
    def custom_print(*args, **options):
        _print('custom print called')
        _print(*args, **options)
    
    # Change the print function globally
    import builtins
    builtins.print = custom_print
    

    此后,每年 打印 电话将接通 custom_print ,即使 打印 位于外部模块中。

    但是,您并不想打印其他文本,而是想更改打印的文本。一种方法是将其替换为要打印的字符串:

    _print = print  
    
    def custom_print(*args, **options):
        # Get the desired seperator or the default whitspace
        sep = options.pop('sep', ' ')
        # Create the final string
        printed_string = sep.join(args)
        # Modify the final string
        printed_string = printed_string.replace('cat', 'dog')
        # Call the default print function
        _print(printed_string, **options)
    
    import builtins
    builtins.print = custom_print
    

    事实上,如果你跑步:

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

    或者如果您将其写入文件:

    test\u文件。py公司

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

    并导入它:

    >>> import test_file
    This dog was scared.
    >>> test_file.print_something()
    This dog was scared.
    

    所以它真的按预期工作。

    但是,如果您只是暂时想使用修补程序打印,可以将其包装在上下文管理器中:

    import builtins
    
    class ChangePrint(object):
        def __init__(self):
            self.old_print = print
    
        def __enter__(self):
            def custom_print(*args, **options):
                # Get the desired seperator or the default whitspace
                sep = options.pop('sep', ' ')
                # Create the final string
                printed_string = sep.join(args)
                # Modify the final string
                printed_string = printed_string.replace('cat', 'dog')
                # Call the default print function
                self.old_print(printed_string, **options)
    
            builtins.print = custom_print
    
        def __exit__(self, *args, **kwargs):
            builtins.print = self.old_print
    

    因此,运行时,它取决于打印的内容:

    >>> with ChangePrint() as x:
    ...     test_file.print_something()
    ... 
    This dog was scared.
    >>> test_file.print_something()
    This cat was scared.
    

    这就是你“黑客”的方式 打印 通过猴子修补。

    修改目标,而不是 打印

    如果你看到 print 你会注意到 file 参数为 sys.stdout 默认情况下。请注意,这是一个动态默认参数(它 真正地 查找 系统。标准装置 每次你打电话 打印 )与Python中的常规默认参数不同。所以如果你改变 系统。标准装置 打印 将更方便地打印到不同的目标,Python还提供了 redirect_stdout 函数(从Python 3.4开始,但很容易为早期的Python版本创建等效函数)。

    缺点是它对 打印 不打印到的语句 系统。标准装置 创造你自己的 stdout 并不是很简单。

    import io
    import sys
    
    class CustomStdout(object):
        def __init__(self, *args, **kwargs):
            self.current_stdout = sys.stdout
    
        def write(self, string):
            self.current_stdout.write(string.replace('cat', 'dog'))
    

    但是,这也适用于:

    >>> import contextlib
    >>> with contextlib.redirect_stdout(CustomStdout()):
    ...     test_file.print_something()
    ... 
    This dog was scared.
    >>> test_file.print_something()
    This cat was scared.
    

    总结

    @abarnet已经提到了其中一些要点,但我想更详细地探讨这些选项。尤其是如何跨模块修改它(使用 内置的 / __内置的__ )以及如何使更改只是暂时的(使用ContextManager)。

        3
  •  5
  •   Uri Goren    7 年前

    print 函数的作用是将输出流更改为其他内容,例如文件。

    我会用 PHP 命名约定( ob_start ,则, ob_get_contents ,…)

    from functools import partial
    output_buffer = None
    print_orig = print
    def ob_start(fname="print.txt"):
        global print
        global output_buffer
        print = partial(print_orig, file=output_buffer)
        output_buffer = open(fname, 'w')
    def ob_end():
        global output_buffer
        close(output_buffer)
        print = print_orig
    def ob_get_contents(fname="print.txt"):
        return open(fname, 'r').read()
    

    用法:

    print ("Hi John")
    ob_start()
    print ("Hi John")
    ob_end()
    print (ob_get_contents().replace("Hi", "Bye"))
    

    将打印

    嗨,约翰 再见,约翰

        4
  •  5
  •   Rafaël Dera    7 年前

    让我们将此与框架内省结合起来!

    import sys
    
    _print = print
    
    def print(*args, **kw):
        frame = sys._getframe(1)
        _print(frame.f_code.co_name)
        _print(*args, **kw)
    
    def greetly(name, greeting = "Hi")
        print(f"{greeting}, {name}!")
    
    class Greeter:
        def __init__(self, greeting = "Hi"):
            self.greeting = greeting
        def greet(self, name):
            print(f"{self.greeting}, {name}!")
    

    您会发现,这个技巧用调用函数或方法作为每个问候语的开头。这对于日志记录或调试可能非常有用;尤其是它允许您“劫持”第三方代码中的打印语句。