我在这里有这个功能,它在反汇编时看起来像这样:
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
,但它不是很直接。有没有人对此有任何信息或有人指出我能做什么?
答案 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>