位置: 编程技术 - 正文

从底层简析Python程序的执行过程(python的底层语言)

编辑:rootadmin

推荐整理分享从底层简析Python程序的执行过程(python的底层语言),希望有所帮助,仅作参考,欢迎阅读内容。

文章相关热门搜索词:python 底层语言,python底层运行原理,python底层运行原理,python底层源码,python主要用于底层开发,python底层数据结构,python底层源码,python底层源码,内容如对您有帮助,希望把文章链接给更多的朋友!

最近我在学习 Python 的运行模型。我对 Python 的一些内部机制很是好奇,比如 Python 是怎么实现类似 YIELDVALUE、YIELDFROM 这样的操作码的;对于 递推式构造列表(List Comprehensions)、生成器表达式(generator expressions)以及其他一些有趣的 Python 特性是怎么编译的;从字节码的层面来看,当异常抛出的时候都发生了什么事情。翻阅 CPython 的代码对于解答这些问题当然是很有帮助的,但我仍然觉得以这样的方式来做的话对于理解字节码的执行和堆栈的变化还是缺少点什么。GDB 是个好选择,但是我懒,而且只想使用一些比较高阶的接口写点 Python 代码来完成这件事。

所以呢,我的目标就是创建一个字节码级别的追踪 API,类似 sys.setrace 所提供的那样,但相对而言会有更好的粒度。这充分锻炼了我编写 Python 实现的 C 代码的编码能力。我们所需要的有如下几项,在这篇文章中所用的 Python 版本为 3.5。

一个新的 Cpython 解释器操作码 一种将操作码注入到 Python 字节码的方法 一些用于处理操作码的 Python 代码

一个新的 Cpython 操作码新操作码:DEBUG_OP

这个新的操作码 DEBUG_OP 是我第一次尝试写 CPython 实现的 C 代码,我将尽可能的让它保持简单。 我们想要达成的目的是,当我们的操作码被执行的时候我能有一种方式来调用一些 Python 代码。同时,我们也想能够追踪一些与执行上下文有关的数据。我们的操作码会把这些信息当作参数传递给我们的回调函数。通过操作码能辨识出的有用信息如下:

堆栈的内容 执行 DEBUG_OP 的帧对象信息

所以呢,我们的操作码需要做的事情是:

找到回调函数 创建一个包含堆栈内容的列表 调用回调函数,并将包含堆栈内容的列表和当前帧作为参数传递给它

听起来挺简单的,现在开始动手吧!声明:下面所有的解释说明和代码是经过了大量段错误调试之后总结得到的结论。首先要做的是给操作码定义一个名字和相应的值,因此我们需要在 Include/opcode.h中添加代码。

这部分工作就完成了,现在我们去编写操作码真正干活的代码。实现 DEBUG_OP

在考虑如何实现DEBUG_OP之前我们需要了解的是 DEBUG_OP 提供的接口将长什么样。 拥有一个可以调用其他代码的新操作码是相当酷眩的,但是究竟它将调用哪些代码捏?这个操作码如何找到回调函数的捏?我选择了一种最简单的方法:在帧的全局区域写死函数名。那么问题就变成了,我该怎么从字典中找到一个固定的 C 字符串?为了回答这个问题我们来看看在 Python 的 main loop 中使用到的和上下文管理相关的标识符 enter 和 exit。

我们可以看到这两标识符被使用在操作码 SETUP_WITH 中:

现在,看一眼宏 _Py_IDENTIFIER 定义

嗯,注释部分已经说明得很清楚了。通过一番查找,我们发现了可以用来从字典找固定字符串的函数 _PyDict_GetItemId,所以我们操作码的查找部分的代码就是长这样滴。

为了方便理解,对这一段代码做一些说明:

f 是当前的帧,f->f_globals 是它的全局区域 如果我们没有找到 op_target,我们将会检查这个异常是不是 KeyError goto error; 是一种在 main loop 中抛出异常的方法 PyErr_Clear() 抑制了当前异常的抛出,而 DISPATCH() 触发了下一个操作码的执行

下一步就是收集我们想要的堆栈信息。

最后一步就是调用我们的回调函数!我们用 call_function 来搞定这件事,我们通过研究操作码 CALL_FUNCTION 的实现来学习怎么使用 call_function 。

有了上面这些信息,我们终于可以捣鼓出一个操作码DEBUG_OP的草稿了:

在编写 CPython 实现的 C 代码方面我确实没有什么经验,有可能我漏掉了些细节。如果您有什么建议还请您纠正,我期待您的反馈。

编译它,成了!

一切看起来很顺利,但是当我们尝试去使用我们定义的操作码 DEBUG_OP 的时候却失败了。自从 年之后,Python 使用预先写好的 goto(你也可以从 这里获取更多的讯息)。故,我们需要更新下 goto jump table,我们在 Python/opcode_targets.h 中做如下修改。

这就完事了,我们现在就有了一个可以工作的新操作码。唯一的问题就是这货虽然存在,但是没有被人调用过。接下来,我们将DEBUG_OP注入到函数的字节码中。在 Python 字节码中注入操作码 DEBUG_OP

有很多方式可以在 Python 字节码中注入新的操作码:

使用 peephole optimizer, Quarkslab就是这么干的 在生成字节码的代码中动些手脚 在运行时直接修改函数的字节码(这就是我们将要干的事儿)

为了创造出一个新操作码,有了上面的那一堆 C 代码就够了。现在让我们回到原点,开始理解奇怪甚至神奇的 Python!

我们将要做的事儿有:

得到我们想要追踪函数的 code object 重写字节码来注入 DEBUG_OP 将新生成的 code object 替换回去

和 code object 有关的小贴士

如果你从没听说过 code object,这里有一个简单的介绍网路上也有一些相关的文档可供查阅,可以直接 Ctrl+F 查找 code object

还有一件事情需要注意的是在这篇文章所指的环境中 code object 是不可变的:

但是不用担心,我们将会找到方法绕过这个问题的。使用的工具

为了修改字节码我们需要一些工具:

dis模块用来反编译和分析字节码 dis.BytecodePython 3.4新增的一个特性,对于反编译和分析字节码特别有用 一个能够简单修改 code object 的方法

用 dis.Bytecode 反编译 code object 能告诉我们一些有关操作码、参数和上下文的信息。

为了能够修改 code object,我定义了一个很小的类用来复制 code object,同时能够按我们的需求修改相应的值,然后重新生成一个新的 code object。

这个类用起来很方便,解决了上面提到的 code object 不可变的问题。

测试我们的新操作码

我们现在拥有了注入 DEBUG_OP 的所有工具,让我们来验证下我们的实现是否可用。我们将我们的操作码注入到一个最简单的函数中:

看起来它成功了!有一行代码需要说明一下 new_nop_code.co_stacksize += 3

co_stacksize 表示 code object 所需要的堆栈的大小 操作码DEBUG_OP往堆栈中增加了三项,所以我们需要为这些增加的项预留些空间从底层简析Python程序的执行过程(python的底层语言)

现在我们可以将我们的操作码注入到每一个 Python 函数中了!重写字节码

正如我们在上面的例子中所看到的那样,重写 Pyhton 的字节码似乎 so easy。为了在每一个操作码之间注入我们的操作码,我们需要获取每一个操作码的偏移量,然后将我们的操作码注入到这些位置上(把我们操作码注入到参数上是有坏处大大滴)。这些偏移量也很容易获取,使用 dis.Bytecode,就像这样。

基于上面的例子,有人可能会想我们的 insert_op_debug 会在指定的偏移量增加一个"x",这尼玛是个坑啊!我们第一个 DEBUG_OP 注入的例子中被注入的函数是没有任何的分支的,为了能够实现完美一个函数注入函数 insert_op_debug 我们需要考虑到存在分支操作码的情况。

Python 的分支一共有两种:

(1) 绝对分支:看起来是类似这样子的 Instruction_Pointer = argument(instruction)

(2)相对分支:看起来是类似这样子的 Instruction_Pointer += argument(instruction)

相对分支总是向前的

我们希望这些分支在我们插入操作码之后仍然能够正常工作,为此我们需要修改一些指令参数。以下是其逻辑流程:

(1) 对于每一个在插入偏移量之前的相对分支而言

如果目标地址是严格大于我们的插入偏移量的话,将指令参数增加 1

如果相等,则不需要增加 1 就能够在跳转操作和目标地址之间执行我们的操作码DEBUG_OP

如果小于,插入我们的操作码的话并不会影响到跳转操作和目标地址之间的距离

(2) 对于 code object 中的每一个绝对分支而言

如果目标地址是严格大于我们的插入偏移量的话,将指令参数增加 1

如果相等,那么不需要任何修改,理由和相对分支部分是一样的

如果小于,插入我们的操作码的话并不会影响到跳转操作和目标地址之间的距离

下面是实现:

让我们看一下效果如何:

甚好!现在我们知道了如何获取堆栈信息和 Python 中每一个操作对应的帧信息。上面结果所展示的结果目前而言并不是很实用。在最后一部分中让我们对注入做进一步的封装。增加 Python 封装

正如您所见到的,所有的底层接口都是好用的。我们最后要做的一件事是让 op_target 更加方便使用(这部分相对而言比较空泛一些,毕竟在我看来这不是整个项目中最有趣的部分)。

首先我们来看一下帧的参数所能提供的信息,如下所示:

f_code当前帧将执行的 code object f_lasti当前的操作(code object 中的字节码字符串的索引)

经过我们的处理我们可以得知 DEBUG_OP 之后要被执行的操作码,这对我们聚合数据并展示是相当有用的。

新建一个用于追踪函数内部机制的类:

改变函数自身的 co_code 设置回调函数作为 op_debug 的目标函数

一旦我们知道下一个操作,我们就可以分析它并修改它的参数。举例来说我们可以增加一个 auto-follow-called-functions 的特性。

现在我们实现一个 Trace 的子类,在这个子类中增加 callback 和 doreport 这两个方法。callback 方法将在每一个操作之后被调用。doreport 方法将我们收集到的信息打印出来。

这是一个伪函数追踪器实现:

这里有一些实现的例子和使用方法。格式有些不方便观看,毕竟我并不擅长于搞这种对用户友好的报告的事儿。

例1自动追踪堆栈信息和已经执行的指令 例2上下文管理

递推式构造列表(List Comprehensions)的追踪示例。

例3伪追踪器的输出 例4输出收集的堆栈信息

总结

这个小项目是一个了解 Python 底层的良好途径,包括解释器的 main loop,Python 实现的 C 代码编程、Python 字节码。通过这个小工具我们可以看到 Python 一些有趣构造函数的字节码行为,例如生成器、上下文管理和递推式构造列表。

这里是这个小项目的完整代码。更进一步的,我们还可以做的是修改我们所追踪的函数的堆栈。我虽然不确定这个是否有用,但是可以肯定是这一过程是相当有趣的。

MySQL中ADDDATE()函数的使用教程 ADDDATE(date,INTERVALexprunit),ADDDATE(expr,days)当被调用的第二个参数的区间形式,ADDDATE()是DATE_ADD()的同义词。相关功能SUBDATE()是DATE_SUB()的代名词。对于区间上

MySQL查询倒数第二条记录实现方法 有时候会用到查询倒数第二条记录last=HolderChangeHistory.find_by_sql(["SELECT*FROMholder_change_historieswheretreasure_id=orderbyiddesclimit1,1",@hch.treasure_id])select*fromtablenameawhe

新建一个MySQL数据库的简单教程 使用mysqladmin创建数据库:需要特殊的权限才能创建或删除一个MySQL数据库。因此,假设以root用户的访问,可以创建任何数据库使用mysqlmysqladmin的二进制

标签: python的底层语言

本文链接地址:https://www.jiuchutong.com/biancheng/347603.html 转载请保留说明!

上一篇:防止服务器宕机时MySQL数据丢失的几种方案(防止服务器断电)

下一篇:MySQL查询倒数第二条记录实现方法(mysql查询倒数第二个字母为a)

  • 国税如何零申报
  • 城镇土地使用税的征税范围
  • 法人买社保不领钱可以吗
  • 企业稳岗返还申请报告模板
  • 厂区打地坪会计怎么做账
  • 城市维护建设税免税
  • 资源税有哪些减免规定
  • 企业分立特殊性税务处理涉税
  • 政府给的专款专用发票
  • 现金账冲账怎么冲
  • 企业购买房屋的行为涉及哪些税种
  • 给分公司开发票由总公司付款怎么办?
  • 转让部分股份流程
  • 支付给职工和为职工支付的现金
  • 简易计税方法是什么意思
  • 纳税人财务核算及申报纳税情况报告
  • 金税盘和税控盘哪个好
  • 土地的增值税进项能否抵扣
  • 企业所得税中准予扣除的损失
  • 上月预付款项 这月开了发票会计分录
  • 金融企业的代理贷款什么意思
  • 抵扣联复印件可以做账吗
  • 投资公司如何确定收入
  • 软件技术开发是什么
  • 进口货物账务处理外币
  • Win10中macos big sur虚拟机无法连接网络怎么办?
  • 一般纳税人问题
  • 在建工程的概念
  • 华为mate x3最新价格
  • web投票系统源码
  • 公司主要开支是指什么
  • 生产型出口企业免抵退
  • 银行日记账本月没有发生业务
  • 企业之间可以背书吗
  • 完美解决索尼电视arc无声音
  • 所有者权益变动表范本
  • nodejs安装及环境配置 centos
  • 自动化测试三年规划
  • react组件constructor
  • 增值税普通发票和专用发票有什么区别
  • python2打包
  • 公司加班餐费会计科目
  • 个税可以补申报几个月的码
  • 与上级往来的会计科目的题目
  • 持有至到期投资属于什么科目
  • mongodb 全文索引
  • 教大家8天学通MongoDB——第一天 基础入门篇
  • 电子商业汇票是指
  • 虚开普票的立案标准
  • 印花税的会计处理方法
  • 加计抵减进项税额怎么计算
  • 上期金额是属于负债吗
  • 将资本公积转为资本会计科目
  • 全额计提坏账准备有什么作用
  • 房地产开发的土地分割
  • 建筑施工企业会计第三版单旭课后题答案
  • 税控系统全额抵扣增值税申报
  • 劳务派遣公司小规模纳税人如何开票
  • 高新技术产品收入归集依据
  • 出口退税转为免税做账处理
  • 评估入账的无形资产能摊销吗
  • 如何设置银行存款日记账
  • mysql分页效率
  • WINDOWS操作系统属于单用户任务操作系统
  • 苹果电脑mac系统键盘无法找到
  • 苹果系统如何访问u盘
  • linux使用cp
  • win10如何添加蓝牙驱动
  • WIN10系统中没有接入音频设备 要启动gui
  • unity3d读取gis数据
  • css中dl
  • 怎样用div css制作网页
  • nodejs.
  • 关于减肥的好方法
  • js鼠标滑动特效
  • 176是哪的号码
  • 养鱼业免增值税吗
  • 契税含不含精装修
  • 开地税发票的操作流程
  • 2020年个税截止时间
  • 免责声明:网站部分图片文字素材来源于网络,如有侵权,请及时告知,我们会第一时间删除,谢谢! 邮箱:opceo@qq.com

    鄂ICP备2023003026号

    网站地图: 企业信息 工商信息 财税知识 网络常识 编程技术

    友情链接: 武汉网站建设