初探Python字节码和dis模块

本文主要介绍 Python 字节码、Python 虚拟机内幕以及 dis 模块的简单应用。阅读本文预计 10 min.

1. 前言

了解 Python 字节码和 Python 虚拟机运行的知识,学会使用 dis 模块分析 Python 字节码,对于我们软件调试、漏洞分析等都非常有用。

它也可以帮助我们分析为什么这么写会更加高效,让我们更了解我们程序的运行过程,从而写出高效简洁的代码。

此外,这也可以帮助我们更好的回答一些 Python 问题,还可以在这个过程中理解面向栈的编程模型是如何工作的,开拓编程视野。

本文主要内容:

  • Python 字节码
  • Python 虚拟机内幕简述
  • dis 模块简单应用
  • 学习资源整理

2. Python 字节码

在了解什么是 Python 字节码之前,我们先学习两个概念:汇编(Assembly)和反汇编(Disassembly)。

2.1 汇编与反汇编

汇编(Assembly):计算机只能执行 010101…这样的二进制代码,即机器语言,汇编是指把汇编语言转换为机器语言。
反汇编(Disassembly):汇编的反义词,把机器码(二进制代码)转为汇编代码的过程,也可以说是把机器语言转换为汇编语言代码、低级转高级的意思(via 百度百科)。

关于汇编语言入门的知识,我强烈推荐阮一峰老师的汇编语言入门教程,这篇博文写的非常通俗易懂,很实用!
此外有需要还可以看看:

  • 《计算机是怎样跑起来的》
  • 《程序是怎样跑起来的》
  • B 站的《计算机科学速成课》

以上 3 个初步了解计算机和程序非常不错,如果要深入学习汇编语言,可以看看王爽老师的《汇编语言》。以上我都看过,觉得很不错,推荐给大家。如果大家需要前两本书的电子版可以关注我公众号,联系我。

2.2 什么是 Python 字节码呢?

字节码(Bytecode):通常指的是已经经过编译,但与特定机器代码无关,需要解释器转译后才能成为机器代码的中间代码。字节码通常不像源码一样可以让人阅读,而是编码后的数值常量、引用、指令(也称操作码,Operation Code)等构成的序列。(Via wiki)

拿 Python 说明,Python 解释器先翻译 Python 源代码( .py 文件)为 Python 字节码( .pyc 文件),然后再由 Python 虚拟机来执行 Python 字节码。Python 字节码是一种类似于汇编指令的中间语言,一条 Python 语句会对应若干条字节码指令,虚拟机一条条执行字节码指令,将其翻译成机器代码,并交个 CPU 执行,从而完成程序的执行。

在 Python 3 中,Python 会自动在 __pycache__ 目录里,缓存每个模块编译后的版本,名称为 module.version.pyc ,这就是 Python 字节码文件。其中 version 一般使用 Python 版本号。例如,在 CPython 版本 3.7 中,spam.py 模块的编译版本将被缓存为 __pycache__/spam.cpython-37.pyc。此命名约定允许来自不同发行版和不同版本的 Python 的已编译模块共存。简单说就是一个源文件,可以存在多个版本的 Python 字节码,如:

hello.cpython-38.pyc
hello.cpython-37.pyc

__pycache__ 目录下,同时存在 hello.py 模块的两个字节码版本,一个是 Python 3.7 编译的,一个是 Python 3.8 编译的。

2.3 为什么需要 Python 字节码?

我们为什么设计出来 Python 字节码?Python 字节码有什么好处呢?

字节码主要为了实现特定软件运行和软件环境、与硬件环境无关。字节码的实现方式是通过编译器和虚拟机。编译器将源码编译成字节码,特定平台上的虚拟机将字节码转译为可以直接运行的指令。字节码的典型应用为 Java bytecode。(Via wiki)

Python 字节码的好处:

  1. 提升可移植性。其实设计字节码是为了实现跨平台运行代码,也就是具备可移植性。有了 Python 虚拟机,我们就可以在不同的操作系统平台运行同一个源代码,因为字节码会被 Python 虚拟机根据不同的操作系统平台翻译成相应的机器语言,从而执行。也就是说,我们有了 Python 虚拟机这个翻译官,只需要安心写代码,至于把我们的代码转化为二进制代码,就交给翻译官虚拟机去做就可以了。

  2. 提升代码的加载速度。有些教程说提升运行速度,这个说法其实不算准确。Python 源代码(.py文件)和 Python 字节码的执行速度其实是一样的,它是快在省略了源代码的解析翻译过程,最后的交给 CPU 执行阶段所花的时间是一样的。

Python 通过检查源文件的修改日期,来确定源文件对应的 Python 字节码文件是否已过期,如果过期就会重新翻译解析,并更新相应的 Python 字节码文件。这是一个完全自动化的过程。

Python 在两种情况下不会检查 Python 字节码文件。首先,对于从命令行直接载入的模块(.py源文件),它从来都是重新编译并且不存储编译结果;其次,如果没有源模块,它不会检查缓存(指 Python 字节码文件)。为了支持无源文件(仅编译)发行版本, 编译模块必须是在源目录下,并且绝对不能有源模块。

Python tutorial 官网给专业人士的一些小建议:

  1. 可以在 Python 命令中使用 -O 或者 -OO 开关, 以减小编译后 Python 字节码文件的大小。 -O 开关去除断言语句,-OO 开关同时去除断言语句和 __doc__ 字符串。由于有些程序可能依赖于这些,你应当只在清楚自己在做什么时才使用这个选项。“优化过的”模块有一个 opt- 标签,并且通常小些。将来的发行版本或许会更改优化的效果。
  2. 引入.pyc 文件的目的是为了加快载入速度,并不会影响执行速度,.pyc 文件和 源文件执行的速度是一样的。
  3. compileall 模块可以为一个目录下的所有模块创建 .pyc 文件。
  4. 更多的细节可以参看,PEP 3147

3. Python 虚拟机内幕

CPython,即我们通常使用的 Python 版本,使用一个基于栈(Stack)的虚拟机。也就是说,它完全面向栈数据结构的,栈是后进先出的数据结构。

CPython 使用三种类型的栈:

  1. 调用栈(Call stack)。这是运行 Python 程序的主要结构。每个当前活动函数调用都有一个叫 帧(Frame) 的东西,栈底是程序的入口点。每次函数调用都会推送一个新的帧到调用栈,当函数调用返回后,这个帧就会被销毁。其实就是函数调用一层一层压入调用栈,随着函数返回,再一层层把相应的帧给释放。
  2. 在每个帧中,有一个 计算栈(Evaluation stack),也称为 数据栈(data stack),这个栈就是 Python 函数运行的地方,运行的 Python 代码大多数是由推入到这个栈中的东西组成的,操作它们,最后在返回结果后,销毁它们。
  3. 在每个帧中,还有一个 块栈(block stack)。它被 Python 用于去跟踪某些类型的控制结构:循环、try/except 块、以及 with 块,这些全部推入到块栈中,当退出这些控制结构时,块栈会被销毁。这将帮助 Python 了解任意给定时刻哪个块是活动的,比如,一个 continue 或者 break 语句可能影响正确的块。

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

为了更好地理解,假设我们有一些调用函数的代码,比如这个:my_function(my_variable, 2)。用 dis 模块将 Python 代码将转换为一系列字节码指令:

import dis

dis.dis('my_function(my_variable, 2)')

结果输出:
  1           0 LOAD_NAME                0 (my_function)
              2 LOAD_NAME                1 (my_variable)
              4 LOAD_CONST               0 (2)
              6 CALL_FUNCTION            2
              8 RETURN_VALUE

dis 模块待会介绍,这里先看看 Python 字节码指令:

  1. 第一个 LOAD_NAME 指令去查找函数对象 my_function,然后将它推入到计算栈的顶部
  2. 第二个 LOAD_NAME 指令去查找变量 my_variable,然后将它推入到计算栈的顶部
  3. 接着 LOAD_CONST 指令去推入一个实整数值 2 到计算栈的顶
  4. CALL_FUNCTION 指令调用函数,并且这个函数是有 2 个位置参数,它表示 Python 需要从栈顶弹出两个位置参数;然后函数将在它上面进行调用,并且它也会被弹出。此外,函数关键字参数使用 CALL_FUNCTION_KW 指令,可变参数和可变关键字参数使用( *** 操作符)使用 CALL_FUNCTION_EX 指令,不过使用的操作原则类似都是类似的。一旦 Python 拥有了这些之后,它将在调用栈上分配一个新帧,把函数调用的局部变量放进去,然后运行那个帧内的 my_function 字节码。
  5. RETURN_VALUE 返回值,运行完成后,这个帧将被调用栈销毁,而在最初的帧内,my_function 的返回值将被推入到计算栈的顶部。

4. dis 模块

Python

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值