运行时修改函数(拉出局部变量)

Modify function at runtime (pulling local variable out)

想象一下这个创建变量 defaultmodified:

的修改值的简单函数
default = 0
def modify():
    modified = default + 1
    print(modified)  # replace with OS call, I can't see the output

modify()  # 1
default  # 0

反汇编:

import dis
dis.dis(modify)
2           0 LOAD_GLOBAL              0 (default)
            3 LOAD_CONST               1 (1)
            6 BINARY_ADD
            7 STORE_FAST               0 (modified)
3          10 LOAD_GLOBAL              1 (print)
           13 LOAD_FAST                0 (modified)
           16 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
           19 POP_TOP
           20 LOAD_CONST               0 (None)
           23 RETURN_VALUE

我无法更改函数 modify(),但我知道其中的内容,直接(我可以看到代码)或间接(反汇编)。我需要的是获取 modified 变量的值,所以我可能有一种方法可以通过 dis 模块删除函数的特定部分 (print(modified)),但是我什么也没找到。

有什么方法可以删除 16 CALL_FUNCTION 之后除 return_value 之外的所有内容,并将其替换为例如return modified?或者有没有其他方法可以在不实际执行最后一行的情况下提取局部变量?

作为一种可能的解决方案,我看到了 3 种方法:

我想避免第二种方式,这可能比第一种方式更容易,但我必须避免第三种方式,所以...有什么办法可以解决我的问题吗?

还有第 4 个选项:替换 print() 全局:

printed = []
print = lambda *args: printed.extend(args)
modify()
del print
modified = printed[0]

否则可能会生成修改后的字节码,但这很容易导致导致解释器崩溃的错误(对无效字节码的保护为零),因此请注意。

您可以使用更新后的字节码的新代码对象创建新函数对象;根据您显示的 dis 中的偏移量,我手动创建了新的字节码,它将 return 索引 0:

处的局部变量
>>> altered_bytecode = modify.__code__.co_code[:8] + bytes(
...     [dis.opmap['LOAD_FAST'], 0,   # load local variable 0 onto the stack
...      dis.opmap['RETURN_VALUE']])) # and return it.
>>> dis.dis(altered_bytecode)
          0 LOAD_GLOBAL              0 (0)
          2 LOAD_CONST               1 (1)
          4 BINARY_ADD
          6 STORE_FAST               0 (0)
          8 LOAD_FAST                0 (0)
         10 RETURN_VALUE

RETURN_VALUE returns 栈顶对象;我所做的只是注入一个 LOAD_FAST 操作码来将 modified 引用加载到堆栈上。

您必须创建一个新的 code 对象,然后是一个新的 function 对象来包装代码对象,才能使其可调用:

>>> code = type(modify.__code__)
>>> function = type(modify)
>>> ocode = modify.__code__
>>> new_modify = function(
...     code(ocode.co_argcount, ocode.co_kwonlyargcount, ocode.co_nlocals, ocode.co_stacksize,
...          ocode.co_flags, altered_bytecode,
...          ocode.co_consts, ocode.co_names, ocode.co_varnames, ocode.co_filename,
...          'new_modify', ocode.co_firstlineno, ocode.co_lnotab, ocode.co_freevars,
...          ocode.co_cellvars),
...     modify.__globals__, 'new_modify', modify.__defaults__, modify.__closure__)
>>> new_modify()
1

显然,这确实需要首先了解 Python 字节码的工作原理; dis 模块确实包含各种代码的描述,dis.opmap dictionary 允许您映射回字节值。

有一些模块试图使这更容易;如果您想进一步探索,请查看 byteplay, the bytecode module of the pwnypack project 或其他几个。

我也可以衷心推荐您观看 Playing with Python Bytecode presentation given by Scott Sanderson, Joe Jevnik at PyCon 2016, and play with their codetransformer module。极具娱乐性和信息量。