旧版Python中END_FINALLY字节码的作用及跳转疑问
请考虑以下代码:
try: helloworld() except: failure()
及其在Python 2.7中的反汇编结果:
1 0 SETUP_EXCEPT 11 (to 14) 2 3 LOAD_NAME 0 (helloworld) 6 CALL_FUNCTION 0 9 POP_TOP 10 POP_BLOCK 11 JUMP_FORWARD 14 (to 28) 3 >> 14 POP_TOP 15 POP_TOP 16 POP_TOP 4 17 LOAD_NAME 1 (failure) 20 CALL_FUNCTION 0 23 POP_TOP 24 JUMP_FORWARD 1 (to 28) 27 END_FINALLY >> 28 LOAD_CONST 0 (None) 31 RETURN_VALUE
假设helloworld()抛出异常,代码将从地址14开始执行。由于该except处理器为通用型,后续的三条POP_TOP指令以及failure()函数调用符合逻辑,但之后存在一条24 JUMP_FORWARD指令跳过了27 END_FINALLY,使其不会被执行。请问该END_FINALLY字节码的作用是什么?
我注意到Python 3.5、3.6、3.7和3.8版本中存在类似行为,而Python 3.9中该字节码被重命名为RERAISE。
背景信息:在简化一个混淆的pyc文件并经过大量调试后,我发现此类结构会破坏uncompyle6工具。
回答:
END_FINALLY字节码的核心作用是处理异常未被当前except块完全捕获的场景,具体细节如下:
处理异常重抛场景:如果
failure()函数本身抛出异常,或者你在except块内显式用raise重新抛出原异常,程序不会执行JUMP_FORWARD跳转,而是会走到END_FINALLY。这个字节码会负责清理异常栈,并将异常传递到上层调用栈中。复用统一的异常处理框架:Python的try/except和try/finally共享底层字节码结构,
SETUP_EXCEPT指令同时支持两种场景。END_FINALLY原本是为finally块设计的收尾逻辑,在这里被保留是为了统一异常处理流程——当except块无法消化异常时,它就承担起重新抛出异常的职责。
Python 3.9将其重命名为RERAISE,其实是更精准地反映了它在这种except场景下的实际作用:当异常未被当前块处理时,负责重新抛出异常。
你遇到的uncompyle6失效问题,大概率是混淆pyc文件篡改了正常的跳转逻辑,导致反编译器无法识别END_FINALLY对应的分支场景,从而无法正确还原源代码结构。
内容的提问来源于stack exchange,提问作者musava_ribica

