如果你曾经写过或者用过 Python,你可能已经习惯了看到 Python 源代码文件;它们的名称以.Py 结尾。你可能还见过另一种类型的文件是 .pyc 结尾的,它们就是 Python “字节码”文件。(在 Python3 的时候这个 .pyc 后缀的文件不太好找了,它在一个名为__pycache__的子目录下面。).pyc文件可以防止Python每次运行时都重新解析源代码,该文件大大节省了时间。

Python是如何工作的

Python 通常被描述为一种解释语言,在这种语言中,你的源代码在程序运行时被翻译成CPU指令,但这只是说对了部分。和许多解释型语言一样,Python 实际上将源代码编译为虚拟机的一组指令,Python 解释器就是该虚拟机的实现。其中这种中间格式称为“字节码”。

因此,Python留下的这些.pyc文件,是为了让运行的速快变得 “更快”,或者是针对你的源代码的”优化“的版本;它们是 Python 虚拟机上运行的字节码指令。

Python 虚拟机内幕

CPython使用基于堆栈的虚拟机。也就是说,它完全围绕堆栈数据结构(你可以将项目“推”到结构的“顶部”,或者将项目“弹出”到“顶部”)。

CPython 使用三种类型的栈:

1.调用堆栈。这是运行中的Python程序的主要结构。对于每个当前活动的函数调用,它都有一个项目一“帧”,堆栈的底部是程序的入口点。每次函数调用都会将新的帧推到调用堆栈上,每次函数调用返回时,它的帧都会弹出

2.在每一帧中,都有一个评估堆栈(也称为数据堆栈)。这个堆栈是执行 Python 函数的地方,执行Python代码主要包括将东西推到这个堆栈上,操纵它们,然后将它们弹出。

3.同样在每一帧中,都有一个块堆栈。Python使用它来跟踪某些类型的控制结构:循环、try /except块,以及 with 块都会导致条目被推送到块堆栈上,每当退出这些结构之一时,块堆栈就会弹出。这有助于Python知道在任何给定时刻哪些块是活动的,例如,continue或break语句可以影响正确的块。

大多数 Python 字节码指令操作的是当前调用栈帧的计算栈,虽然,还有一些指令可以做其它的事情(比如跳转到指定指令,或者操作块栈)。

为了更好地理解,假设我们有一些调用函数的代码,比如这个:

my_function(my_variable,2)。

Python 将转换为一系列字节码指令:

1.一个LOAD_NAME指令,用于查找函数对象 my_function,并将其推送到计算栈的顶部

2.另一个 LOAD_NAME 指令去查找变量 my_variable,并将其推送到计算栈的顶部

3.一个 LOAD_CONST 指令将一个整数 2 推送到计算栈的顶部

4.一个 CALL_FUNCTION 指令

CALL_FUNCTION 指令有2个参数,它表示 Python 需要在堆栈顶部弹出两个位置参数; 然后函数将在它上面进行调用,并且它也同时被弹出(关键字参数的函数,使用指令-CALL_FUNCTION_KW-类似的操作,并配合使用第三条指令CALL_FUNCTION_EX,它适用于函数调用涉及到参数使用 * 或 ** 操作符的情况)

一旦 Python 具备了这些,它将在调用堆栈上分配一个新的帧,填充到函数调用的本地变量,然后运行该帧内的 my_function 的字节码。一旦运行完成,帧将从调用堆栈中弹出,在原始帧中,my_function 的返回值将被推入到计算栈的顶部。

我们知道了这个东西了,也知道字节码了文件了,但是如何去使用字节码呢?ok不知道也没关系,接下来的时间我们所有的话题都将围绕字节码,在python有一个模块可以通过反编译Python代码来生成字节码这个模块就是今天要说的--dis模块。

dis模块的使用

dis模块包括一些用于处理 Python 字节码的函数,可以将字节码“反汇编”为更便于人阅读的形式。查看解释器运行的字节码还有助于优化代码。这个模块对于查找多线程中的竞态条件也很有用,因为可以用它评估代码中哪一点线程控制可能切换。参考源码Include/opcode.h,可以找到字节码的正式列表。详细可以看官方文档。注意不同版本的python生成的字节码内容可能不一样,这里我用的Python 3.8.

访问和理解字节码

输入如下内容,然后运行它:

def hello()
print("Hello, World!")
import dis
dis.dis(hello)

函数 dis.dis() 将反汇编一个函数、方法、类、模块、编译过的 Python 代码对象、或者字符串包含的源代码,以及显示出一个人类可读的版本。dis 模块中另一个方便的功能是 distb()。你可以给它传递一个 Python 追溯对象,或者在发生预期外情况时调用它,然后它将在发生预期外情况时反汇编调用栈上最顶端的函数,并显示它的字节码,以及插入一个指向到引发意外情况的指令的指针。

它也可以用于查看 Python 为每个函数构建的编译后的代码对象,因为运行一个函数将会用到这些代码对象的属性。这里有一个查看 hello() 函数的示例:

>>> hello.__code__
<code object hello at 0x104e46930, file "<stdin>", line 1>
>>> hello.__code__.co_consts
(None, 'Hello, World!')
>>> hello.__code__.co_varnames
()
>>> hello.__code__.co_names
('print',)

代码对象在函数中可以以属性 _code_ 来访问,并且携带了一些重要的属性:

co_consts 是存在于函数体内的任意实数的元组

co_varnames 是函数体内使用的包含任意本地变量名字的元组

co_names 是在函数体内引用的任意非本地名字的元组

许多字节码指令--尤其是那些推入到栈中的加载值,或者在变量和属性中的存储值--在这些元组中的索引作为它们参数。

因此,现在我们能够理解 hello() 函数中所列出的字节码:

1、LOAD_GLOBAL 0:告诉 Python 通过 co_names (它是 print 函数)的索引 0 上的名字去查找它指向的全局对象,然后将它推入到计算栈

2、LOAD_CONST 1:带入 co_consts 在索引 1 上的字面值,并将它推入(索引 0 上的字面值是 None,它表示在 co_consts 中,因为 Python 函数调用有一个隐式的返回值 None,如果没有显式的返回表达式,就返回这个隐式的值 )。

3、CALL_FUNCTION 1:告诉 Python 去调用一个函数;它需要从栈中弹出一个位置参数,然后,新的栈顶将被函数调用。

“原始的” 字节码--是非人类可读格式的字节--也可以在代码对象上作为 co_code 属性可用。如果你有兴趣尝试手工反汇编一个函数时,你可以从它们的十进制字节值中,使用列出 dis.opname 的方式去查看字节码指令的名字。

基本反汇编

函数dis()可以打印 Python 源代码(模块、类、方法、函数或代码对象)的反汇编表示。可以通过从命令行运行 dis 来反汇编 dis_simple.py 之类的模块。

dis_simple.py
#!/usr/bin/env python3
# encoding: utf-8
my_dict = {'a': 1}

输出按列组织,包含原始源代码行号,代码对象中的指令地址,操作码名称以及传递给操作码的任何参数。

对于简单的代码我们可以通过命令行的形式执行下面的命令:

python3 -m dis dis_simple.py

输出

  1           0 LOAD_CONST               0 ('a')
2 LOAD_CONST 1 (1)
4 BUILD_MAP 1
6 STORE_NAME 0 (my_dict)
8 LOAD_CONST 2 (None)
10 RETURN_VALUE

在这里源代码转换为4个不同的操作来创建和填充字典,然后将结果保存到一个局部变量。

首先解释每一行各列参数的含义:

以第一条指令为例:

第一列 数字(1)表示对应源代码的行数。

第二列(可选)指示当前执行的指令(例如,当字节码来自帧对象时)【这个例子没有】

第三列 一个标签,表示从之前的指令到此可能的JUMP 【这个例子没有】

第四列 数字是字节码中对应于字节索引的地址(这些是2的倍数,因为Python 3.6每条指令使用2个字节,而在以前的版本中可能会有所不同)指令LOAD_CONST在0位置。

第五列 指令本身对应的人类可读的名字这里是"LOAD_CONST"

第六列 Python内部用于获取某些常量或变量,管理堆栈,跳转到特定指令等的指令的参数(如果有的话)。

第七列 计算后的实际参数。

然后让我们看看这个过程:

由于 Python 解释器是基于栈的,所以前几步是用LOAD_CONST将常量按正确顺序放入到栈中,然后使用 BUILD_MAP 弹出要增加到字典的新键和值。用 STORE_NAME 将所得到的dict对象绑定名为my_dict.

反汇编函数

需要注意的是上面的命令行反编译的形式,不能自动的递归反编译函数,所以我们要使用在文件中导入dis的模式进行反编译,就像下面这样。

#dis_function.py
def f(*args):
nargs = len(args)
print(nargs, args) if __name__ == '__main__':
import dis
dis.dis(f)

运行命令

python3 dis_function.py

然后得到以下结果

  2           0 LOAD_GLOBAL              0 (len)
2 LOAD_FAST 0 (args)
4 CALL_FUNCTION 1
6 STORE_FAST 1 (nargs) 3 8 LOAD_GLOBAL 1 (print)
10 LOAD_FAST 1 (nargs)
12 LOAD_FAST 0 (args)
14 CALL_FUNCTION 2
16 POP_TOP
18 LOAD_CONST 0 (None)
20 RETURN_VALUE

要查看函数的内部,必须把函数传递到dis().因为这里打印的是函数内部的东西,所以没有显示函数的在外层的行编号,而是从2开始的。

下面解析下每一行指令的含义:

1、LOAD_GLOBAL 用来加载全局变量,包括指定函数名,类名,模块名等全局符号,这里是len函数,LOAD_FAST 一般加载局部变量的值,也就是读取值,用于计算或者函数调用传参等,这里就是传入参数args。

2、一般是先指定要调用的函数,然后压参数,最后通过 CALL_FUNCTION 调用。

3、STORE_FAST 保存值到局部变量。也就是把结果赋值给 STORE_FAST。

4、下面的print因为2个参数所以LOAD_FAST了2次,POP_TOP删除堆栈顶部(TOS)项。LOAD_CONST加载const变量,比如数值、字符串等等,这里因为是print所以值为None。

5、最后通过RETURN_VALUE来确定函数结尾。

要打印一个函数的总结信息我们可以使用dis的show_code的方法,它包含使用的参数和名的相关信息,show_code的参数就是这个函数对象,代码如下:

def f(*args):
nargs = len(args)
print(nargs, args) if __name__ == '__main__':
import dis
dis.show_code(f)

运行之后,结果如下

Name:              f
Filename: dis_function_showcode.py
Argument count: 0
Kw-only arguments: 0
Number of locals: 2
Stack size: 3
Flags: OPTIMIZED, NEWLOCALS, VARARGS, NOFREE
Constants:
0: None
Names:
0: len
1: print
Variable names:
0: args
1: nargs

可以看到返回的内容有函数,方法,参数等信息。

反汇编类

上面我们知道了如何反汇编一个函数的内部,同样的我们也可以用类似的方法反汇编一个类。

我们看一个例子:

import dis

class MyObject:
"""Example for dis.""" CLASS_ATTRIBUTE = 'some value' def __str__(self):
return 'MyObject({})'.format(self.name) def __init__(self, name):
self.name = name if __name__ == '__main__':
dis.dis(MyObject)

运行之和得到如下结果

Disassembly of __init__:
12 0 LOAD_FAST 1 (name)
2 LOAD_FAST 0 (self)
4 STORE_ATTR 0 (name)
6 LOAD_CONST 0 (None)
8 RETURN_VALUE Disassembly of __str__:
9 0 LOAD_CONST 1 ('MyObject({})')
2 LOAD_METHOD 0 (format)
4 LOAD_FAST 0 (self)
6 LOAD_ATTR 1 (name)
8 CALL_METHOD 1
10 RETURN_VALUE

从整体内容来看,结果分为了两部分Disassembly of _init__和Disassembly of _str,Disassembly就是反汇编的意思。

首先分析__init__部分:

1、然后需要注意的一点是,方法是按照字母的顺序列出的,所以在部分,先看到name再看到self,但是他们都是 LOAD_FAST。

2、STORE_ATTR实现self.name = name。

3、然后LOAD_CONST一个None和RETURN_VALUE标志着函数结束。

接下来分析__str__部分:

1、LOAD_CONST将'MyObject({})'加载到栈

2、然后通过 LOAD_METHOD 调用字符串format方法。这个方法是Python3.7新加入的。

3、LOAD_FAST 也就是到了self了。

4、LOAD_ATTR 一般是调用某个对象的方法时。这里就是self.name的.name操作

5、CALL_METHOD 是 python3.7 新增加的内容,这里是执行方法。

6、RETURN_VALUE表示函数的结束。

上面字符串的拼接我们用了format,之前我一直推荐用f-string,下面就让我们通过字节码来分析,为什么f-string比format要高快。

代码其他代码不变,把return改成以下内容:

return f'MyObject({self.name})'

再次执行,下面我们只看__str__函数的部分。

Disassembly of __str__:
9 0 LOAD_CONST 1 ('MyObject(')
2 LOAD_FAST 0 (self)
4 LOAD_ATTR 0 (name)
6 FORMAT_VALUE 0
8 LOAD_CONST 2 (')')
10 BUILD_STRING 3
12 RETURN_VALUE
```对比发现我们这里没有了调用方法的操作LOAD_METHOD,取而代之使用了用于实现fstring的FORMAT_VALUE指令。之后通过BUILD_STRING连接堆栈中的计数字符串并将结果字符串推入堆栈.为什么format慢呢, python中的函数调用具有相当大的开销。 当使用str.format()时,CALL_METHOD 中花费的额外时间是导致str.format()比fstring慢得多。
### 使用反汇编调试
调试一个异常时,有时要查看哪个字节码带来了问题。这个时候就很有用了,要对一个错误周围的代码反汇编,有多种方法。第一种策略是在交互解释器中使用dis()报告最后一个异常。
如果没有向dis()传入任何参数,那么它会查找一个异常,并显示导致这个异常的栈顶元素的反汇编效果。
####命令行上使用
打开我的命令行执行如下操作:

chennan@chennandeMacBook-Pro-2  ~  python3

Python 3.8.0a3 (v3.8.0a3:9a448855b5, Mar 25 2019, 17:05:20)

[Clang 6.0 (clang-600.0.57)] on darwin

Type "help", "copyright", "credits" or "license" for more information.

import dis

j = 4

i = i + 4

Traceback (most recent call last):

File "", line 1, in

NameError: name 'i' is not defined

dis.dis()

1 --> 0 LOAD_NAME 0 (i)

2 LOAD_CONST 0 (4)

4 BINARY_ADD

6 STORE_NAME 0 (i)

8 LOAD_CONST 1 (None)

10 RETURN_VALUE

行号后面的-->就是导致错误的操作码,一个LOAD_NAME指令,由于没有定义变量i,所以无法将与这个名关联的值加载到栈中。
####代码中使用distb
程序还可以打印一个活动的traceback的有关信息,将它传递到distb()方法。 下面的程序中有个DiviedByZero异常;但是这个公式有两个除法,所以不清楚是哪一部分出错,此时我们就可以使用下面的方法:
dis_traceback.py

i = 1

j = 0

k = 3

try:

result = k * (i / j) + (i / k)

except Exception:

import dis

import sys

exc_type, exc_value, exc_tb = sys.exc_info()

dis.distb(exc_tb)

运行之后输出

1 0 LOAD_CONST 0 (1)

2 STORE_NAME 0 (i)

2 4 LOAD_CONST 1 (0)

6 STORE_NAME 1 (j)

3 8 LOAD_CONST 2 (3)

10 STORE_NAME 2 (k)

5 12 SETUP_FINALLY 24 (to 38)

6 14 LOAD_NAME 2 (k)

16 LOAD_NAME 0 (i)

18 LOAD_NAME 1 (j)

--> 20 BINARY_TRUE_DIVIDE

22 BINARY_MULTIPLY

24 LOAD_NAME 0 (i)

26 LOAD_NAME 2 (k)

28 BINARY_TRUE_DIVIDE

...

>> 96 END_FINALLY

>> 98 LOAD_CONST 3 (None)

100 RETURN_VALUE

结果反映的字节码很长我们不用全看了,看最开始出现--> 就可以知道错误的位置了。
其中SETUP_FINALLY 字节码的含义是将try块从try-except子句推入块堆栈。
这里可以看出将LOAD_NAME 将j压入栈之后就报错了。所以可以推断出在(i/j)就出错了。 今天的内容就到这吧,更多精彩内容请关注公众号:python学习开发。 ### 参考资料
https://docs.python.org/zh-cn/3.7/library/dis.html#opcode-STORE_FAST
https://opensource.com/article/18/4/introduction-python-bytecode
https://hackernoon.com/a-closer-look-at-how-python-f-strings-work-f197736b3bdb

python反编译之字节码的更多相关文章

  1. Python 文件编译为字节码的方法

    一般情况下 python 不需要手动编译字节码.但是如果不想直接 release 源代码给其他人,将文件编译成字节码,可以实现一定程度的信息隐藏. 1) 使用模块 py_compile 编译一个单文件 ...

  2. @使用javap反编译Java字节码文件

    在Sun公司提供的JDK中,就已经内置了Java字节码文件反编译工具javap.exe(位于JDK安装目录的bin文件夹下). 我们可以在dos窗口中使用javap来反汇编指定的Java字节码文件.在 ...

  3. 【synchronized锁】通过synchronized锁 反编译查看字节码指令分析synchronized关键字修饰方法与代码块的区别

    前提: 首先要铺垫几个前置的知识: Java中的锁如sychronize锁是对象锁,Java对象头中具有标识位,当对象锁升级为重量级锁时,重量级锁的标识位会指向监视器monitor, 而每个Java对 ...

  4. python反编译工具

    开发类在线工具:https://tool.lu/一个反编译网站:https://tool.lu/pyc/ 一看这个标题,就是搞坏事用的, 用 java 写程序多了,很习惯用反编译工具了,而且玩java ...

  5. java编译后字节码解析

    java编译后字节码解析 参考网摘: https://my.oschina.net/indestiny/blog/194260

  6. Android反编译(一)之反编译JAVA源码

    Android反编译(一) 之反编译JAVA源码 [目录] 1.工具 2.反编译步骤 3.实例 4.装X技巧 1.工具 1).dex反编译JAR工具  dex2jar   http://code.go ...

  7. 检测微信小程序是否被反编译获取源码

    众所周知,微信小程序的代码安全性很弱,很容易被别人反编译获取源码.我自己的小程序也被别人反编译拿到源码还上线了,非常无语. 既然客户端不好防范,服务端还是可以做点手脚的. 小程序的Referer是不可 ...

  8. Android反编译调试源码

    Android反编译调试源码 1. 反编译得到源码 直接在windows 命令行下输入命令java -jar apktool_2.0.0.jar d -d 小米运动_1.4.641_1058.apk ...

  9. VS反编译查看源码时,会把类实现的所有接口都直接显示

    今天在看ArrayList,发现一个很有意思的问题.从VS里反编译看,ArrayList继承了ICollection. IEnumerable.IList和ICloneable,而IList又继承了I ...

随机推荐

  1. 【Xcode学C-4】进制知识、位运算符、变量存储细节以及指针的知识点介绍

    一.进制知识 (1)默认是十进制.八进制前面加0.即int num1=015;是13.十六进制前面加0x/0X.即int num1=0xd.结果是13.二进制前面是0b/0B,即int num1=0b ...

  2. (转)php 根据url自动生成缩略图并处理高并发问题

    分享是一种精神,与技术高低无关!   图片缩略图动态生成- [代码编程] 2011-08-23 版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明http://www.blogbus.c ...

  3. ajax实时获取下拉数据

    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> ajax ...

  4. CALL FUNCTION 'BAPI_GOODSMVT_CREATE'-(物料凭证创建)

    *&---------------------------------------------------------------------* *& Report  YTST_RAI ...

  5. Spring IOC 容器源码分析(转)

    原文地址 Spring 最重要的概念是 IOC 和 AOP,本篇文章其实就是要带领大家来分析下 Spring 的 IOC 容器.既然大家平时都要用到 Spring,怎么可以不好好了解 Spring 呢 ...

  6. Java for LeetCode 113 Path Sum II

    Given a binary tree and a sum, find all root-to-leaf paths where each path's sum equals the given su ...

  7. shell将字符串转换为大写变量并将小写作为变量值

    group_name='a b c d e f g' for a in $group_name; do typeset -u a ; echo "$a='$(echo $a | tr '[A ...

  8. (1)Java多线程编程核心——Java多线程技能

    1.为什么要使用多线程?多线程的优点? 提高CPU的利用率 2.什么是多线程? 3.Java实现多线程编程的两种方式? a.继承Thread类 public class MyThread01 exte ...

  9. listen and translation exercise 53

    It was hard work and there weren't any interesting things for him. You should be an expert with comp ...

  10. OpenCV——Skewing

    // define head function #ifndef PS_ALGORITHM_H_INCLUDED #define PS_ALGORITHM_H_INCLUDED #include < ...