美文网首页Python
大师兄的Python源码学习笔记(十七): 虚拟机中的控制流(四

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

作者: superkmi | 来源:发表于2021-05-21 20:15 被阅读0次

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

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

1 虚拟机自身抛出异常

  • Python内部有一套内建的异常捕捉机制,即使没有在代码中进行异常控制,Python脚本执行中所抛出的异常还是会被Python虚拟机捕捉到:
>>>a = 10
>>>b = 0
>>>c = a/b
Traceback (most recent call last):
  File "demo.py", line 3, in <module>
    c = a/b
ZeroDivisionError: division by zero
  • 对应指令字节码:
  1           0 LOAD_CONST               0 (10)
              2 STORE_NAME               0 (a)

  2           4 LOAD_CONST               1 (0)
              6 STORE_NAME               1 (b)

  3           8 LOAD_NAME                1 (a)
             10 LOAD_NAME                0 (b)
             12 BINARY_TRUE_DIVIDE
             14 STORE_NAME               2 (c)
             16 LOAD_CONST               2 (None)
             18 RETURN_VALUE
  • 这段字节码的核心部分是在BINARY_TRUE_DIVIDE,也就是除法计算:
ceval.c

        TARGET(BINARY_TRUE_DIVIDE) {
            PyObject *divisor = POP();
            PyObject *dividend = TOP();
            PyObject *quotient = PyNumber_TrueDivide(dividend, divisor);
            Py_DECREF(dividend);
            Py_DECREF(divisor);
            SET_TOP(quotient);
            if (quotient == NULL)
                goto error;
            DISPATCH();
        }
  • 在上面的代码中,PyNumber_TrueDivide(dividend, divisor)将栈中的两个值进行除法操作,并将结果赋值quotient ,如果quotientNULL,则抛出异常。
  • 深入研究PyNumber_TrueDivide,经过一系列的操作,除法路径最终会达到long_divrem,并抛出异常:
Objects\longobject.c

static int
long_divrem(PyLongObject *a, PyLongObject *b,
            PyLongObject **pdiv, PyLongObject **prem)
{
    Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b));
    PyLongObject *z;

    if (size_b == 0) {
        PyErr_SetString(PyExc_ZeroDivisionError,
                        "integer division or modulo by zero");
        return -1;
    }
... ...
}
  • 在上面的代码中,PyErr_SetString在抛出异常时,包含了一个异常对象PyExc_ZeroDivisionError
  • PyExc_ZeroDivisionError是一个PyObject对象异常指针,用来指明异常种类:
Include\pyerrors.h

...
PyAPI_DATA(PyObject *) PyExc_ReferenceError;
PyAPI_DATA(PyObject *) PyExc_SystemError;
PyAPI_DATA(PyObject *) PyExc_SystemExit;
PyAPI_DATA(PyObject *) PyExc_TypeError;
PyAPI_DATA(PyObject *) PyExc_UnboundLocalError;
PyAPI_DATA(PyObject *) PyExc_UnicodeError;
PyAPI_DATA(PyObject *) PyExc_UnicodeEncodeError;
PyAPI_DATA(PyObject *) PyExc_UnicodeDecodeError;
PyAPI_DATA(PyObject *) PyExc_UnicodeTranslateError;
PyAPI_DATA(PyObject *) PyExc_ValueError;
PyAPI_DATA(PyObject *) PyExc_ZeroDivisionError;
...
1.1在线程状态对象中记录异常信息
  • 在抛出异常后,虚拟机会沿着PyErr_SetString一路向前,直到PyErr_Restore:
Python\errors.c

void
PyErr_SetString(PyObject *exception, const char *string)
{
    PyObject *value = PyUnicode_FromString(string);
    PyErr_SetObject(exception, value);
    Py_XDECREF(value);
}
Python\errors.c

void
PyErr_SetObject(PyObject *exception, PyObject *value)
{
    PyThreadState *tstate = PyThreadState_GET();
    PyObject *exc_value;
    PyObject *tb = NULL;

    if (exception != NULL &&
        !PyExceptionClass_Check(exception)) {
        PyErr_Format(PyExc_SystemError,
                     "exception %R not a BaseException subclass",
                     exception);
        return;
    }

    Py_XINCREF(value);
    exc_value = _PyErr_GetTopmostException(tstate)->exc_value;
    if (exc_value != NULL && exc_value != Py_None) {
        /* Implicit exception chaining */
        Py_INCREF(exc_value);
        if (value == NULL || !PyExceptionInstance_Check(value)) {
            /* We must normalize the value right now */
            PyObject *fixed_value;

            /* Issue #23571: functions must not be called with an
               exception set */
            PyErr_Clear();

            fixed_value = _PyErr_CreateException(exception, value);
            Py_XDECREF(value);
            if (fixed_value == NULL) {
                Py_DECREF(exc_value);
                return;
            }

            value = fixed_value;
        }

        /* Avoid reference cycles through the context chain.
           This is O(chain length) but context chains are
           usually very short. Sensitive readers may try
           to inline the call to PyException_GetContext. */
        if (exc_value != value) {
            PyObject *o = exc_value, *context;
            while ((context = PyException_GetContext(o))) {
                Py_DECREF(context);
                if (context == value) {
                    PyException_SetContext(o, NULL);
                    break;
                }
                o = context;
            }
            PyException_SetContext(value, exc_value);
        }
        else {
            Py_DECREF(exc_value);
        }
    }
    if (value != NULL && PyExceptionInstance_Check(value))
        tb = PyException_GetTraceback(value);
    Py_XINCREF(exception);
    PyErr_Restore(exception, value, tb);
}
Python\errors.c

void
PyErr_Restore(PyObject *type, PyObject *value, PyObject *traceback)
{
    PyThreadState *tstate = PyThreadState_GET();
    PyObject *oldtype, *oldvalue, *oldtraceback;

    if (traceback != NULL && !PyTraceBack_Check(traceback)) {
        /* XXX Should never happen -- fatal error instead? */
        /* Well, it could be None. */
        Py_DECREF(traceback);
        traceback = NULL;
    }

    /* Save these in locals to safeguard against recursive
       invocation through Py_XDECREF */
    oldtype = tstate->curexc_type;
    oldvalue = tstate->curexc_value;
    oldtraceback = tstate->curexc_traceback;

    tstate->curexc_type = type;
    tstate->curexc_value = value;
    tstate->curexc_traceback = traceback;

    Py_XDECREF(oldtype);
    Py_XDECREF(oldvalue);
    Py_XDECREF(oldtraceback);
}
  • 在这里,虚拟机获取了当前的线程tstate,并将异常信息储存在当前线程,分别放在tstate->curexc_typetstate->curexc_valuetstate->curexc_traceback中。
  • 在Python中,我们可以通过sys库的接口获取当前线程的状态:
>>>import sys

>>>try:
>>>    a = 10
>>>    b = 0
>>>    c = a/b
>>>except Exception:
>>>    print(sys.exc_info()[0])
>>>    print(sys.exc_info()[1])
<class 'ZeroDivisionError'>
division by zero
1.2 展开栈帧
  • 跳出巨大的判断循环后,如果发生异常,将通过goto_error将why设置为WHY_EXCEPTION:
ceval.c

error:

        assert(why == WHY_NOT);
        why = WHY_EXCEPTION;

        /* Double-check exception status. */
#ifdef NDEBUG
        if (!PyErr_Occurred())
            PyErr_SetString(PyExc_SystemError,
                            "error return without exception set");
#else
        assert(PyErr_Occurred());
  • 如果why为WHY_EXCEPTION,则表示执行字节码的过程中,有异常被抛出了。
ceval.c

        /* Log traceback info. */
        PyTraceBack_Here(f);

        if (tstate->c_tracefunc != NULL)
            call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj,
                           tstate, f);
  • 深入观察PyTraceBack_Here
Python\traceback.c

int
PyTraceBack_Here(PyFrameObject *frame)
{
    PyObject *exc, *val, *tb, *newtb;
    PyErr_Fetch(&exc, &val, &tb);
    newtb = tb_create_raw((PyTracebackObject *)tb, frame, frame->f_lasti,
                          PyFrame_GetLineNumber(frame));
    if (newtb == NULL) {
        _PyErr_ChainExceptions(exc, val, tb);
        return -1;
    }
    PyErr_Restore(exc, val, newtb);
    Py_XDECREF(tb);
    return 0;
}
Python\errors.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;
}
  • 可以看到PyTraceBack_Here从当前线程中取出异常值,并创建了PyTracebackObject对象。
  • PyTracebackObject对象是链表结构:
Include\traceback.h

typedef struct _traceback {
   PyObject_HEAD
   struct _traceback *tb_next;
   struct _frame *tb_frame;
   int tb_lasti;
   int tb_lineno;
} PyTracebackObject;
  • 在创建对象后,虚拟机通过tb_create_rawPyTracebackObject接入链表,并储存了当前最后执行的字节码指令以及其在源码中的行号。
Python\traceback.c

static PyObject *
tb_create_raw(PyTracebackObject *next, PyFrameObject *frame, int lasti,
              int lineno)
{
    PyTracebackObject *tb;
    if ((next != NULL && !PyTraceBack_Check(next)) ||
                    frame == NULL || !PyFrame_Check(frame)) {
        PyErr_BadInternalCall();
        return NULL;
    }
    tb = PyObject_GC_New(PyTracebackObject, &PyTraceBack_Type);
    if (tb != NULL) {
        Py_XINCREF(next);
        tb->tb_next = next;
        Py_XINCREF(frame);
        tb->tb_frame = frame;
        tb->tb_lasti = lasti;
        tb->tb_lineno = lineno;
        PyObject_GC_Track(tb);
    }
    return (PyObject *)tb;
}
  • 当虚拟机意识到有异常抛出,并创建了traceback对象后,会在当前栈帧中寻找except语句,如果没有找到,则退出当前活动栈帧,并沿着栈帧链表向上回退到上一个栈帧。
ceval.c

exit_eval_frame:
    if (PyDTrace_FUNCTION_RETURN_ENABLED())
        dtrace_function_return(f);
    Py_LeaveRecursiveCall();
    f->f_executing = 0;
    tstate->frame = f->f_back;

    return _Py_CheckFunctionResult(NULL, retval, "PyEval_EvalFrameEx");
  • 如果异常没有被任何捕获动作捕获到,栈帧链表将一路回退,这个回退的过程被称为栈帧展开
  • 最终,虚拟机的执行流程将一直返回到PyRun_SimpleFileExFlags中:
Python\pythonrun.c

int
PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit,
                        PyCompilerFlags *flags)
{
    ...
    if (v == NULL) {
        Py_CLEAR(m);
        PyErr_Print();
        goto done;
    }
...
}
  • PyRun_SimpleFileExFlags最终通过PyErr_Print()从线程状态信息中取出traceback对象,遍历traceback对象链表,并逐个输出其中的信息。
Python\pythonrun.c

void
PyErr_PrintEx(int set_sys_last_vars)
{
    PyObject *exception, *v, *tb, *hook;

    if (PyErr_ExceptionMatches(PyExc_SystemExit)) {
        handle_system_exit();
    }
    PyErr_Fetch(&exception, &v, &tb);
    if (exception == NULL)
        return;
    PyErr_NormalizeException(&exception, &v, &tb);
    if (tb == NULL) {
        tb = Py_None;
        Py_INCREF(tb);
    }
    PyException_SetTraceback(v, tb);
    if (exception == NULL)
        return;
    /* Now we know v != NULL too */
    if (set_sys_last_vars) {
        if (_PySys_SetObjectId(&PyId_last_type, exception) < 0) {
            PyErr_Clear();
        }
        if (_PySys_SetObjectId(&PyId_last_value, v) < 0) {
            PyErr_Clear();
        }
        if (_PySys_SetObjectId(&PyId_last_traceback, tb) < 0) {
            PyErr_Clear();
        }
    }
    hook = _PySys_GetObjectId(&PyId_excepthook);
    if (hook) {
        PyObject* stack[3];
        PyObject *result;

        stack[0] = exception;
        stack[1] = v;
        stack[2] = tb;
        result = _PyObject_FastCall(hook, stack, 3);
        if (result == NULL) {
            PyObject *exception2, *v2, *tb2;
            if (PyErr_ExceptionMatches(PyExc_SystemExit)) {
                handle_system_exit();
            }
            PyErr_Fetch(&exception2, &v2, &tb2);
            PyErr_NormalizeException(&exception2, &v2, &tb2);
            /* It should not be possible for exception2 or v2
               to be NULL. However PyErr_Display() can't
               tolerate NULLs, so just be safe. */
            if (exception2 == NULL) {
                exception2 = Py_None;
                Py_INCREF(exception2);
            }
            if (v2 == NULL) {
                v2 = Py_None;
                Py_INCREF(v2);
            }
            fflush(stdout);
            PySys_WriteStderr("Error in sys.excepthook:\n");
            PyErr_Display(exception2, v2, tb2);
            PySys_WriteStderr("\nOriginal exception was:\n");
            PyErr_Display(exception, v, tb);
            Py_DECREF(exception2);
            Py_DECREF(v2);
            Py_XDECREF(tb2);
        }
        Py_XDECREF(result);
    } else {
        PySys_WriteStderr("sys.excepthook is missing\n");
        PyErr_Display(exception, v, tb);
    }
    Py_XDECREF(exception);
    Py_XDECREF(v);
    Py_XDECREF(tb);
}

相关文章

网友评论

    本文标题:大师兄的Python源码学习笔记(十七): 虚拟机中的控制流(四

    本文链接:https://www.haomeiwen.com/subject/evgijltx.html