美文网首页
【Python】动态加载机制

【Python】动态加载机制

作者: lndyzwdxhs | 来源:发表于2018-12-23 22:21 被阅读13次

    0x01 import前奏曲

    Python是如何将硬盘上的py文件中的内容来创建Python可以识别的运行时模块的?

    # import sys
     0 LOAD_CONST               0 (-1)
     3 LOAD_CONST               1 (None)
     6 IMPORT_NAME              0 (sys)
     9 STORE_NAME               0 (sys)
    12 LOAD_CONST               1 (None)
    15 RETURN_VALUE
    

    可以看到,import的结果最终将sys module存储在当前PyFrameObject的local名字空间中。具体来看下IMPORT_NAME指令:

    case IMPORT_NAME:
        w = GETITEM(names, oparg);
        x = PyDict_GetItemString(f->f_builtins, "__import__");
        if (x == NULL) {
            PyErr_SetString(PyExc_ImportError,
                    "__import__ not found");
            break;
        }
        Py_INCREF(x);
        v = POP();
        u = TOP();
        // 将Python的import动作需要使用的信息打包到tuple中
        if (PyInt_AsLong(u) != -1 || PyErr_Occurred())
            w = PyTuple_Pack(5,
                    w,
                    f->f_globals,
                    f->f_locals == NULL ?
                      Py_None : f->f_locals,
                    v,
                    u);
        else
            w = PyTuple_Pack(4,
                    w,
                    f->f_globals,
                    f->f_locals == NULL ?
                      Py_None : f->f_locals,
                    v);
        Py_DECREF(v);
        Py_DECREF(u);
        if (w == NULL) {
            u = POP();
            Py_DECREF(x);
            x = NULL;
            break;
        }
        READ_TIMESTAMP(intr0);
        v = x;
        x = PyEval_CallObject(v, w);
        Py_DECREF(v);
        READ_TIMESTAMP(intr1);
        Py_DECREF(w);
        SET_TOP(x);
        if (x != NULL) continue;
        break;
    

    最开始的w是PyStringObject对象"sys",v是通过3 LOAD_CONST 1指令被压入到运行时栈中的PyNone,u则是0 LOAD_CONST 0指令被压入到运行时栈的-1,x是PyCFunctionObject对象(builtin模块的"import"对应的函数)。

    将import相关的所有信息打包成PyTupleObject对象,然后传入PyEval_CallObject中。

    // ceval.c
    #define PyEval_CallObject(func,arg) \
            PyEval_CallObjectWithKeywords(func, arg, (PyObject *)NULL)
    
    PyObject *
    PyEval_CallObjectWithKeywords(PyObject *func, PyObject *arg, PyObject *kw)
    {
        PyObject *result;
    
        if (arg == NULL) {
            arg = PyTuple_New(0);
            if (arg == NULL)
                return NULL;
        }
        else if (!PyTuple_Check(arg)) {
            PyErr_SetString(PyExc_TypeError,
                    "argument list must be a tuple");
            return NULL;
        }
        else
            Py_INCREF(arg);
    
        if (kw != NULL && !PyDict_Check(kw)) {
            PyErr_SetString(PyExc_TypeError,
                    "keyword list must be a dictionary");
            Py_DECREF(arg);
            return NULL;
        }
    
        result = PyObject_Call(func, arg, kw);
        Py_DECREF(arg);
        return result;
    }
    

    这里的arg就是前面打包好的PyTupleObject对象,PyEval_CallObjectWithKeywords检查了参数的有效性,实际执行还是调用的PyObject_Call。

    之前在函数机制中使用到了PyObject_Call函数,这是一个相当范型的函数,它将对一切可调用的对象进行“调用”操作。具体说,最终PyObject_Call将调用func参数对应的类型对象中所定义的tp_call操作。

    那么在本例中,func对象实际上就是一个PyCFunctionObject对象,它的类型对象是PyCFunction_Type,它的tp_call定义为PyCFunction_Call(可以看出PyCFunctionObject对象确实是一个可调用对象)。

    // methodobject.c
    PyObject *
    PyCFunction_Call(PyObject *func, PyObject *arg, PyObject *kw)
    {
        PyCFunctionObject* f = (PyCFunctionObject*)func;
        PyCFunction meth = PyCFunction_GET_FUNCTION(func);
        PyObject *self = PyCFunction_GET_SELF(func);
        Py_ssize_t size;
    
        switch (PyCFunction_GET_FLAGS(func) & ~(METH_CLASS | METH_STATIC | METH_COEXIST)) {
        case METH_VARARGS:
            if (kw == NULL || PyDict_Size(kw) == 0)
                return (*meth)(self, arg);
            break;
        case METH_VARARGS | METH_KEYWORDS:
        case METH_OLDARGS | METH_KEYWORDS:
            // 函数调用
            return (*(PyCFunctionWithKeywords)meth)(self, arg, kw);
        ......
        }
        PyErr_Format(PyExc_TypeError, "%.200s() takes no keyword arguments",
                 f->m_ml->ml_name);
        return NULL;
    }
    

    在PyCFunction_Call中,Python虚拟机从PyCFunctionObject对象中抽取出它维护的那个函数指针meth(这个指针指向的是builtin___import__函数)。builtin___import__才是真正实现import操作的地方。

    import的语法有多种(import sys、from sys import path as mypath等);import的目标也有多种(Python标准module、用户自定义的module、Python写的module、C语言写的以dll形式存在的module)。

    0x02 Python中import机制的黑盒探测

    忽略

    0x03 import机制的实现

    Python的import机制实现的功能:

    • Python运行时的全局module pool的维护和搜索;
    • 解析与搜索module路径的树状结构;
    • 对不同文件格式的module的动态加载机制。

    这里我们分析的import格式是import x.y.z(其他形式都可以归结为此类型)。

    // bltinmodule.c
    static PyObject *
    builtin___import__(PyObject *self, PyObject *args, PyObject *kwds)
    {
        static char *kwlist[] = {"name", "globals", "locals", "fromlist",
                     "level", 0};
        char *name;
        PyObject *globals = NULL;
        PyObject *locals = NULL;
        PyObject *fromlist = NULL;
        int level = -1;
        // 从tuple解析出需要的信息
        if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|OOOi:__import__",
                kwlist, &name, &globals, &locals, &fromlist, &level))
            return NULL;
        return PyImport_ImportModuleLevel(name, globals, locals,
                          fromlist, level);
    }
    

    这里的PyArg_ParseTupleAndKeywords函数需要重点说一下,它的函数原型是:int PyArg_ParseTupleAndKeywords(PyObject *args, PyObject *keywords, const char *format, char **kwlist, ...)

    这个函数的目的是将args和keywords中所包含的所有对象按照format中指定的格式解析成各种目标对象(目标对象可以是Python中对象也可以是C中的原生类型)。

    args实际上就是之前我们打包的PyTupleObject对象,里面包含了所有import需要的参数和信息。

    format参数可用的格式字符非常多,这里大概说一下import机制使用到的"s|OOOi:__import__"

    • s表示目标对象是一个char *,通常用来将tuple中的PyStringObject对象解析成char *;
    • i表示tuple中的PyIntObject对象解析成int类型的值;
    • o表示解析的对象是Python中合法的对象,故不进行任何的解析和转换;
    • |是指示字符,非格式字符,表示其后的所带的格式字符是可选的;
    • :也是指示字符,表示格式字符到此结束。其后所带的字符串在程序出错时输出错误信息时用。
    // import.c
    PyObject *
    PyImport_ImportModuleLevel(char *name, PyObject *globals, PyObject *locals,
                 PyObject *fromlist, int level)
    {
        PyObject *result;
        lock_import();
        result = import_module_level(name, globals, locals, fromlist, level);
        if (unlock_import() < 0) {
            Py_XDECREF(result);
            PyErr_SetString(PyExc_RuntimeError,
                    "not holding the import lock");
            return NULL;
        }
        return result;
    }
    

    Python虚拟机在进行import之前,会动import这个动作上锁,这样做是为了同步不同的线程对同一个module的import动作(线程安全的线程同步问题),执行完import动作以后再释放锁。

    // import.c
    static PyObject *
    import_module_level(char *name, PyObject *globals, PyObject *locals,
                PyObject *fromlist, int level)
    {
        char buf[MAXPATHLEN+1];
        Py_ssize_t buflen = 0;
        PyObject *parent, *head, *next, *tail;
        // 获得import发生的package环境
        parent = get_parent(globals, buf, &buflen, level);
        if (parent == NULL)
            return NULL;
        // 解析module的路径结构,依次加载每一个package/module
        head = load_next(parent, Py_None, &name, buf, &buflen);
        if (head == NULL)
            return NULL;
        tail = head;
        Py_INCREF(tail);
        while (name) {
            next = load_next(tail, tail, &name, buf, &buflen);
            Py_DECREF(tail);
            if (next == NULL) {
                Py_DECREF(head);
                return NULL;
            }
            tail = next;
        }
        ......
        // 处理from ... import ...语句
        if (fromlist != NULL) {
            if (fromlist == Py_None || !PyObject_IsTrue(fromlist))
                fromlist = NULL;
        }
        // import语句不是from ... import ...形式,返回head
        if (fromlist == NULL) {
            Py_DECREF(tail);
            return head;
        }
        Py_DECREF(head);
        // import的形式是from ... import ...,返回tail
        if (!ensure_fromlist(tail, fromlist, buf, buflen, 0)) {
            Py_DECREF(tail);
            return NULL;
        }
        return tail;
    }
    

    上面代码可以看出,之前字节码中的返回值就是在这里返回的(head/tail),返回值依赖fromlist的值,一般情况下fromlist都是Py_None,但是当import语句是"from a import b,c"时,fromlist就是一个类似(b, c)这样的PyTupleObject对象。

    解析module/package树状结构

    import_module_level函数的代码主要实现了对x.y.z这样的树状结构的遍历,遍历的规则是把x.y.z看做是一个二叉树,然后遍历整个二叉树,对每个节点都只访问其右子树。

    // import.c
    static PyObject *
    get_parent(PyObject *globals, char *buf, Py_ssize_t *p_buflen, int level)
    {
        static PyObject *namestr = NULL;
        static PyObject *pathstr = NULL;
        PyObject *modname, *modpath, *modules, *parent;
    
        if (globals == NULL || !PyDict_Check(globals) || !level)
            return Py_None;
    
        if (namestr == NULL) {
            // 获得当前的module的名字
            namestr = PyString_InternFromString("__name__");
            if (namestr == NULL)
                return NULL;
        }
        if (pathstr == NULL) {
            pathstr = PyString_InternFromString("__path__");
            if (pathstr == NULL)
                return NULL;
        }
    
        *buf = '\0';
        *p_buflen = 0;
        modname = PyDict_GetItem(globals, namestr);
        if (modname == NULL || !PyString_Check(modname))
            return Py_None;
    
        modpath = PyDict_GetItem(globals, pathstr);
        if (modpath != NULL) {
            // 在package的__init__.py中进行import动作
            Py_ssize_t len = PyString_GET_SIZE(modname);
            if (len > MAXPATHLEN) {
                PyErr_SetString(PyExc_ValueError,
                        "Module name too long");
                return NULL;
            }
            strcpy(buf, PyString_AS_STRING(modname));
        }
        else {
            // 在package的module中进行import动作
            char *start = PyString_AS_STRING(modname);
            char *lastdot = strrchr(start, '.');
            size_t len;
            if (lastdot == NULL && level > 0) {
                PyErr_SetString(PyExc_ValueError,
                    "Attempted relative import in non-package");
                return NULL;
            }
            if (lastdot == NULL)
                return Py_None;
            len = lastdot - start;
            if (len >= MAXPATHLEN) {
                PyErr_SetString(PyExc_ValueError,
                        "Module name too long");
                return NULL;
            }
            strncpy(buf, start, len);
            buf[len] = '\0';
        }
    
        while (--level > 0) {
            char *dot = strrchr(buf, '.');
            if (dot == NULL) {
                PyErr_SetString(PyExc_ValueError,
                    "Attempted relative import beyond "
                    "toplevel package");
                return NULL;
            }
            *dot = '\0';
        }
        *p_buflen = strlen(buf);
        // 在sys.module中查找当前package的名字对应的module对象
        modules = PyImport_GetModuleDict();
        parent = PyDict_GetItemString(modules, buf);
        if (parent == NULL)
            PyErr_Format(PyExc_SystemError,
                    "Parent module '%.200s' not loaded", buf);
        return parent;
        /* We expect, but can't guarantee, if parent != None, that:
           - parent.__name__ == buf
           - parent.__dict__ is globals
           If this is violated...  Who cares? */
    }
    

    上面代码中,level一般情况下都为-1,这时level不对get_parent产生影响,所以这里不用考虑。

    函数get_parent的功能是返回一个package,这个package是当前的import动作执行的环境。

    Python中的import动作都是发生在某一个package的环境中,而不是一个module的环境中。

    在上面代码中获得了import动作执行的package环境后,Python虚拟机立即通过load_next开始了在package环境中对module的import动作:

    0x04 Python中的import操作

    0x05 与module有关的名字空间问题


    欢迎关注微信公众号(coder0x00)或扫描下方二维码关注,我们将持续搜寻程序员必备基础技能包提供给大家。


    相关文章

      网友评论

          本文标题:【Python】动态加载机制

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