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

时间:2018-04-07 00:59:10

标签: python python-3.x disassembly

我在这里有这个功能,它在反汇编时看起来像这样:

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

有没有办法修改字节码,因此它不打印&#34; Hello world。&#34;这就像我想跳过第10行并继续到11。

有许多材料,如检查员和settrace,但它不是很直接。有没有人对此有任何信息或有人指出我能做什么?

1 个答案:

答案 0 :(得分:2)

修改函数字节码的最佳方法(好吧,假设任何事情都可以称之为好方法......)是第三方库。目前,bytecode似乎是最好的,但对于旧版本的Python,您可能需要byteplay - 对于3.4(您似乎正在使用),特别是Seprex's version of the 3.x port。< / p>

但你可以手动完成所有事情。值得这样做至少一次,只是为了确保你理解一切(并了解为什么bytecode是一个很酷的库)。

inspect文档中可以看出,函数基本上是__code__对象的包装器,带有额外的东西(闭包单元格,默认值和反射内容,如名称和类型注释) ,一个代码对象是一个包含完整字节码的co_code字节串的包装器,带有一大堆额外的东西。

所以,你认为砍掉一些字节码只是一个问题:

del func.__code__.co_code[12:22]

但遗憾的是,字节码在偏移方面做了所有事情,从跳转指令到用于生成回溯的行号表。你可以解决所有问题,但这很痛苦。因此,您可以使用NOP替换要杀死的说明。 (在幕后,编译器和窥孔优化器会在所有地方丢弃NOP,然后在最后执行一个大的修复。但执行该修复的代码不会暴露给Python。)

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

所以,这是一个通过偏移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'\0') * ((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函数并手动读取它们,则可以使用{{ 3}}以编程方式执行:

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版本之间发生变化,但是行数明显不计算。< / p>