大师兄的Python源码学习笔记(十八): 虚拟机中的控制流(五)

时间:2021-6-9 作者:qvyue

大师兄的Python源码学习笔记(十七): 虚拟机中的控制流(四)
大师兄的Python源码学习笔记(十九): 虚拟机中的函数机制(一)

四、虚拟机中的异常控制流

2 异常控制语义结构

  • 首先创建一段异常捕获代码:
demo.py

try:
    raise Exception("an exception")
except Exception as e:
    ...
finally:
    ...
  • 对应的指令字节码如下:
  1           0 SETUP_FINALLY           52 (to 54)
              2 SETUP_EXCEPT            12 (to 16)

  2           4 LOAD_NAME                0 (Exception)
              6 LOAD_CONST               0 ('an exception')
              8 CALL_FUNCTION            1
             10 RAISE_VARARGS            1
             12 POP_BLOCK
             14 JUMP_FORWARD            34 (to 50)

  3     >>   16 DUP_TOP
             18 LOAD_NAME                0 (Exception)
             20 COMPARE_OP              10 (exception match)
             22 POP_JUMP_IF_FALSE       48
             24 POP_TOP
             26 STORE_NAME               1 (e)
             28 POP_TOP
             30 SETUP_FINALLY            4 (to 36)

  4          32 POP_BLOCK
             34 LOAD_CONST               1 (None)
        >>   36 LOAD_CONST               1 (None)
             38 STORE_NAME               1 (e)
             40 DELETE_NAME              1 (e)
             42 END_FINALLY
             44 POP_EXCEPT
             46 JUMP_FORWARD             2 (to 50)
        >>   48 END_FINALLY
        >>   50 POP_BLOCK
             52 LOAD_CONST               1 (None)

  6     >>   54 END_FINALLY
             56 LOAD_CONST               1 (None)
             58 RETURN_VALUE
  • 开始的两个字节码SETUP_FINALLYSETUP_EXCEPT都是调用了PyFrame_BlockSetup
ceval.c

        TARGET(SETUP_LOOP)
        TARGET(SETUP_EXCEPT)
        TARGET(SETUP_FINALLY) {
            /* NOTE: If you add any new block-setup opcodes that
               are not try/except/finally handlers, you may need
               to update the PyGen_NeedsFinalizing() function.
               */

            PyFrame_BlockSetup(f, opcode, INSTR_OFFSET() + oparg,
                               STACK_LEVEL());
            DISPATCH();
        }
Objectsframeobject.c

void
PyFrame_BlockSetup(PyFrameObject *f, int type, int handler, int level)
{
    PyTryBlock *b;
    if (f->f_iblock >= CO_MAXBLOCKS)
        Py_FatalError("XXX block stack overflow");
    b = &f->f_blockstack[f->f_iblock++];
    b->b_type = type;
    b->b_level = level;
    b->b_handler = handler;
}
  • 从上面的代码可以看出PyFrame_BlockSetup对当前PyFrameObject进行了配置:
Objectsframeobject.c

typedef struct _frame {
   ... ...
   PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
   ... ...
} PyFrameObject;
Objectsframeobject.c

typedef struct {
   int b_type;                 /* what kind of block this is */
   int b_handler;              /* where to jump to find handler */
   int b_level;                /* value stack level to pop to */
} PyTryBlock;
  • 接着,通过LOAD_NAMELOAD_CONSTCALL_FUNCTION构造了一个异常对象压入栈中,并通过RAISE_VARARGS将异常对象从运行时栈中取出:
ceval.c

TARGET(RAISE_VARARGS) {
            PyObject *cause = NULL, *exc = NULL;
            switch (oparg) {
            case 2:
                cause = POP(); /* cause */
                /* fall through */
            case 1:
                exc = POP(); /* exc */
                /* fall through */
            case 0:
                if (do_raise(exc, cause)) {
                    why = WHY_EXCEPTION;
                    goto fast_block_end;
                }
                break;
            default:
                PyErr_SetString(PyExc_SystemError,
                           "bad RAISE_VARARGS oparg");
                break;
            }
            goto error;
        }
  • 这里oparg参数为1,所以将异常对象取出赋值给w,接着运行do_raise函数:
ceval.c

static int
do_raise(PyObject *exc, PyObject *cause)
{
   PyObject *type = NULL, *value = NULL;

   if (exc == NULL) {
       /* Reraise */
       PyThreadState *tstate = PyThreadState_GET();
       _PyErr_StackItem *exc_info = _PyErr_GetTopmostException(tstate);
       PyObject *tb;
       type = exc_info->exc_type;
       value = exc_info->exc_value;
       tb = exc_info->exc_traceback;
       if (type == Py_None || type == NULL) {
           PyErr_SetString(PyExc_RuntimeError,
                           "No active exception to reraise");
           return 0;
       }
       Py_XINCREF(type);
       Py_XINCREF(value);
       Py_XINCREF(tb);
       PyErr_Restore(type, value, tb);
       return 1;
   }

   /* We support the following forms of raise:
      raise
      raise 
      raise  */

   if (PyExceptionClass_Check(exc)) {
       type = exc;
       value = _PyObject_CallNoArg(exc);
       if (value == NULL)
           goto raise_error;
       if (!PyExceptionInstance_Check(value)) {
           PyErr_Format(PyExc_TypeError,
                        "calling %R should have returned an instance of "
                        "BaseException, not %R",
                        type, Py_TYPE(value));
           goto raise_error;
       }
   }
   else if (PyExceptionInstance_Check(exc)) {
       value = exc;
       type = PyExceptionInstance_Class(exc);
       Py_INCREF(type);
   }
   else {
       /* Not something you can raise.  You get an exception
          anyway, just not what you specified :-) */
       Py_DECREF(exc);
       PyErr_SetString(PyExc_TypeError,
                       "exceptions must derive from BaseException");
       goto raise_error;
   }

   assert(type != NULL);
   assert(value != NULL);

   if (cause) {
       PyObject *fixed_cause;
       if (PyExceptionClass_Check(cause)) {
           fixed_cause = _PyObject_CallNoArg(cause);
           if (fixed_cause == NULL)
               goto raise_error;
           Py_DECREF(cause);
       }
       else if (PyExceptionInstance_Check(cause)) {
           fixed_cause = cause;
       }
       else if (cause == Py_None) {
           Py_DECREF(cause);
           fixed_cause = NULL;
       }
       else {
           PyErr_SetString(PyExc_TypeError,
                           "exception causes must derive from "
                           "BaseException");
           goto raise_error;
       }
       PyException_SetCause(value, fixed_cause);
   }

   PyErr_SetObject(type, value);
   /* PyErr_SetObject incref's its arguments */
   Py_DECREF(value);
   Py_DECREF(type);
   return 0;

raise_error:
   Py_XDECREF(value);
   Py_XDECREF(type);
   Py_XDECREF(cause);
   return 0;
}
  • do_raise将异常对象存储到当前线程状态中,并在结束后跳到fast_block_end
ceval.c

fast_block_end:
       assert(why != WHY_NOT);

       /* Unwind stacks if a (pseudo) exception occurred */
       while (why != WHY_NOT && f->f_iblock > 0) {
           /* Peek at the current block. */
           PyTryBlock *b = &f->f_blockstack[f->f_iblock - 1];

           assert(why != WHY_YIELD);
           if (b->b_type == SETUP_LOOP && why == WHY_CONTINUE) {
               why = WHY_NOT;
               JUMPTO(PyLong_AS_LONG(retval));
               Py_DECREF(retval);
               break;
           }
           /* Now we have to pop the block. */
           f->f_iblock--;

           if (b->b_type == EXCEPT_HANDLER) {
               UNWIND_EXCEPT_HANDLER(b);
               continue;
           }
           UNWIND_BLOCK(b);
           if (b->b_type == SETUP_LOOP && why == WHY_BREAK) {
               why = WHY_NOT;
               JUMPTO(b->b_handler);
               break;
           }
           if (why == WHY_EXCEPTION && (b->b_type == SETUP_EXCEPT
               || b->b_type == SETUP_FINALLY)) {
               PyObject *exc, *val, *tb;
               int handler = b->b_handler;
               _PyErr_StackItem *exc_info = tstate->exc_info;
               /* Beware, this invalidates all b->b_* fields */
               PyFrame_BlockSetup(f, EXCEPT_HANDLER, -1, STACK_LEVEL());
               PUSH(exc_info->exc_traceback);
               PUSH(exc_info->exc_value);
               if (exc_info->exc_type != NULL) {
                   PUSH(exc_info->exc_type);
               }
               else {
                   Py_INCREF(Py_None);
                   PUSH(Py_None);
               }
               PyErr_Fetch(&exc, &val, &tb);
               /* Make the raw exception data
                  available to the handler,
                  so a program can emulate the
                  Python main loop. */
               PyErr_NormalizeException(
                   &exc, &val, &tb);
               if (tb != NULL)
                   PyException_SetTraceback(val, tb);
               else
                   PyException_SetTraceback(val, Py_None);
               Py_INCREF(exc);
               exc_info->exc_type = exc;
               Py_INCREF(val);
               exc_info->exc_value = val;
               exc_info->exc_traceback = tb;
               if (tb == NULL)
                   tb = Py_None;
               Py_INCREF(tb);
               PUSH(tb);
               PUSH(val);
               PUSH(exc);
               why = WHY_NOT;
               JUMPTO(handler);
               break;
           }
           if (b->b_type == SETUP_FINALLY) {
               if (why & (WHY_RETURN | WHY_CONTINUE))
                   PUSH(retval);
               PUSH(PyLong_FromLong((long)why));
               why = WHY_NOT;
               JUMPTO(b->b_handler);
               break;
           }
       } /* unwind stack */

       /* End the loop if we still have an error (or return) */

       if (why != WHY_NOT)
           break;

       assert(!PyErr_Occurred());

   } /* main loop */

   assert(why != WHY_YIELD);
   /* Pop remaining stack entries. */
   while (!EMPTY()) {
       PyObject *o = POP();
       Py_XDECREF(o);
   }

   if (why != WHY_RETURN)
       retval = NULL;

   assert((retval != NULL) ^ (PyErr_Occurred() != NULL));
  • 在这里,虚拟机首先从当前PyFrameObject对象中的f_blockstack弹出一个PyTryBlock来。
  • 接着虚拟机通过PyErr_Fetch获取当前线程状态对象中存储的最新异常对象和traceback对象。
ceval.c

void
PyErr_Fetch(PyObject **p_type, PyObject **p_value, PyObject **p_traceback)
{
   PyThreadState *tstate = PyThreadState_GET();

   *p_type = tstate->curexc_type;
   *p_value = tstate->curexc_value;
   *p_traceback = tstate->curexc_traceback;

   tstate->curexc_type = NULL;
   tstate->curexc_value = NULL;
   tstate->curexc_traceback = NULL;
}
  • 随后,虚拟机将tb、val和exc压入到运行时栈中,并将why设置为WHY_NOT。
  • 最后通过JUMPTO(b->b_handler)将要执行的吓一跳指令设置为异常处理代码编译后所得到的第一条字节码指令。
  • 经过偏移后,字节码指令跳转字节码DUP_TOP的位置,它是异常处理代码队形的第一条字节码指令:
ceval.c

        TARGET(DUP_TOP) {
            PyObject *top = TOP();
            Py_INCREF(top);
            PUSH(top);
            FAST_DISPATCH();
        }
  • 在异常处理中,首先通过COMPARE_OP进行比对,用来判断是否需要使用POP_JUMP_IF_FALSE进行指令跳跃:
ceval.c

        TARGET(COMPARE_OP) {
            PyObject *right = POP();
            PyObject *left = TOP();
            PyObject *res = cmp_outcome(oparg, left, right);
            Py_DECREF(left);
            Py_DECREF(right);
            SET_TOP(res);
            if (res == NULL)
                goto error;
            PREDICT(POP_JUMP_IF_FALSE);
            PREDICT(POP_JUMP_IF_TRUE);
            DISPATCH();
        }
  • 由于在进入except之前,虚拟机已经从当前线程将异常信息tb、val和exc提取了出来。
  • COMPARE_OP匹配时,异常信息会从栈中取出来处理掉。
  • 而当COMPARE_OP不匹配时,则需要将异常信息重新放回到线程状态对象中,然后重新设置why状态,寻找真正能处理异常的代码,通过字节指令码POP TOPSETUP_FINALLY完成:
ceval.c

            if (opcode == SETUP_FINALLY ||
                opcode == SETUP_WITH ||
                opcode == BEFORE_ASYNC_WITH ||
                opcode == YIELD_FROM) {
                /* Few cases where we skip running signal handlers and other
                   pending calls:
                   - If we're about to enter the 'with:'. It will prevent
                     emitting a resource warning in the common idiom
                     'with open(path) as file:'.
                   - If we're about to enter the 'async with:'.
                   - If we're about to enter the 'try:' of a try/finally (not
                     *very* useful, but might help in some cases and it's
                     traditional)
                   - If we're resuming a chain of nested 'yield from' or
                     'await' calls, then each frame is parked with YIELD_FROM
                     as its next opcode. If the user hit control-C we want to
                     wait until we've reached the innermost frame before
                     running the signal handler and raising KeyboardInterrupt
                     (see bpo-30039).
                */
                goto fast_next_opcode;
            }
            if (_Py_atomic_load_relaxed(
                        &_PyRuntime.ceval.pending.calls_to_do))
            {
                if (Py_MakePendingCalls() async_exc != NULL) {
                PyObject *exc = tstate->async_exc;
                tstate->async_exc = NULL;
                UNSIGNAL_ASYNC_EXC();
                PyErr_SetNone(exc);
                Py_DECREF(exc);
                goto error;
            }
        }
  • 从上面的代码的结尾处,重新跳转到error设置了why。
  • 最终不管是否匹配,两条岔路在POP_BLOCK会和:
ceval.c

        PREDICTED(POP_BLOCK);
        TARGET(POP_BLOCK) {
            PyTryBlock *b = PyFrame_BlockPop(f);
            UNWIND_BLOCK(b);
            DISPATCH();
        }
  • 这里将PyTryBlock弹出,并进入finally表达式对应的字节码指令。
  • 最终Python实现异常机制的详细流程如下:

    大师兄的Python源码学习笔记(十八): 虚拟机中的控制流(五)
声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。