1. ホーム
  2. python

[解決済み] Pythonのprint関数を "ハック "することは可能か?

2022-06-29 10:50:30

質問

注:この質問は情報提供のみを目的としています。Pythonの内部をどれだけ深く掘り下げることができるかに興味があります。

それほど昔ではないのですが、ある 質問 の呼び出しの後/最中に、print文に渡される文字列を変更できるかどうかについて、ある質問の中で議論が始まりました。 print の呼び出しが行われた後/中に、print文に渡された文字列を変更できるかどうかという質問

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

さて、ここで print を実行すると、ターミナルへの出力が表示されるはずです。

This dog was scared.

cat" という単語が "dog" という単語で置き換えられていることに注目してください。どこかの誰かが、印刷されたものを変更するために、これらの内部バッファを修正することができたのです。これは、元のコードの著者の明示的な許可なしに行われたと仮定してください (したがって、ハッキング/ハイジャック)。

これは コメント は、特に賢明な@abarnertから、私に考えさせました。

これを行う方法はいくつかありますが、どれも非常に醜いもので、決して行うべきではありません。 決してやってはいけないことです。最も醜くない方法は、おそらく code オブジェクトを別の co_consts のリストが表示されます。次に、おそらく C API に到達して str の内部バッファにアクセスします。 内部バッファにアクセスすることです。[...]

ということで、実際に可能なようです。

この問題にアプローチする私の素朴な方法を紹介します。

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

もちろん exec は悪いものですが、それは実際には何も修正しないので、質問には答えられません。 の間、または print が呼び出されます。

@abarnertさんの説明の通りだと、どうなるのでしょうか?

どのように解決するのですか?

まず、実際にはもっと簡単な方法があります。私たちがしたいのは 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)

また、同様にモンキーパッチで 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 がスタック全体を食いつぶしてしまうこと、もっと普通の RuntimeError を処理することができるもの、 あるいは TypeError または AttributeError を使おうとすると 例えば、コードオブジェクトを作るときに RETURN_VALUE だけで、スタックには何もない状態(バイトコードの b'S\0' 3.6+ では b'S' 以前は)、あるいは、空のタプルで co_consts がある場合は LOAD_CONST 0 がある場合は、バイトコードに varnames が 1 つ減算されるため、最も高い LOAD_FAST は実際にフリーバー/セルバーのセルをロードします。本当に楽しいのは、もしあなたが lnotab を十分に間違えると、デバッガで実行したときだけコードがセグメンテーションフォルトになります。

使用方法 bytecode または byteplay は、これらの問題のすべてからあなたを守ることはできませんが、いくつかの基本的なサニティチェックと、コードの塊を挿入して、あなたが間違えないようにすべてのオフセットとラベルの更新を心配させるようなことをさせる素晴らしいヘルパーを備えています。(さらに、馬鹿げた6行のコンストラクタを入力したり、そのために生じる愚かなタイプミスをデバッグする必要もありません)。


次は2番です。

コードオブジェクトはイミュータブルであると書きました。そしてもちろんconstはタプルなので、それを直接変更することはできません。そしてconstタプル内のものは文字列で、これも直接変更することはできません。だから、新しいコードオブジェクトを作るために、新しいタプルを作るために、新しい文字列を作らなければならないのです。

しかし、文字列を直接変更できるとしたらどうでしょうか?

まあ、蓋を開けてみれば、全てはあるC言語のデータへのポインタに過ぎないわけですがね。CPythonを使っているのであれば オブジェクトにアクセスするためのC言語のAPI で、そして を使用することができます。 ctypes を使って Python 自身からその API にアクセスすることができますが、これはとてもひどい考えで、Python に pythonapi を標準ライブラリの ctypes モジュールにあります。 . :) 知っておくべき最も重要なトリックは id(x) への実際のポインタであるということです。 x への実際のポインタは、メモリ上の int ).

残念ながら、文字列のためのCのAPIは、すでに凍結された文字列の内部ストレージを安全に取得することを許しません。ですから、安全にねじ込むには、単に ヘッダーファイルを読む を読んで、自分自身でそのストレージを見つけましょう。

CPython 3.4 - 3.7 を使用している場合 (古いバージョンでは異なりますし、将来のこともわかりません)、純粋な ASCII でできているモジュールからの文字列リテラルは、コンパクト ASCII フォーマットを使用して保存されます。文字列に非 ASCII 文字を入れたり、ある種の非リテラル文字列を入れたりすると、これは(おそらくセグメンテーションのように)壊れますが、異なる種類の文字列に対してバッファにアクセスする他の 4 つの方法については、こちらをお読みください。

少し簡単にするために、私は superhackyinternals プロジェクトを使っています。(意図的に pip-installable ではないので、インタプリタなどのローカルビルドの実験以外には、これを使うべきではありません)。

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 . の値を変更することで何が壊れるかを推測するのはずっと簡単です。 21 という具合になりますよね?実は、想像するのはやめて、実際にやってみましょう。 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 ループの何かに使っているのでしょう。