热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:《python源码剖析字节码和虚拟机》

篇首语:本文由编程笔记#小编为大家整理,主要介绍了《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源码剖析-字节码和虚拟机

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, , ‘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 (00000000039E69B0, file "", 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 (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

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

(完)






推荐阅读
  • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
  • CentOS7源码编译安装MySQL5.6
    2019独角兽企业重金招聘Python工程师标准一、先在cmake官网下个最新的cmake源码包cmake官网:https:www.cmake.org如此时最新 ... [详细]
  • 本文深入探讨了Linux系统中网卡绑定(bonding)的七种工作模式。网卡绑定技术通过将多个物理网卡组合成一个逻辑网卡,实现网络冗余、带宽聚合和负载均衡,在生产环境中广泛应用。文章详细介绍了每种模式的特点、适用场景及配置方法。 ... [详细]
  • 本文将介绍如何编写一些有趣的VBScript脚本,这些脚本可以在朋友之间进行无害的恶作剧。通过简单的代码示例,帮助您了解VBScript的基本语法和功能。 ... [详细]
  • 1.如何在运行状态查看源代码?查看函数的源代码,我们通常会使用IDE来完成。比如在PyCharm中,你可以Ctrl+鼠标点击进入函数的源代码。那如果没有IDE呢?当我们想使用一个函 ... [详细]
  • 深入理解 SQL 视图、存储过程与事务
    本文详细介绍了SQL中的视图、存储过程和事务的概念及应用。视图为用户提供了一种灵活的数据查询方式,存储过程则封装了复杂的SQL逻辑,而事务确保了数据库操作的完整性和一致性。 ... [详细]
  • 数据库内核开发入门 | 搭建研发环境的初步指南
    本课程将带你从零开始,逐步掌握数据库内核开发的基础知识和实践技能,重点介绍如何搭建OceanBase的开发环境。 ... [详细]
  • 本文详细介绍了Java中org.eclipse.ui.forms.widgets.ExpandableComposite类的addExpansionListener()方法,并提供了多个实际代码示例,帮助开发者更好地理解和使用该方法。这些示例来源于多个知名开源项目,具有很高的参考价值。 ... [详细]
  • 解决PHP与MySQL连接时出现500错误的方法
    本文详细探讨了当使用PHP连接MySQL数据库时遇到500内部服务器错误的多种解决方案,提供了详尽的操作步骤和专业建议。无论是初学者还是有经验的开发者,都能从中受益。 ... [详细]
  • 本文将介绍由密歇根大学Charles Severance教授主讲的顶级Python入门系列课程,该课程广受好评,被誉为Python学习的最佳选择。通过生动有趣的教学方式,帮助初学者轻松掌握编程基础。 ... [详细]
  • 在Ubuntu 16.04 LTS上配置Qt Creator开发环境
    本文详细介绍了如何在Ubuntu 16.04 LTS系统中安装和配置Qt Creator,涵盖了从下载到安装的全过程,并提供了常见问题的解决方案。 ... [详细]
  • DNN Community 和 Professional 版本的主要差异
    本文详细解析了 DotNetNuke (DNN) 的两种主要版本:Community 和 Professional。通过对比两者的功能和附加组件,帮助用户选择最适合其需求的版本。 ... [详细]
  • Python自动化处理:从Word文档提取内容并生成带水印的PDF
    本文介绍如何利用Python实现从特定网站下载Word文档,去除水印并添加自定义水印,最终将文档转换为PDF格式。该方法适用于批量处理和自动化需求。 ... [详细]
  • XNA 3.0 游戏编程:从 XML 文件加载数据
    本文介绍如何在 XNA 3.0 游戏项目中从 XML 文件加载数据。我们将探讨如何将 XML 数据序列化为二进制文件,并通过内容管道加载到游戏中。此外,还会涉及自定义类型读取器和写入器的实现。 ... [详细]
  • 本文详细介绍了macOS系统的核心组件,包括如何管理其安全特性——系统完整性保护(SIP),并探讨了不同版本的更新亮点。对于使用macOS系统的用户来说,了解这些信息有助于更好地管理和优化系统性能。 ... [详细]
author-avatar
zhengfke
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有