0x00 前言
-
py
文件是如何执行的?-
py
代码不会转换成一系列的机器指令,然后让cpu
去执行 -
Python
的本质和Java/C#
是一样的,都是通过虚拟机来执行字节码实现的
-
-
Python
解释器是什么?- 就是平时调用的
/usr/bin/python
可执行文件 - 它包含
Python
编译器和Python
虚拟机 -
Python
解释器在执行Python
程序文件的时候。会首先使用Python
编译器对源文件进行编译,编译会产生Python
的一组字节码(byte code
)(当然还会产生其他内容。。。);然后将编译结果交给Python
虚拟机,由虚拟机一条一条顺序执行字节码
- 就是平时调用的
-
Python
编译器和虚拟机在什么地方?- 没有在
python.exe
中,而是在python2*.dll
动态库中,编译和执行的过程都调用了python2*.dll
动态库 - 对比
java
:a.java
通过javac
编译成a.class
文件,然后通过java
执行a.class
中的字节码,得到执行结果
- 没有在
-
py
源文件经过Python
编译器后会产生什么?- 代码中操作类型的内容会生成字节码
- 字符串、常量值和字节码等静态信息都会存储为一个
PyCodeObject
对象(Python
运行时的对象);通过也会存储到一个文件中,就是pyc
文件 -
PyCodeObject
对象和pyc
文件之间的关系就是序列化和反序列化的关系
python interpreter
0x01 PyCodeObject
typedef struct {
PyObject_HEAD
int co_argcount; /*Code Block位置参数的个数 #arguments, except *args */
int co_nlocals; /*Code Block中局部变量的个数,包括位置参数的个数 #local variables */
int co_stacksize; /*执行该段Code Block需要的栈空间 #entries needed for evaluation stack */
int co_flags; /* CO_..., see below */
PyObject *co_code; /*Code Block编译得到的字节码指令序列 instruction opcodes */
PyObject *co_consts; /*Code Block中的所有常量 list (constants used) */
PyObject *co_names; /*Code Block中的所有符号 list of strings (names used) */
PyObject *co_varnames; /*Code Block中局部变量名集合 tuple of strings (local variable names) */
PyObject *co_freevars; /*实现闭包需要的对象 tuple of strings (free variable names) */
PyObject *co_cellvars; /*Code Block中内部嵌套函数所引用的局部变量名集合 tuple of strings (cell variable names) */
/* The rest doesn't count for hash/cmp */
PyObject *co_filename; /*Code Block对应的py文件的完整路径 string (where it was loaded from) */
PyObject *co_name; /*Code Block的名字,通常是函数名或类名 string (name, for reference) */
int co_firstlineno; /*Code Block在对应的py文件中的起始行 first source line number */
PyObject *co_lnotab; /*字节码指令与py文件中源代码行号的对应关系 string (encoding addr<->lineno mapping) */
void *co_zombieframe; /* for optimization only (see frameobject.c) */
} PyCodeObject;
-
py
文件中,一段code block
都会产生一个PyCodeObject
对象;- 怎样才算一段
code block
呢?其实从静态代码就可以观察出来(依据Python
语法),属于一个作用域的就是一个code block
,也可以说是名字空间(作用域是静态的,名字空间是运行时的)。
- 怎样才算一段
字节码
/* opcode.h */
#define STOP_CODE 0
#define POP_TOP 1
#define ROT_TWO 2
#define ROT_THREE 3
#define DUP_TOP 4
#define ROT_FOUR 5
#define NOP 9
#define UNARY_POSITIVE 10
#define UNARY_NEGATIVE 11
#define UNARY_NOT 12
#define UNARY_CONVERT 13
#define UNARY_INVERT 15
#define LIST_APPEND 18
#define BINARY_POWER 19
#define BINARY_MULTIPLY 20
#define BINARY_DIVIDE 21
#define BINARY_MODULO 22
#define BINARY_ADD 23
#define BINARY_SUBTRACT 24
#define BINARY_SUBSCR 25
#define BINARY_FLOOR_DIVIDE 26
#define BINARY_TRUE_DIVIDE 27
#define INPLACE_FLOOR_DIVIDE 28
#define INPLACE_TRUE_DIVIDE 29
#define SLICE 30
/* Also uses 31-33 */
#define STORE_SLICE 40
/* Also uses 41-43 */
#define DELETE_SLICE 50
/* Also uses 51-53 */
#define INPLACE_ADD 55
#define INPLACE_SUBTRACT 56
#define INPLACE_MULTIPLY 57
#define INPLACE_DIVIDE 58
#define INPLACE_MODULO 59
#define STORE_SUBSCR 60
#define DELETE_SUBSCR 61
#define BINARY_LSHIFT 62
#define BINARY_RSHIFT 63
#define BINARY_AND 64
#define BINARY_XOR 65
#define BINARY_OR 66
#define INPLACE_POWER 67
#define GET_ITER 68
#define PRINT_EXPR 70
#define PRINT_ITEM 71
#define PRINT_NEWLINE 72
#define PRINT_ITEM_TO 73
#define PRINT_NEWLINE_TO 74
#define INPLACE_LSHIFT 75
#define INPLACE_RSHIFT 76
#define INPLACE_AND 77
#define INPLACE_XOR 78
#define INPLACE_OR 79
#define BREAK_LOOP 80
#define WITH_CLEANUP 81
#define LOAD_LOCALS 82
#define RETURN_VALUE 83
#define IMPORT_STAR 84
#define EXEC_STMT 85
#define YIELD_VALUE 86
#define POP_BLOCK 87
#define END_FINALLY 88
#define BUILD_CLASS 89
#define HAVE_ARGUMENT 90 /* Opcodes from here have an argument: */
#define STORE_NAME 90 /* Index in name list */
#define DELETE_NAME 91 /* "" */
#define UNPACK_SEQUENCE 92 /* Number of sequence items */
#define FOR_ITER 93
#define STORE_ATTR 95 /* Index in name list */
#define DELETE_ATTR 96 /* "" */
#define STORE_GLOBAL 97 /* "" */
#define DELETE_GLOBAL 98 /* "" */
#define DUP_TOPX 99 /* number of items to duplicate */
#define LOAD_CONST 100 /* Index in const list */
#define LOAD_NAME 101 /* Index in name list */
#define BUILD_TUPLE 102 /* Number of tuple items */
#define BUILD_LIST 103 /* Number of list items */
#define BUILD_MAP 104 /* Always zero for now */
#define LOAD_ATTR 105 /* Index in name list */
#define COMPARE_OP 106 /* Comparison operator */
#define IMPORT_NAME 107 /* Index in name list */
#define IMPORT_FROM 108 /* Index in name list */
#define JUMP_FORWARD 110 /* Number of bytes to skip */
#define JUMP_IF_FALSE 111 /* "" */
#define JUMP_IF_TRUE 112 /* "" */
#define JUMP_ABSOLUTE 113 /* Target byte offset from beginning of code */
#define LOAD_GLOBAL 116 /* Index in name list */
#define CONTINUE_LOOP 119 /* Start of loop (absolute) */
#define SETUP_LOOP 120 /* Target address (relative) */
#define SETUP_EXCEPT 121 /* "" */
#define SETUP_FINALLY 122 /* "" */
#define LOAD_FAST 124 /* Local variable number */
#define STORE_FAST 125 /* Local variable number */
#define DELETE_FAST 126 /* Local variable number */
#define RAISE_VARARGS 130 /* Number of raise arguments (1, 2 or 3) */
/* CALL_FUNCTION_XXX opcodes defined below depend on this definition */
#define CALL_FUNCTION 131 /* #args + (#kwargs<<8) */
#define MAKE_FUNCTION 132 /* #defaults */
#define BUILD_SLICE 133 /* Number of items */
#define MAKE_CLOSURE 134 /* #free vars */
#define LOAD_CLOSURE 135 /* Load free variable from closure */
#define LOAD_DEREF 136 /* Load and dereference from closure cell */
#define STORE_DEREF 137 /* Store into cell */
/* The next 3 opcodes must be contiguous and satisfy
(CALL_FUNCTION_VAR - CALL_FUNCTION) & 3 == 1 */
#define CALL_FUNCTION_VAR 140 /* #args + (#kwargs<<8) */
#define CALL_FUNCTION_KW 141 /* #args + (#kwargs<<8) */
#define CALL_FUNCTION_VAR_KW 142 /* #args + (#kwargs<<8) */
/* Support for opargs more than 16 bits long */
#define EXTENDED_ARG 143
0x02 pyc文件
- 字节码指令和
PyCodeObject
对象(其中包含了字节码)都会存储到pyc
文件中 - 直接执行
python test.py
并不会产生pyc
文件,import
语句会触发生成pyc文件(执行到import
语句时,先查看有没有对应的pyc
文件,如果没有,会创建PyCodeObject
对象,将对象写入文件中,接下来Python
重新从pyc
文件执行import
动作) -
py_compile
、compiler
标准库也可以生成pyc
文件 -
pyc
文件是二进制文件,想知道其中含义,需要知道PyCodeObject
对象的结构,即每一个域的含义
pyc文件的读写
static void
write_compiled_module(PyCodeObject *co, char *cpathname, time_t mtime)
{
FILE *fp;
fp = open_exclusive(cpathname);
if (fp == NULL) {
if (Py_VerboseFlag)
PySys_WriteStderr(
"# can't create %s\n", cpathname);
return;
}
/* ==写入magic number */
PyMarshal_WriteLongToFile(pyc_magic, fp, Py_MARSHAL_VERSION);
/* First write a 0 for mtime */
/* ==写入时间信息 */
PyMarshal_WriteLongToFile(0L, fp, Py_MARSHAL_VERSION);
/* ==写入PyCodeObject对象信息 */
PyMarshal_WriteObjectToFile((PyObject *)co, fp, Py_MARSHAL_VERSION);
if (fflush(fp) != 0 || ferror(fp)) {
if (Py_VerboseFlag)
PySys_WriteStderr("# can't write %s\n", cpathname);
/* Don't keep partial file */
fclose(fp);
(void) unlink(cpathname);
return;
}
/* Now write the true mtime */
fseek(fp, 4L, 0);
assert(mtime < LONG_MAX);
PyMarshal_WriteLongToFile((long)mtime, fp, Py_MARSHAL_VERSION);
fflush(fp);
fclose(fp);
if (Py_VerboseFlag)
PySys_WriteStderr("# wrote %s\n", cpathname);
}
- 一个
pyc
文件会包含三个独立的信息:magic number
、时间、PyCodeObject
对象-
magic number
用来区分不同的Python
版本,每个版本的magic number
不同(为了兼容不同的Python
版本,2.5
版本不能import2.7
的pyc
文件等情况)。 - 时间信息用来保持
pyc
文件是最新的py
文件生成的 -
PyCodeObject
对象的写入
-
void
PyMarshal_WriteLongToFile(long x, FILE *fp, int version)
{
WFILE wf;
wf.fp = fp;
wf.error = 0;
wf.depth = 0;
wf.strings = NULL;
wf.version = version;
w_long(x, &wf);
}
void
PyMarshal_WriteObjectToFile(PyObject *x, FILE *fp, int version)
{
WFILE wf;
wf.fp = fp;
wf.error = 0;
wf.depth = 0;
wf.strings = (version > 0) ? PyDict_New() : NULL;
wf.version = version;
w_object(x, &wf);
Py_XDECREF(wf.strings);
}
- 以上是将内容写入文件调用的函数
-
w_long()
函数就是将数据一个字节一个字节写入文件中 -
w_object()
函数的代码很长,但是逻辑很简单,内部其实就是针对不同的类型(int
、string
等)做不同的操作。但是最源头都是w_long
和w_string
(毕竟所有的字符最后不是数字就是字母)。 -
Python
在向pyc
文件写入list
的时候,只是将list
中的数值或字符串写入文件;这也就意味着Python
在加载pyc
文件的时候,需要根据这些数值或字符串重新组装list
。 - 写入每一种类型的数据时,都会先写入一个类型标识,要不然咋知道那一堆二进制字节流是什么意思呢。一个类型标识符的出现,预示着一个对象的开始,上一个对象的结束。
- 类型标识符如下所示:
#define TYPE_NULL '0'
#define TYPE_NONE 'N'
#define TYPE_FALSE 'F'
#define TYPE_TRUE 'T'
#define TYPE_STOPITER 'S'
#define TYPE_ELLIPSIS '.'
#define TYPE_INT 'i'
#define TYPE_INT64 'I'
#define TYPE_FLOAT 'f'
#define TYPE_BINARY_FLOAT 'g'
#define TYPE_COMPLEX 'x'
#define TYPE_BINARY_COMPLEX 'y'
#define TYPE_LONG 'l'
#define TYPE_STRING 's'
#define TYPE_INTERNED 't'
#define TYPE_STRINGREF 'R'
#define TYPE_TUPLE '('
#define TYPE_LIST '['
#define TYPE_DICT '{'
#define TYPE_CODE 'c'
#define TYPE_UNICODE 'u'
#define TYPE_UNKNOWN '?'
#define TYPE_SET '<'
#define TYPE_FROZENSET '>'
- 不论写入多复杂的对象,归结到最后都是数值的写入和字符串的写入。
- 数值写入很简单,直接写入即可。
- 字符串的写入相对复杂。
字符串写入pyc文件
typedef struct {
FILE *fp;
int error;
int depth;
/* If fp == NULL, the following are valid: */
PyObject *str;
char *ptr;
char *end;
PyObject *strings; /* dict on marshal, list on unmarshal */
int version;
} WFILE;
-
WFILE
结构就是对FILE
文件句柄的封装,strings
域需要重点关注一下,它在向pyc
文件写入时,string
会指向PyDictObject
对象;而从pyc
文件读出时,string
则会指向PyListObject
对象。 -
写入
pyc
文件时,PyMarshal_WriteObjectToFile()
函数就创建了PyDictObject
对象(wf.strings = (version > 0) ? PyDict_New() : NULL;
)。 -
string
对象的键值对存储的结构为(PyStringObject
,PyIntObject
),PyIntObject
对象表示的是第几个被写入到pyc
文件的intern
字符串(类似一个索引,到从pyc
文件读取时,即string
指向PyListObject
对象时会用到)。
/* w_object中处理string的代码 */
else if (PyString_Check(v)) {
if (p->strings && PyString_CHECK_INTERNED(v)) {
PyObject *o = PyDict_GetItem(p->strings, v);
if (o) {
long w = PyInt_AsLong(o);
w_byte(TYPE_STRINGREF, p);
w_long(w, p);
goto exit;
}
else {
int ok;
o = PyInt_FromSsize_t(PyDict_Size(p->strings));
ok = o &&
PyDict_SetItem(p->strings, v, o) >= 0;
Py_XDECREF(o);
if (!ok) {
p->depth--;
p->error = 1;
return;
}
w_byte(TYPE_INTERNED, p);
}
}
else {
w_byte(TYPE_STRING, p);
}
n = PyString_GET_SIZE(v);
if (n > INT_MAX) {
/* huge strings are not supported */
p->depth--;
p->error = 1;
return;
}
w_long((long)n, p);
w_string(PyString_AS_STRING(v), (int)n, p);
}
- 写入分为三种情况
- 普通字符串:写入类型标识
TYPE_STRING
,w_long()
写入字符串长度,w_string()
写入字符串本身 -
intern
字符串首次写入:string
字典中加入字符串的键值对,写入类型标识符TYPE_INTERNED
,后面和普通字符串一样 -
intern
字符串非首次写入:写入类型标识TYPE_STRINGREF
,然后w_long()
写入string
中查找到的key
对应的value
值
- 普通字符串:写入类型标识
pyc文件读出字符串
PyObject *
PyMarshal_ReadObjectFromFile(FILE *fp)
{
RFILE rf;
PyObject *result;
rf.fp = fp;
rf.strings = PyList_New(0);
rf.depth = 0;
rf.ptr = rf.end = NULL;
result = r_object(&rf);
Py_DECREF(rf.strings);
return result;
}
- 如上所示,
pyc
文件读取内容时,string
被初始化为PyListObject
对象 - 进入
r_object()
(r_object
是w_object
的逆运算),开始从pyc
文件读取数据,创建PyCodeObject
对象。 - 当读取到
TYPE_INTERNED
标识符时,将标识符后面的字符串进行intern
操作,然后将它加到string
指向的PyListObject
对象中; - 当读取到
TYPE_STRINGREF
标识符时,直接根据写入时存储的索引ID
在string
中查找已经intern
的字符串
欢迎关注微信公众号(coder0x00)或扫描下方二维码关注,我们将持续搜寻程序员必备基础技能包提供给大家。
网友评论