《python源码剖析-字节码和虚拟机》

|   分类于 python源码剖析  |

https://fanchao01.github.io/blog/categories/python%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90/

https://fanchao01.github.io/blog/2014/12/26/python-GIL/


python源码剖析-字节码和虚拟机

发表于 2016-11-13   |   分类于 python源码剖析  |

Python会将代码先编译成字节码,然后在虚拟机中动态得依次解释执行字节码。编译好的字节码存储在硬盘中以.pyc.pyd等为扩展名。而在运行态,这些字节码会作为Python的一种对象PyCodeObject存在。PyCodeObject可以理解为C语言中的文本段,用于存储编译后的字节码、调试信息、常量值、变量名等。

本文不会讲述代码如何一步步编译成PyCodeObject,只会简单介绍PyCodeObject中各个域的含义,而把重点放在介绍Python的虚拟机和执行流。

Python中的伪码PyCodeObject



PyCodeObject保存代编译后的静态信息,在运行时再结合上下文形成一个完整的运行态环境。让我们看看静态编译后的信息都有哪些。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21


typedef struct {

PyObject_HEAD

int co_argcount; // co_argcount 参数,不包括不定参数

int co_nlocals; // co_nlocals 变量个数,co_argcount +

// 可变参数个数 + co_kwonlyargcount(py3.0) + 局部变量个数

int co_stacksize; // 栈的大小 (编译后需要的最大栈深度)

int co_flags; // PyCodeObject的一些标志位,用来优化运行时的性能

PyObject *co_code; // 编译后的字节码字符串

PyObject *co_consts; // 常量的列表

PyObject *co_names; // 常量中的字符串对象

PyObject *co_varnames; // 变量名字的元组

PyObject *co_freevars; // 自由变量的元组

PyObject *co_cellvars; // cell变量的元组

/* The rest doesn‘t count for hash/cmp */

PyObject *co_filename; // 文件名

PyObject *co_name; // 对象的名字,例如函数的名字、类的名字等

int co_firstlineno; // 对应的代码在源码文件中的起始行号

PyObject *co_lnotab; // 伪码与行号的映射

void *co_zombieframe; // 对于一些特殊情况下的优化

PyObject *co_weakreflist; // 支持弱引用

} PyCodeObject;

其中有些域需要特别解释。

  • co_flags 用来保存一些编译信息,主要用于优化工作。例如co_VARARGS(0x0004)表示有可变参数等,具体见code.h文件。
  • co_freevars 自由变量是一些在作用域内使用,但是没有在本作用域定义的变量。
  • co_cellvars 当前作用域定义,而在闭包等内部使用的变量。
  • co_lnotab 字节码的偏移值与对应的源码的行号的相对值。

1

2

3

4


字节码在co_code中的偏移值 真实行号 行号的偏移值

0 1 0

6 2 1

50 7 5

那么实际上co_lnotab记录的是(0, 0), (6, 1), (44, 5),当然实际记录中没有括号。具体偏移值和真实行号的对应关系可以通过下面的算法计算出来。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16


// codeobject.c

int

PyCode_Addr2Line(PyCodeObject *co, int addrq)

{

int size = PyString_Size(co->co_lnotab) / 2;

unsigned char *p = (unsigned char*)PyString_AsString(co->co_lnotab);

int line = co->co_firstlineno;

int addr = 0;

while (--size >= 0) {

addr += *p++;

if (addr > addrq)

break;

line += *p++;

}

return line;

}

  • co_code 记录编译后的字节码,以字符串的形式保存,而实际上就是数字。后面我们通过一个例子详细描述。

PyCodeObject的示例



先给定一个Python代码示例,然后打印出其中的各个域。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47


from __future__ import print_function

import dis

def out(a, b=1, *args, **kwargs):

c = 2

def inner(d, e=3, *iargs, **ikwargs):

f = 4

g = c

print(‘inner-->co_argcount :‘, inner.__code__.co_argcount)

# print(‘inner-->co_kwonlyargcount :‘, inner.__code__.co_kwonlyargcount)

print(‘inner-->co_nlocals :‘, inner.__code__.co_nlocals)

print(‘inner-->co_stacksize :‘, inner.__code__.co_stacksize)

print(‘inner-->co_flags :‘, inner.__code__.co_flags)

print(‘inner-->co_code :‘, inner.__code__.co_code)

print(‘inner-->co_consts :‘, inner.__code__.co_consts)

print(‘inner-->co_names :‘, inner.__code__.co_names)

print(‘inner-->co_varnames :‘, inner.__code__.co_varnames)

print(‘inner-->co_freevars :‘, inner.__code__.co_freevars)

print(‘inner-->co_cellvars :‘, inner.__code__.co_cellvars)

print(‘inner-->co_filename :‘, inner.__code__.co_filename)

print(‘inner-->co_name :‘, inner.__code__.co_name)

print(‘inner-->co_firstlineno :‘, inner.__code__.co_firstlineno)

print(‘inner-->co_lnotab :‘, inner.__code__.co_lnotab)

print(‘out-->co_argcount :‘, out.__code__.co_argcount)

#print(‘out-->co_kwonlyargcount :‘, out.__code__.co_kwonlyargcount)

print(‘out-->co_nlocals :‘, out.__code__.co_nlocals)

print(‘out-->co_stacksize :‘, out.__code__.co_stacksize)

print(‘out-->co_flags :‘, out.__code__.co_flags)

print(‘out-->co_code :‘, out.__code__.co_code)

print(‘out-->co_consts :‘, out.__code__.co_consts)

print(‘out-->co_names :‘, out.__code__.co_names)

print(‘out-->co_varnames :‘, out.__code__.co_varnames)

print(‘out-->co_freevars :‘, out.__code__.co_freevars)

print(‘out-->co_cellvars :‘, out.__code__.co_cellvars)

print(‘out-->co_filename :‘, out.__code__.co_filename)

print(‘out-->co_name :‘, out.__code__.co_name)

print(‘out-->co_firstlineno :‘, out.__code__.co_firstlineno)

print(‘out-->co_lnotab :‘, out.__code__.co_lnotab)

print(‘=========================================================‘)

out(1, 2, 3, 4, 5, 6, 7, e = 8, f = 9)

print()

print(‘disamble:‘)

print(dis.dis(out))

需要先解释一下co_kwonlyargcount,这个域在PY3才有,用于支持在不定参数后定义的位置参数,例如def func(*args, kwonly=None)

这个实例的输出可以看到对应的各个域的详细内容。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29


out-->co_argcount : 2 # a, b

out-->co_nlocals : 5 # a, b, c, d, e

out-->co_stacksize : 3

out-->co_flags : 65551 # b‘0b10000000000001111‘ CO_FUTURE_PRINT_FUNCTION|CO_VARKEYWORDS|CO_VARARGS|CO_NEWLOCALS|CO_OPTIMIZED

out-->co_code : ddfd}t... # 部分省略,后续分析

out-->co_consts : (None, 2, 3, <code object inner>, ‘inner-->co_argcount :‘, # 省略其他‘inner-->‘) # 常量值,这里添加了默认返回值None

out-->co_names : (‘print‘, ‘__code__‘, ‘co_argcount‘, ‘co_nlocals‘, ‘co_stacksize‘, ‘co_flags‘, ‘co_code‘, ‘co_consts‘, ‘co_names‘, ‘co_varnames‘, ‘co_freevars‘,‘co_cellvars‘, ‘co_filename‘, ‘co_name‘, ‘co_firstlineno‘, ‘co_lnotab‘) # 常量名

out-->co_varnames : (‘a‘, ‘b‘, ‘args‘, ‘kwargs‘, ‘inner‘) # 变量名字,包括参数变量和内部变量

out-->co_freevars : () # 无

out-->co_cellvars : (‘c‘,) # 用于给子作用域使用的变量

out-->co_filename : pycode.py

out-->co_name : out

out-->co_firstlineno : 3 # 起始行号

out-->co_lnotab : # 省略

=========================================================

inner-->co_argcount : 2 # d, e

inner-->co_nlocals : 6 # d, e, iargs, ikwargs, f, g

inner-->co_stacksize : 1 #

inner-->co_flags : 65567 # ‘0b10000000000011111‘ CO_FUTURE_PRINT_FUNCTION|CO_NESTED |CO_VARKEYWORDS|CO_VARARGS|CO_NEWLOCALS|CO_OPTIMIZED

inner-->co_code : d}}dS # 省略

inner-->co_consts : (None, 4) # 常量

inner-->co_names : () #

inner-->co_varnames : (‘d‘, ‘e‘, ‘iargs‘, ‘ikwargs‘, ‘f‘, ‘g‘) # 变量名字

inner-->co_freevars : (‘c‘,) # 自由变量,引用的父作用域的变量

inner-->co_cellvars : () # 无

inner-->co_filename : pycode.py

inner-->co_name : inner

inner-->co_firstlineno : 6 # 起始行号

inner-->co_lnotab : # 省略

从这个例子中可以清楚了解常量、变量、自由变量以及cell变量的含义。接下来我们看下co_code的含义,使用linux的xdd工具将其转换成十六进制,并且使用dis模块反编译其字节码。


1

2

3

4

5

6

7

8

9

10

11


import dis

def out(a, b=1, *args, **kwargs):

c = 2

def inner(d, e=3, *iargs, **ikwargs):

f = 4

g = c

print out.__code__.co_code

dis.dis(out)


1

2

3


# co_code的十六进制内容

0000000: 6401 0089 0000 6402 0087 0000 6601 0064 d.....d.....f..d

0000010: 0300 8601 007d 0400 6400 0053 0a .....}..d..S.


1

2

3

4

5

6

7

8

9

10

11

12


# 字节码的反编译

4 0 LOAD_CONST 1 (2)

3 STORE_DEREF 0 (c)

6 6 LOAD_CONST 2 (3)

9 LOAD_CLOSURE 0 (c)

12 BUILD_TUPLE 1

15 LOAD_CONST 3 (<code object inner at 00000000039E69B0, file "<ipython-input-2-656e8bface8a>", line 6>)

18 MAKE_CLOSURE 1

21 STORE_FAST 4 (inner)

24 LOAD_CONST 0 (None)

27 RETURN_VALUE

  • 十六进制的第一个为64100,查阅opcode.h可以看到起对应的字节码#define LOAD_CONST 100,与反编译中的命令LOAD_CONST相符。
  • 十六进制的第二个为0101,对应的是字节码LOAD_CONST的参数1
  • 十六进制的第三个为0000,此值表示STOP_CDOE,一个完整字节码的结束标志。

同理可以解析接下来的字节码和对应的操作的含义。至此,我们明白字节码的格式为


1

字节码指令编号(64) 多个参数值(1) 结束标志(00)

到现在为止我们明白了字节码的数据结构、各域值的含义,co_code字节码的格式以及如何与操作命令对应。下面我们看看这些字节码如何运行。

PyFrameObject



Python模拟了C语言中的运行栈作为运行时的环境,每个栈用PyFrameObject结构表示。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19


typedef struct _frame {

PyObject_VAR_HEAD

struct _frame *f_back; // 前一个运行栈,调用方

PyCodeObject *f_code; // 执行的PyCodeObject对象

PyObject *f_builtins; // builtins环境变量集合

PyObject *f_globals; // globals全局变量集合

PyObject *f_locals; // locals本地变量集合

PyObject **f_valuestack; // 栈起始地址,最后一个本地变量之后

PyObject **f_stacktop; // 栈针位置,指向栈中下一个空闲位置

PyObject *f_trace; // trace函数

PyObject *f_exc_type, *f_exc_value, *f_exc_traceback; // 记录异常处理

PyThreadState *f_tstate; // 当前的线程

int f_lasti; // 当前执行的字节码的地址

int f_lineno; // 当前的行号

int f_iblock; // 一些局部block块

PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */

PyObject *f_localsplus[1]; // 栈地址,大小为 本地变量+co_stacksize

} PyFrameObject;

对应的结构图

当执行函数调用时会进入新的栈帧,那么当前栈帧就作为下一个栈帧的f_back字段。

多个栈帧链属于一个线程,而同时可能存在多个线程,每个线程拥有一个栈帧链。这样形成了Python的虚拟机运行环境。

Python执行字节码



字节码的执行就像上图所示,由一个大的循环和选择语句构成,逻辑骨干比较简单。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18


for(;;;) {

switch(opcode) {

case 100: # LOAD_CONST

{

x = POP()

... // 执行的具体操作

break;

};

case 101: # LOAD_NAME

{

...

break;

}

...

};

接下来,我们通过反编译代码追踪其如何一步步执行。


1

2

3

4

5

6

7

8

9

10

11

12


# 字节码的反编译

4 0 LOAD_CONST 1 (2)

3 STORE_DEREF 0 (c)

6 6 LOAD_CONST 2 (3)

9 LOAD_CLOSURE 0 (c)

12 BUILD_TUPLE 1

15 LOAD_CONST 3 (<code object inner at 00000000039E69B0, ...>)

18 MAKE_CLOSURE 1

21 STORE_FAST 4 (inner)

24 LOAD_CONST 0 (None)

27 RETURN_VALUE

通过追踪每个指令码的执行过程以及对应的PyFrameObject的栈帧变化,可以一步步看到虚拟机的执行过程。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44


PyObject *

PyEval_EvalFrame(PyFrameObject *f) {

co = f->f_code;

names = co->co_names;

consts = co->co_consts;

fastlocals = f->f_localsplus;

// freevars在内存中对应的不是f->f_freevars,而是f->f_cellvars

freevars = f->f_localsplus + co->co_nlocals;

first_instr = (unsigned char*) PyString_AS_STRING(co->co_code);

// f->f_lasti默认值为-1

next_instr = first_instr + f->f_lasti + 1;

// 执行栈顶

stack_pointer = f->f_stacktop;

for (;;) {

fast_next_opcode:

f->f_lasti = INSTR_OFFSET();

opcode = NEXTOP(); // 获取字节码

oparg = 0;

if (HAS_ARG(opcode)) // 如果字节码有参数,获取参数

oparg = NEXTARG();

TARGET(LOAD_CONST) // 0, 6, 24 行反编译指令LOAD_CONST

{

x = GETITEM(consts, oparg); // 从const中获取值压栈

Py_INCREF(x);

PUSH(x);

FAST_DISPATCH(); // goto fast_next_opcode

}

...

TARGET(STORE_DEREF) // 3

{

w = POP(); // 从栈中取值,设置为CellObejct的值

x = freevars[oparg];

PyCell_Set(x, w);

Py_DECREF(w);

DISPATCH();

}

初始化以及分别执行03字节码的PyFrameObject结构变化。

  • LOAD_CONST 将co_consts中对应的值压栈
  • STORE_DEREF 解引用,设置栈中的变量值


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58


TARGET(LOAD_CLOSURE) // 9

{

x = freevars[oparg];

Py_INCREF(x);

PUSH(x);

if (x != NULL) DISPATCH();

break;

}

TARGET(BUILD_TUPLE) // 12

{

x = PyTuple_New(oparg); // 创建一个元组,并且将栈中的元素设置为元组的元素

if (x != NULL) {

for (; --oparg >= 0;) {

w = POP();

PyTuple_SET_ITEM(x, oparg, w);

}

PUSH(x);

DISPATCH();

}

break;

}

TARGET(MAKE_CLOSURE) // 18

{

v = POP(); /* code object */

x = PyFunction_New(v, f->f_globals); // 创建函数

Py_DECREF(v);

if (x != NULL) {

v = POP();

if (PyFunction_SetClosure(x, v) != 0) {

/* Can‘t happen unless bytecode is corrupt. */

why = WHY_EXCEPTION;

}

Py_DECREF(v);

}

if (x != NULL && oparg > 0) {

v = PyTuple_New(oparg);

if (v == NULL) {

Py_DECREF(x);

x = NULL;

break;

}

while (--oparg >= 0) {

w = POP();

PyTuple_SET_ITEM(v, oparg, w);

}

if (PyFunction_SetDefaults(x, v) != 0) {

/* Can‘t happen unless

PyFunction_SetDefaults changes. */

why = WHY_EXCEPTION;

}

Py_DECREF(v);

}

PUSH(x);

break;

}

  • LOAD_CLOSURE 将freevars中的对象压栈
  • BUILD_TUPLE 用栈帧中的元素创建元组,并压栈
  • BUILD_CLOSURE 创建PyFunction对象,并设置其中的f_closure


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

TARGET(STORE_FAST) // 21

{

v = POP(); // 设置locals值

SETLOCAL(oparg, v);

FAST_DISPATCH();

}

TARGET_NOARG(RETURN_VALUE) // 27

{

retval = POP();

why = WHY_RETURN;

goto fast_block_end;

}

}

  • STORE_FAST 将栈中的一个元素设置到对应的本地变量域中
  • RETURN_VALUE return,并且设置退出原因WHY_RETURN

从上面的代码和过程图,整个代码的执行过程清楚的显现出来:)

(完)

原文地址:https://www.cnblogs.com/cx2016/p/12082984.html

时间: 2024-10-20 12:01:51

《python源码剖析-字节码和虚拟机》的相关文章

CI框架源码阅读笔记3 全局函数Common.php

从本篇开始,将深入CI框架的内部,一步步去探索这个框架的实现.结构和设计. Common.php文件定义了一系列的全局函数(一般来说,全局函数具有最高的加载优先权,因此大多数的框架中BootStrap引导文件都会最先引入全局函数,以便于之后的处理工作). 打开Common.php中,第一行代码就非常诡异: if ( ! defined('BASEPATH')) exit('No direct script access allowed'); 上一篇(CI框架源码阅读笔记2 一切的入口 index

IOS测试框架之:athrun的InstrumentDriver源码阅读笔记

athrun的InstrumentDriver源码阅读笔记 作者:唯一 athrun是淘宝的开源测试项目,InstrumentDriver是ios端的实现,之前在公司项目中用过这个框架,没有深入了解,现在回来记录下. 官方介绍:http://code.taobao.org/p/athrun/wiki/instrumentDriver/ 优点:这个框架是对UIAutomation的java实现,在代码提示.用例维护方面比UIAutomation强多了,借junit4的光,我们可以通过junit4的

Yii源码阅读笔记 - 日志组件

?使用 Yii框架为开发者提供两个静态方法进行日志记录: Yii::log($message, $level, $category);Yii::trace($message, $category); 两者的区别在于后者依赖于应用开启调试模式,即定义常量YII_DEBUG: defined('YII_DEBUG') or define('YII_DEBUG', true); Yii::log方法的调用需要指定message的level和category.category是格式为“xxx.yyy.z

源码阅读笔记 - 1 MSVC2015中的std::sort

大约寒假开始的时候我就已经把std::sort的源码阅读完毕并理解其中的做法了,到了寒假结尾,姑且把它写出来 这是我的第一篇源码阅读笔记,以后会发更多的,包括算法和库实现,源码会按照我自己的代码风格格式化,去掉或者展开用于条件编译或者debug检查的宏,依重要程度重新排序函数,但是不会改变命名方式(虽然MSVC的STL命名实在是我不能接受的那种),对于代码块的解释会在代码块前(上面)用注释标明. template<class _RanIt, class _Diff, class _Pr> in

CI框架源码阅读笔记5 基准测试 BenchMark.php

上一篇博客(CI框架源码阅读笔记4 引导文件CodeIgniter.php)中,我们已经看到:CI中核心流程的核心功能都是由不同的组件来完成的.这些组件类似于一个一个单独的模块,不同的模块完成不同的功能,各模块之间可以相互调用,共同构成了CI的核心骨架. 从本篇开始,将进一步去分析各组件的实现细节,深入CI核心的黑盒内部(研究之后,其实就应该是白盒了,仅仅对于应用来说,它应该算是黑盒),从而更好的去认识.把握这个框架. 按照惯例,在开始之前,我们贴上CI中不完全的核心组件图: 由于BenchMa

CI框架源码阅读笔记2 一切的入口 index.php

上一节(CI框架源码阅读笔记1 - 环境准备.基本术语和框架流程)中,我们提到了CI框架的基本流程,这里这次贴出流程图,以备参考: 作为CI框架的入口文件,源码阅读,自然由此开始.在源码阅读的过程中,我们并不会逐行进行解释,而只解释核心的功能和实现. 1.       设置应用程序环境 define('ENVIRONMENT', 'development'); 这里的development可以是任何你喜欢的环境名称(比如dev,再如test),相对应的,你要在下面的switch case代码块中

Apache Storm源码阅读笔记

欢迎转载,转载请注明出处. 楔子 自从建了Spark交流的QQ群之后,热情加入的同学不少,大家不仅对Spark很热衷对于Storm也是充满好奇.大家都提到一个问题就是有关storm内部实现机理的资料比较少,理解起来非常费劲. 尽管自己也陆续对storm的源码走读发表了一些博文,当时写的时候比较匆忙,有时候衔接的不是太好,此番做了一些整理,主要是针对TridentTopology部分,修改过的内容采用pdf格式发布,方便打印. 文章中有些内容的理解得益于徐明明和fxjwind两位的指点,非常感谢.

CI框架源码阅读笔记4 引导文件CodeIgniter.php

到了这里,终于进入CI框架的核心了.既然是"引导"文件,那么就是对用户的请求.参数等做相应的导向,让用户请求和数据流按照正确的线路各就各位.例如,用户的请求url: http://you.host.com/usr/reg 经过引导文件,实际上会交给Application中的UsrController控制器的reg方法去处理. 这之中,CodeIgniter.php做了哪些工作?我们一步步来看. 1.    导入预定义常量.框架环境初始化 之前的一篇博客(CI框架源码阅读笔记2 一切的入

jdk源码阅读笔记之java集合框架(二)(ArrayList)

关于ArrayList的分析,会从且仅从其添加(add)与删除(remove)方法入手. ArrayList类定义: p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 18.0px Monaco } span.s1 { color: #931a68 } public class ArrayList<E> extends AbstractList<E> implements List<E> ArrayList基本属性: /** *

dubbo源码阅读笔记--服务调用时序

上接dubbo源码阅读笔记--暴露服务时序,继续梳理服务调用时序,下图右面红线流程. 整理了调用时序图 分为3步,connect,decode,invoke. 连接 AllChannelHandler.connected(Channel) line: 38 HeartbeatHandler.connected(Channel) line: 47 MultiMessageHandler(AbstractChannelHandlerDelegate).connected(Channel) line: