起因: 下面这段奇怪的 python 代码,一个奇怪的 bug,简单来说就是在一个模块内定义了一个 class Test, 然后创建了一个 Test 的对象 ,然后在一个函数内通过 from import 方式从外部 import 了 Test这个类,然后就发现 a 不是从外部import这个Test类的实例。于是就想看下 cpython 是怎样处理这块儿逻辑的。一直不喜欢 python ,好多奇怪的语法,都需要从cpython源码上去发现。

import inspect
import sys
import pdb from A import A
from B import test_a class Test(object):
print 'TEST EXE'
def echo(self):
print 'hello'
pass def makeTest():
# from test import Test
t = Test()
print 'makeTest'
print inspect.getmodule(t), t.__module__, id(Test)
checkTest(t)
return t def checkTest(t):
from Test import Test
def echo_hot(self):
print 'hot'
Test.echo = echo_hot
print 'checkTest'
pdb.set_trace()
print isinstance(t, Test), inspect.getmodule(t), Test.__module__, id(Test)
t.echo() if __name__ == '__main__':
print 'A id from main: ', id(A)
makeTest()

  

cpython源码调试

为了跟踪特定 python 写法对应到 cpython 的逻辑,我们需要在运行 python 代码的时候,能够跟踪到cpython源码层的各个细节,变量等等,也就能找到python一些奇怪语法对应的实现原理。

通过 python dis 模块,我们可以很容易得到一份python代码对应的字节码:

比如,我们上边的那段代码的 isinstance 函数就是Python源码内处理内置函数 bltinmodule.c 文件内的一个函数。

找一份 cpython 源码,编译好,带调试符号的,然后我们可以轻松的在 builtin_isinstance 处打断点,似乎很友善,然而跑起来就发现会有无数的逻辑会跑到这个断点,基本都是cpython虚拟机内部的一些库啊,函数啊啥的,很难定位到我们上面那段代码执行的时间点。

因此,我们的问题就是如何在python代码运行的恰当时间点,在cpython源码触发断点。这个问题下有人给了解决方案

https://stackoverflow.com/questions/41160447/cant-enable-py-bt-for-gdb

方法是自己写一个 cpython 插件,然后在python代码中需要断点的地方调用自己写的函数,在插件的c代码处打断点,自己写的插件当然不会有其它地方触发,我们也在恰当的时间点中断cpython执行。当然我们有现成的工具pdb,pdb的 set_trace 可以实现上面回答中的cpython插件功能。联调的步骤也就变成了下面这样。

1. 在要调试的python代码位置,设置 pdb.set_trace()

2. 启动 gdb 加载带调试符号的 python 虚拟机

3. 在 gdb 内执行对应的 python 代码

4. 在 python 代码运行到 pdb trace 的时候会触发pdb中断

5. 这个时候另起个终端,给cpython发送个信号, pkill python -SIGTRAP

6. gdb 就会退回到调试窗口

7. 在 gdb 内设置断点

8. continue 回到 pdb

9. pdb 内用 n + enter 继续执行

就断到了 cpython 代码里,在这儿就可以用 gdb 看当前 python frame 的各种状态了。

所以上面的Test代码到底发生了啥

在上面的代码内我们定义了个 python class,叫做 Test,如果我们直接在python控制台 import 文件后通过 dis.dis 查看字节码看到的应该是这样的,并没有预想到的看到 BUILD_CLASS 字节码。

所以当我们 import python模块的时候, BUILD_CLASS 这个过程是如何触发的呢,这需要通过pyc文件dis才能看得到,具体可以参考:

然后就可以找到 BUILD_CLASS 和 STORE_NAME 相关的字节码

BUILD_CLASS 是创建一个类的过程,STORE_NAME 是把创建好的类对象存放到 f -> f_locals 作用域里

而当我们通过 run_pyc_file 运行一个 pyc 文件的时候,传入的和 globals 和 locals 相同

通过 gdb 看到的地址也印证了这一点

也就是说当我们通过 run_pyc_file 运行一个py module 的时候,定义的 class 会同时存放在 global 和 locals 里。关于 globals 和 locals,只有在调用函数的时候,globals 传的是 globals 地址,locals传的是NULL,这时候 globals 和后续的 locals 才发生分化。

当我们在 makeTest 构造一个Test的对象时,使用的字节码是,LOAD_GLOBAL 找到对应的类, 然后执行类的构造

而当我们在 checkTest 内通过 from ... import 的方式引入一个 Class 时首先会调 IMPORT_NAME 把 module/package 导入,这里是 Test 模块,然后会在 Test模块内查找Test类

IMPORT_NAME 的逻辑一开始还是不太好看懂的,只是知道最后会在 import.c 代码里完成 import ,于是在下面这个函数内加了个断点。

中断后,trace 就是下面这样

可以看出,IMPORT_NAME 是调用 PyEval_CallObject 然后调用 builtinimport来完成 package 导入的。

而在 PyImport_ExecCodeModuleEx 内,会根据 name 从 sys.modules 里找是否已经 load 了该模块,值得注意的是,这里边函数参数 name 是没有 sys.path 前缀的路径名字,这个例子里就是 ‘Test’,这个名字也就是 sys.modules 里的键值。

import 最后执行的就是在 module 空间内执行模块代码,module空间传入的globals和locals都是 sys.modules[..].md_dict 因此,modules里的module的 dict 里也就有了相关类的实例。

所以,例子中奇怪的表现就可以解释了,我是把 Test 作为 main module 的,因此执行时,Test类在 main module里实例化了,后面再 from .. import 时,module就不是 main了,因此Test 类在 sys.modules 里有了多份实例,当然多个 Class 实例的 id 是不同的,这个似乎可以理解,毕竟指针也不一样。我本以为isinstance会做些处理,虽然引用的路径不同,但终归是一份代码而且没有reload过,返回test对象是通过 from .. import 方式导入的Class的对象似乎合情合理,但结果还是有点诧异的。所以最后又看了下,Python内置函数isInstance是如何检测一个对象是否属于一个类。

最终 PyObject_IsInstance 会调用到 PyClass_IsSubclass,然后这里边是直接用 kclass 和 base 进行指针比较。。。也就是说同一个声明的多个Python类对象所定义的对象也属于不同的类,只要import的时候旧import的class已经不存在了,或者找不到了,或者找的方式不对。

其实我们可以简化出另一个测试例子:当sys.path 内有多条路径找到一个 class 时,而代码又通过不同的路径去 import 这些class时,sys.modules 就会缓存多个 class 对象,然后多个 class 实例化的对象也就所属不同的类。

目录结构

从不同路径导入 package

sys.modules里就有了不同的 package key,而A类就会在不同的类下有了实例。

虽然是同一份代码,对于习惯 C++,java之类的,everything is object 属实很坑。。。

总结

单调试 python 挺好调试的,单调试 cpython 也挺方便的,但是在python运行的某个时间点想看看cpython的各种状态要稍微麻烦点,需要cpython在特定的时间点中断给设置gdb breakpoint提供机会,pdb可以做,自己写的插件也可以处理。而关于python本身,没有系统学习过,所以会经常遇到自己感觉很奇怪的语法,也许只是自己不够pythonic吧,遇到这种问题不跟到源码始终感觉莫名其妙。

参考:

http://www.xumenger.com/01-python-pyc-20180521/

https://stackoverflow.com/questions/41160447/cant-enable-py-bt-for-gdb

https://medium.com/@skabbass1/how-to-step-through-the-cpython-interpreter-2337da8a47ba

https://wzt.ac.cn/2019/02/13/pyc-simple/

pdb 和 gdb 联调 python + cpython源码的更多相关文章

  1. 【转】python:让源码更安全之将py编译成so

    python:让源码更安全之将py编译成so 应用场景 Python是一种面向对象的解释型计算机程序设计语言,具有丰富和强大的库,使用其开发产品快速高效. python的解释特性是将py编译为独有的二 ...

  2. 《python解释器源码剖析》第0章--python的架构与编译python

    本系列是以陈儒先生的<python源码剖析>为学习素材,所记录的学习内容.不同的是陈儒先生的<python源码剖析>所剖析的是python2.5,本系列对应的是python3. ...

  3. 从 CPython 源码角度看 Python 垃圾回收机制

    环状双向链表 refchain 在 Python 程序中创建的任何对象都会被放到 refchain 链表中,当创建一个 Python 对象时,内部实际上创建了一些基本的数据: 上一个对象 下一个对象 ...

  4. python slots源码分析

    上次总结Python3的字典实现后的某一天,突然开窍Python的__slots__的实现应该也是类似,于是翻了翻CPython的源码,果然如此! 关于在自定义类里面添加__slots__的效果,网上 ...

  5. 如何编译和调试Python内核源码?

    目录 写在前面 获取源代码 源代码的组织 windows下编译CPython 调试CPython 小结 参考 博客:blog.shinelee.me | 博客园 | CSDN 写在前面 如果对Pyth ...

  6. 《python解释器源码剖析》第13章--python虚拟机中的类机制

    13.0 序 这一章我们就来看看python中类是怎么实现的,我们知道C不是一个面向对象语言,而python却是一个面向对象的语言,那么在python的底层,是如何使用C来支持python实现面向对象 ...

  7. 《python解释器源码剖析》第12章--python虚拟机中的函数机制

    12.0 序 函数是任何一门编程语言都具备的基本元素,它可以将多个动作组合起来,一个函数代表了一系列的动作.当然在调用函数时,会干什么来着.对,要在运行时栈中创建栈帧,用于函数的执行. 在python ...

  8. 《python解释器源码剖析》第7章--python中的set对象

    7.0 序 集合和字典一样,都是性能非常高效的数据结构,性能高效的原因就在于底层使用了哈希表.因此集合和字典的原理本质上是一样的,都是把值映射成索引,通过索引去查找. 7.1 PySetObject ...

  9. python:让源码更安全之将py编译成so

    应用场景 Python是一种面向对象的解释型计算机程序设计语言,具有丰富和强大的库,使用其开发产品快速高效. python的解释特性是将py编译为独有的二进制编码pyc文件,然后对pyc中的指令进行解 ...

  10. 『Python』源码解析_源码文件介绍

    本篇代码针对2.X版本,与3.X版本细节不尽相同,由于两者架构差别不大加之本人能力有限,所以就使用2.X体验python的底层原理了. 一.主要文件夹内容 Include :该目录下包含了Python ...

随机推荐

  1. 蓝桥杯训练赛二-1199 问题 E: 哥德巴赫曾猜测

    题目描述 德国数学家哥德巴赫曾猜测:任何大于6的偶数都可以分解成两个素数(素数对)的和.但有些偶数可以分解成多种素数对的和,如: 10=3+7,10=5+5,即10可以分解成两种不同的素数对 输入 输 ...

  2. nop调试-区域路由问题

    1.在修改nop代码时,web项目里有一个namecontroller,然后区域Areas里也有一个namecontroller, 然后跳转时报错,提示有多个匹配项. 2.查看区域添加路由步骤:两步即 ...

  3. 冷冻法快速还原Oracle数据库

    1.重新装完系统之后,把D:\app\Administrator\oradata拷贝出来,app目录. 2.安装Oracle数据库,创建一个和oradata目录内同名的实例. 3.通过停止服务的方式关 ...

  4. npm run serve 报错问题 (npm ERR! code ELIFECYCLE)

    运行 npm cache clean --force删除 node_modules删除 package-lock.json运行 npm install最后 npm run serve

  5. 城壁 (Rampart)

    题意简述 给定一张 $H \times W $ 的网格图,其中有 \(P\) 个被标记的点,求边长为 \(L\) 或以上的正方形的个数,要求正方形的边不得经过被标记的点. \(1 \le H,W \l ...

  6. CF1067E 题解

    题意 传送门 给定一棵 \(n\) 个节点的树,每条边有 \(\frac{1}{2}\) 的概率出现,可以得到一个森林,求这个森林邻接矩阵的秩的期望. \(1\le n\le5\times10^5\) ...

  7. c++单例模式设计

    class singleClass { public: static singleClass* getInstance() { //双判断保证在多线程下不会每次getInstance都加锁 if(!m ...

  8. gRPC中protobuff type和C# type原生标量对应表

    查看Microsoft官方文档: Create Protobuf messages for .NET apps | Microsoft Docs

  9. 【SQL Server】获取表格插入的id(二)——newID()

    现在有一个需求,插入api调用日志表.然后,发起HTTP请求()请求时,需要带入日志表的id). 简化无关的添加,SQL Server表格设计如下: CREATE TABLE mylog ( id I ...

  10. 【Java】zuul

    报错 com.netflix.zuul.exception.ZuulException: Hystrix Readed time out 解决办法,zuul模块的yml配置文件增加 ribbon: C ...