使用 Python 中的反汇编程序停止函数打印?

Stop a function from printing using the disassembler in Python?

我这里有这个功能,拆开后是这样的:

def game_on():    
    def other_function():
        print('Statement within a another function')
    print("Hello World")
    sys.exit()
    print("Statement after sys.exit")

8           0 LOAD_CONST               1 (<code object easter_egg at 0x0000000005609C90, file "filename", line 8>)
              3 LOAD_CONST               2 ('game_on.<locals>.other_function')
              6 MAKE_FUNCTION            0
              9 STORE_FAST               0 (other_function)

10          12 LOAD_GLOBAL              0 (print)
             15 LOAD_CONST               3 ('Hello World')
             18 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             21 POP_TOP

11          22 LOAD_GLOBAL              1 (sys)
             25 LOAD_ATTR                2 (exit)
             28 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
             31 POP_TOP

12          32 LOAD_GLOBAL              0 (print)
             35 LOAD_CONST               4 ('second print statement')
             38 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             41 POP_TOP
             42 LOAD_CONST               5 (None)
             45 RETURN_VALUE

有没有办法修改字节码使其不打印"Hello world."好像我想跳过第10行继续到第11行。

有很多 material 像检查员和 settrace 但不是很直接。有没有人对此有任何信息,或者有人可以指出我可以做什么?

修改函数字节码的最佳方法(好吧,首先假设任何东西都可以称为好方法……)是使用 third-party 库。目前,bytecode seems to be the best one, but for older versions of Python, you probably want byteplay—for 3.4 (which you seem to be using), specifically Seprex's version of the 3.x port.

但是您可以手动完成所有操作。至少值得这样做一次,只是为了确保您理解所有内容(并了解为什么 bytecode 是一个如此酷的库)。

正如您从 inspect 文档中看到的那样,函数基本上是 __code__ 对象的包装器,带有额外的东西(闭包单元、默认值和反射东西,如名称和类型注释),代码对象是一个 co_code 字节串的包装器,字节串充满了字节码和一大堆额外的东西。

所以,您认为删除一些字节码只是一个问题:

del func.__code__.co_code[12:22]

但遗憾的是,字节码在偏移方面完成了所有工作,从跳转指令到用于生成回溯的 line-number table。您可以修复所有问题,但这很痛苦。所以你可以用 NOP 代替你想杀死的指令。 (在幕后,编译器和窥孔优化器在所有地方丢弃 NOP,然后在最后做一个大的修复。但是做那个修复的代码没有暴露给 Python。)

此外,字节码存储在 immutable bytes 中,而不是 mutable bytearray,并且 code 对象本身就是 immutable (并试图通过 C API hacks 在解释器背后更改它们是一个非常糟糕的主意)。因此,您必须围绕修改后的字节码构建一个新的 code 对象。但是函数是 mutable,所以你可以破解你的函数以指向那个新的代码对象。


所以,这里有一个函数可以通过偏移量来 NOP 出一系列指令:

import dis
import sys
import types

NOP = bytes([dis.opmap['NOP']])

def noprange(func, start, end):
    c = func.__code__
    cc = c.co_code
    if sys.version_info >= (3,6):
        if (end - start) % 2:
            raise ValueError('Cannot nop out partial wordcodes')
        nops = (NOP + b'[=11=]') * ((end-start)//2)
    else:
        nops = NOP * (end-start)
    newcc = cc[:start] + nops + cc[end:]
    newc = types.CodeType(
        c.co_argcount, c.co_kwonlyargcount, c.co_nlocals, c.co_stacksize,
        c.co_flags, newcc, c.co_consts, c.co_names, c.co_varnames,
        c.co_filename, c.co_name, c.co_firstlineno, c.co_lnotab,
        c.co_freevars, c.co_cellvars)
    func.__code__ = newc

如果您想了解该版本检查:在 Python 2.x 和 3.0-3.5 中,每条指令的长度为 1 或 3 个字节,具体取决于它是否需要任何参数,因此NOP为1字节;在 3.6+ 中,每条指令长度为 2 个字节,包括 NOP。

无论如何,我实际上只在 3.6 上测试过,而不是 3.4 或 3.5,所以希望我没有弄错那部分。希望我在 3.4 之后没有添加任何添加到 dis 的功能。那么,祈祷吧:

noprange(game_on, 12, 22)

... 会完全按照您的意愿行事。或者它会修改您的函数以引发 RuntimeError 或在您尝试调用它时崩溃,但段错误是学习的一部分,对吧?无论如何,如果你 dis.dis(noprange) 你应该看到第 10 行的四个指令被一串 NOP 行替换,然后函数的其余部分保持不变,所以在你调用它之前尝试一下。


一旦你确信你已经让这个工作正常,如果你想从一个源代码行中删除所有指令而不必 dis 函数并手动阅读它们,你可以使用findlinestarts 以编程方式进行:

def nopline(func, line):
    linestarts = dis.findlinestarts(func.__code__)
    for offset, lineno in linestarts:
        if lineno > line:
            raise ValueError('No code found for line')
        if lineno == line:
            try:
                nextoffset, _ = next(linestarts)
            except StopIteration:
                raise ValueError('Do not nop out the last return')
            noprange(func, offset, nextoffset)
            return
    raise ValueError('No line found')

现在只是:

nopline(game_on, 10)

这有一个很好的优势,你可以在代码中使用它,在 3.4 和 3.8 中以相同的方式工作(或崩溃),因为 Python 版本之间的偏移量可能会发生变化,但行号的计数方式显然不会。