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