【Python】动态加载机制

2018-12-23  本文已影响13人  lndyzwdxhs

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机制实现的功能:

这里我们分析的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__"

// 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)或扫描下方二维码关注,我们将持续搜寻程序员必备基础技能包提供给大家。


上一篇下一篇

猜你喜欢

热点阅读