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

大师兄的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();
        }
Objects\frameobject.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进行了配置:
Objects\frameobject.c

typedef struct _frame {
   ... ...
   PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
   ... ...
} PyFrameObject;
Objects\frameobject.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 <instance>
      raise <type> */

   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() < 0)
                    goto error;
            }
            if (_Py_atomic_load_relaxed(
                        &_PyRuntime.ceval.gil_drop_request))
            {
                /* Give another thread a chance */
                if (PyThreadState_Swap(NULL) != tstate)
                    Py_FatalError("ceval: tstate mix-up");
                drop_gil(tstate);

                /* Other threads may run now */

                take_gil(tstate);

                /* Check if we should make a quick exit. */
                if (_Py_IsFinalizing() &&
                    !_Py_CURRENTLY_FINALIZING(tstate))
                {
                    drop_gil(tstate);
                    PyThread_exit_thread();
                }

                if (PyThreadState_Swap(tstate) != NULL)
                    Py_FatalError("ceval: orphan tstate");
            }
            /* Check for asynchronous exceptions. */
            if (tstate->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实现异常机制的详细流程如下:


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容