草庐IT

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

coder 2023-04-29 原文

注意:此问题仅供引用。我很想知道如何深入了解 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/

有关python - 是否有可能 "hack"Python 的打印功能?的更多相关文章

  1. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  2. ruby-on-rails - rails : "missing partial" when calling 'render' in RSpec test - 2

    我正在尝试测试是否存在表单。我是Rails新手。我的new.html.erb_spec.rb文件的内容是:require'spec_helper'describe"messages/new.html.erb"doit"shouldrendertheform"dorender'/messages/new.html.erb'reponse.shouldhave_form_putting_to(@message)with_submit_buttonendendView本身,new.html.erb,有代码:当我运行rspec时,它失败了:1)messages/new.html.erbshou

  3. ruby-on-rails - 由于 "wkhtmltopdf",PDFKIT 显然无法正常工作 - 2

    我在从html页面生成PDF时遇到问题。我正在使用PDFkit。在安装它的过程中,我注意到我需要wkhtmltopdf。所以我也安装了它。我做了PDFkit的文档所说的一切......现在我在尝试加载PDF时遇到了这个错误。这里是错误:commandfailed:"/usr/local/bin/wkhtmltopdf""--margin-right""0.75in""--page-size""Letter""--margin-top""0.75in""--margin-bottom""0.75in""--encoding""UTF-8""--margin-left""0.75in""-

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby - 如何以所有可能的方式将字符串拆分为长度最多为 3 的连续子字符串? - 2

    我试图获取一个长度在1到10之间的字符串,并输出将字符串分解为大小为1、2或3的连续子字符串的所有可能方式。例如:输入:123456将整数分割成单个字符,然后继续查找组合。该代码将返回以下所有数组。[1,2,3,4,5,6][12,3,4,5,6][1,23,4,5,6][1,2,34,5,6][1,2,3,45,6][1,2,3,4,56][12,34,5,6][12,3,45,6][12,3,4,56][1,23,45,6][1,2,34,56][1,23,4,56][12,34,56][123,4,5,6][1,234,5,6][1,2,345,6][1,2,3,456][123

  6. ruby - 检查 "command"的输出应该包含 NilClass 的意外崩溃 - 2

    为了将Cucumber用于命令行脚本,我按照提供的说明安装了arubagem。它在我的Gemfile中,我可以验证是否安装了正确的版本并且我已经包含了require'aruba/cucumber'在'features/env.rb'中为了确保它能正常工作,我写了以下场景:@announceScenario:Testingcucumber/arubaGivenablankslateThentheoutputfrom"ls-la"shouldcontain"drw"假设事情应该失败。它确实失败了,但失败的原因是错误的:@announceScenario:Testingcucumber/ar

  7. ruby - 检查数组是否在增加 - 2

    这个问题在这里已经有了答案:Checktoseeifanarrayisalreadysorted?(8个答案)关闭9年前。我只是想知道是否有办法检查数组是否在增加?这是我的解决方案,但我正在寻找更漂亮的方法:n=-1@arr.flatten.each{|e|returnfalseife

  8. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

    我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

  9. ruby-on-rails - 迷你测试错误 : "NameError: uninitialized constant" - 2

    我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test

  10. ruby - 检查字符串是否包含散列中的任何键并返回它包含的键的值 - 2

    我有一个包含多个键的散列和一个字符串,该字符串不包含散列中的任何键或包含一个键。h={"k1"=>"v1","k2"=>"v2","k3"=>"v3"}s="thisisanexamplestringthatmightoccurwithakeysomewhereinthestringk1(withspecialcharacterslike(^&*$#@!^&&*))"检查s是否包含h中的任何键的最佳方法是什么,如果包含,则返回它包含的键的值?例如,对于上面的h和s的例子,输出应该是v1。编辑:只有字符串是用户定义的。哈希将始终相同。 最佳答案

随机推荐