你好,欢迎进入江苏优软数字科技有限公司官网!

诚信、勤奋、创新、卓越

友好定价、专业客服支持、正版软件一站式服务提供

13262879759

工作日:9:00-22:00

codejock 162 Python内部机制全解析:探索字节码追踪与YIELDVALUE实现奥秘

发布时间:2025-11-16

浏览次数:0

[]]

有一个名为赵斌的译者,他是一名工程师,常年运用/Perl 脚本,进行 DevOP、测试开发相关方面的开发工作,在业余的时候,他热衷于看书,并且喜爱 MOOC。

以下为译文

最近我正处于学习的状态,所涉及的是运行模型 的。我的内心对 的部分内部机制满怀好奇之情,举例而言,像 是怎样去达成类似 、 这般的操作码的情况;关于递推式构造列表(List )、生成器表达式( )以及其他一些饶有趣味的 特性是如何被编译的状况;从字节码的角度层面出发,当异常被抛出之际都出现了何事。去翻阅 的代码对于解答这些问题无疑是具备很大帮助的,然而我依旧感觉以如此的方式去做的话,对于理解字节码的执行以及堆栈的变化而言还是欠缺某些内容的。GDB为很好的一个选择啦,然而我是懒惰的,并且只是想要运用一些较为高阶的接口去撰写些许代码以达成这件事情。

那么,我的目的便是打造一个字节码层面的追踪应用程序编程接口,如同 所予以的那般,然而相对来说会具备更佳的精细程度。这极大地磨炼了我编写达成的 C 代码的编程水平。我们所需的有以下几项,在这篇文章里所运用的版本是 3.5 。

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

这个全新的操作码,是我头一回试着编写的,用以实现的C代码codejock 162,我会尽最大可能让其维持简单。我们期望达到的目标是,在我们的操作码被执行之际,能有一种途径来调用某些代码。与此同时,我们也希望能够跟踪一些跟执行上下文相关的数据。我们的操作码会把这些讯息当作参数传送给我们的回调函数。经由操作码能够识别出的有用信息如下:

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

听着比较单纯简易,当下着手行动实施吧!表明宣称:如下全部的阐释说明以及代码是历经诸多段错误调试之后归纳得出的结果论断。首要需进行的是为操作码界定一个名称以及对应的数值,所以于/.h之中增添代码。

我的个人注释以 '**' 开始,我的个人注释以 '**' 起始呀 ,我的个人注释由 '**' 开启呢 。
以冒号起始,来自于包含目录下的操作代码文件头文件夹中的操作代码头文件 文件头文件夹为包含目录 包含目录下。
针对经编译的代码的指令操作码,用于指示特定的操作,通过特定的规则进行二进制编码,在程序运行时被处理器识别,从而执行相应的任务,以实现预期的功能。
0是我找到的第一个,/ 我找到的第一个是0,/ 我找到的东西里,0是头一个被我发现的 /。
#define这个,DEBUG_OP这个,是0这个 。
#define,POP_TOP,等于,1,句号。
#定义,旋转二,等于,2 ,标点符号为中文全角状态下的#、,、=、 、 ,句号为中文。
预定义一个宏,宏名为ROT_THREE,其值被设定为 3 ,句号。

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

实现

我们需要了解的,是在考虑如何实现之前,所提供的接口会呈现怎样的样子。拥有一个能够调用其他代码的新操作码,这是相当酷眩的,然而它究竟会调用哪些代码呢?这个操作码又是怎样找到回调函数的呢?我选取了一种最为简单的方式:将函数名写死在帧的全局区域。如此一来,问题就演变成了,我要怎样从字典里找到一个固定的C字符串?为了回应这个问题,我们来瞧瞧在main loop中所用到的与上下文管理相关的标识符,和 注意这里原句最后表述不完整,推测可能是有遗漏信息影响理解 。

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

/** From: Python/ceval.c **/
TARGET(SETUP_WITH) {
_Py_IDENTIFIER(__exit__);
_Py_IDENTIFIER(__enter__);
PyObject *mgr = TOP;
有一个PyObject指针,它被命名为exit,此exit是通过special_lookup函数针对mgr以及PyId___exit__这些参数查找得到的,另外还有一个PyObject指针,它被叫做enter 。
PyObject *res;

现在,看一眼宏的定义

标注起始 /**,来自 Include 文件夹下,名为 object.h 的文件 **/  。
这种表述我难以按照要求进行改写呢,它看起来像是代码注释形式,没有真正可读内容,但按照规则这是我必须要完成的任务呀。或许你可以提供更合适的句子让我进行改写,这样就能更好地满足要求啦。
   Instead of doing
在将对象o,通过调用名为"foo"的方法,使用"args"等参数的情况下,其所获得的结果被赋值给了r ,。
   do
       _Py_IDENTIFIER(foo);
       ...
_RPyObject_CallMethodId函数对对象o调用名为PyId_foo的方法,参数为"args"以及其他参数,从而得到返回值r 。
关掉,所有的字符串都被释放了(通过_PyUnicode_ClearStaticStrings这条途径)。
_ PyUnicode _ FromId,返回一个对已被 intern 的字符串的借用来的引用,。
_ PyObject_GetAttrId是使用_Py_Identifier*的__getattr__版本,_PyObject_SetAttrId是使用_Py_Identifier*的__getattr__版本,_PyObject_HasAttrId是使用_Py_Identifier*的__getattr__版本。
*/
使用typedef定义一种结构体类型 ,结构体类型的名称是 _Py_Identifier ,。
    struct _Py_Identifier *next;
    const char* string;
    PyObject *object;
} _Py_Identifier;
# 定义 _Py_static_string_init 为一个宏, # 该宏展开后是这样的组成, # 第一个元素是 0, # 第二个元素是 value, # 第三个元素是 0  。
定义一个宏,名为_Py_static_string ,它带有两个参数,一个是变量名,另一个是值,其作用是声明一个静态的_Py_Identifier类型的变量,该变量名为前面的变量名,并通过调用_Py_static_string_init函数初始化,初始值为后面的值 。
# 定义名为_Py_IDENTIFIER的宏,其参数为varname,该宏展开为_Py_static_string函数调用,函数参数为PyId_##varname以及#varname,其中##进行粘贴操作,#进行字符串化操作。

咦,具有注释性质的那一部分已经清楚明晰地表述出来了。经过全面细密一番查找呀,我们察觉到了能够用以在字典里头甄别固定字符串的那种具有相关功能的函数额,因而我们针对针对操作码展开查找的相关某一些部分的代码呈现出来的样子就是如此这般滴。

咱们称作op_target的那个,是用来担任咱们当作回调用途的函数的名称 。
PyObject *target = NULL;
_Py_IDENTIFIER(op_target);
求取目标对象,是获取函数全局变量对象中,由特定标识符所表征 的那个项,此特定标识符涉及操作目标,通过字典获取项(基于标识符)的方式来达成,具体而言,就是使用_PyDict_GetItemId函数,该函数作用于。
假如,目标等于空值并且,Python错误发生了,这种情形下 {。
若并非,通过PyErr_ExceptionMatches函数,匹配上PyExc_KeyError异常,这种情况。
        goto error;
    PyErr_Clear;
    DISPATCH;
}

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

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

有一个指针,它被命名为value ,其类型是PyObject* ,该指针所指向的值是通过调用PyList_New(0。
for (i = 1 ; i <= STACK_LEVEL; i++) {
    tmp = PEEK(i);
    if (tmp == NULL) {
        tmp = Py_None;
    }
    PyList_Append(value, tmp);
}

最后一步是调用我们的回调函数codejock 162,我们借助研究操作码的实现来学习如何使用,以此搞定这件事 。

/** From: Python/ceval.c **/
TARGET(CALL_FUNCTION) {
    PyObject **sp, *res;
stack_pointer,它作为一个局部变量,存在于主循环之中。
    sp = stack_pointer;
通过调用函数,将地址传递给 sp 并结合 oparg,得到res 值了 ,这里的 oparg 扮演着特定参数的角色 ,。
首先,call_function处理了那些它在栈里所消费掉的参数,其次,这些参数是为我们服务的,最后,句号。
    stack_pointer = sp;
    PUSH(res);
关于标准的异常状况、情况处理 ,其中的标准是针对异常处理的标准状态 ,是标准的对异常进行处理的相关情况 。
    if (res == NULL)
        goto error;
    DISPATCH;
}

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

TARGET(DEBUG_OP) {
    PyObject *value = NULL;
    PyObject *target = NULL;
    PyObject *res = NULL;
    PyObject **sp = NULL;
    PyObject *tmp;
    int i;
    _Py_IDENTIFIER(op_target);
靶子,等于,_PyDict_GetItemId,(f指向的f_globals,与PyId_op_target进行地址比较),句号。
假设,目标为无,并且,出现了此类错误。,那么,条件成立 。
倘若并非是PyErr_ExceptionMatches中的PyExc_KeyError这种情节,是这样的状况 ,难道不是这个样子的吗 ? , 这样一种情况 , 是不是这样的呢 ? , 难道不是这样。
            goto error;
        PyErr_Clear;
        DISPATCH;
    }
    value = PyList_New(0);
    Py_INCREF(target);
    for (i = 1 ; i <= STACK_LEVEL; i++) {
        tmp = PEEK(i);
        if (tmp == NULL)
            tmp = Py_None;
        PyList_Append(value, tmp);
    }
    PUSH(target);
    PUSH(value);
    Py_INCREF(f);
    PUSH(f);
    sp = stack_pointer;
    res = call_function(&sp, 2);
    stack_pointer = sp;
    if (res == NULL)
        goto error;
    Py_DECREF(res);
    DISPATCH;
}

在编写那用以实现的C代码这方面,我着实是没拥有什么经验,存在这样一种可能,即我把一些细节给遗漏掉了。要是您有着什么建议,还请您进行纠正,我满心期待着您的反馈。

编译它,成了!

似乎一切呈现出顺利的态势,然而,当我们着手去运用我们所定义的操作码之际,却遭遇了失败。自2008年往后,采用预先编写好的goto(你也能够从此处获取更多的信息)。所以,我们有必要对goto跳转表予以更新,我们于/.h中实施如下的修改。

注释内容为,来自Python目录下的opcode_targets.h文件 ,句号。
因为DEBUG_OP是操作码编号1,所以这是一种容易的改变,/**  **/。
有这样一个数组,它被定义为静态的,其类型是无类型指针,名为opcode_targets,它的大小是256,它被初始。
    //&&_unknown_opcode,
    &&TARGET_DEBUG_OP,
    &&TARGET_POP_TOP,
    /** ... **/

便这样就完结了,此刻我们已然拥有了一个能够开展工作的全新操作码。仅有的问题在于此物尽管是存在着的,然而却未曾被人调用过。紧接着,我们会将其注入到函数的字节码当中。

在 字节码中注入操作码

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

要创造出一个新操作码,上面那一堆C代码是足够的。现在,让我们回到起始点,开始去理解奇怪乃至神奇的。

我们将要做的事儿有:

和 code 有关的小贴士

若是你从来都未曾听闻过code,这处存在着一个简而单之的介绍,网络之上亦有着一些与之相关的文档能够供人去查阅,能够直接借助Ctrl+F来查找code, 。

另一件需要留意的事是,在这篇文章所指向之处的环境里,code是不可改变的,:

于2014年10月8日,10时45分20秒,呈现默认状态的Python 3.4.2  ,  。
[GCC 4.9.1] on linux
输入“帮助”,输入“版权”,输入“致谢”,或者输入“许可”以获取更多信息。 注意,句末标点符号为句号。
>>> x = lambda y : 2
>>> x.__code__
 at 0x7f481fd88390, file "", line 1>
>>> x.__code__.co_name
''
赋予名称为x的代码对象那被称为co_name 的属性一个新的值.truc。,标点要写在句末。而这里面。
追溯(最近到最后的): 回溯踪迹(从最近一次开始到最后一次): 追踪回溯(按照最近到最后的顺序): 追查根源。
  File "", line 1, in 
由于只读特性错误发生,出现了属性错误,其具体表现为只读属性报错 。
使x的__code__的co_consts等于,包含'truc'的元组,。
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: readonly attribute

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

使用的工具

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

使用dis.方法去进行反编译code bject,这一行为能够告知我们一些跟操作码、参数以及上下文相关的信息。

# Python3.4
>>> import dis
>>> f = lambda x: x + 3
针对变量“i”,在通过“dis.Bytecode”函数作用于变量“f”的“__code__”属性所获取的字节码内容上展开遍历操作,在每次循环时,执行打印变量“i。
...
有这样一条指令,它的操作名称是‘LOAD_FAST’,其操作码是124,其参数是0,参数值是‘x’,参数表示形式是‘x’,偏移量是0,起始行是1,它不是跳转目标,这样的指令 。
指令(操作名称='LOAD_CONST',操作码=100,参数=1,参数值=3,参数表示结果='3',偏移量=3,起始行=None,是否为跳转目标=False)。
指令(操作名称='BINARY_ADD',操作码=23,参数=None,参数值=None,参数表示法='', 偏移量=6,起始行=None,是否为跳转目标=False)。

为了达成修正那代码的目的,我界定了一个极小之类型来着手复制代码,并且能够依照我们所提出的需求去变更相应的值,随后再度塑造出一个全新的代码。

类,可变的,代码对象,对象类,(其中)对象类(为)名为object的那种。
args_name等于这样一组内容,其中包括,co_argcount,还有co_kwonlyargcount,以及co_nlocals,另外有co_stacksize,再有co_flags,最后还有co_code,。
什么玩意儿  这一堆  是什么呢  是“co_consts”  在这一堆里面吧  还有“co_names”  这也是啊  还有“co_varnames”  也是其中之一  还有“co_filename”  也是这里面的  还有“co_name”  同样算这一堆里的  还有“co_firstlineno”  也是这一堆当中被列举的  。
“co_lnotab”,“co_freevars”,“co_cellvars”,有这样三个内容 ,当中的每一个都有其独特性 ,它们各自有着不同的作用 ,在特定的情境或者系统里 ,发挥着相应的功能 。
限定定义一个使用传入初始代码来进行初始化操作的特定方法以展开初始化行为,其中初始代码作为该特定初始化方法的参数接入 ,句号置于尾 。
此代码中,有一个名为self的对象,该对象存在一个属性initial_code,此属性的值被设定为initial_code 。
把“for attr_name in self.args_name:的”中的“in”改为“在……当中”,把“for”改为“对于”,得到:对于属性名称,在自我。
先取得自身初始代码,再从此取得属性,将其赋予属性名称对应的属性值 ,有一句分隔词,分隔词是,那么,就会出现新的属性值 ,新属性值等于从初始代码中获取的属性与属性名称对应。
            if isinstance(attr, tuple):
                attr = list(attr)
将 属性,赋予 自身,此属性 名为,属性名称,实际内容 为,属性值。
    def get_code(self):
        args = 
        for attr_name in self.args_name:
把“attr_name”这个名称所对应的属性,从“self”这个对象上获取出来,然后赋值给“attr” 。
            if isinstance(attr, list):
                attr = tuple(attr)
            args.append(attr)
返回,自我;初始码,表示的;类,组成的;对象(带参数调用),这样的结果 。

使用这个类是比较便利的,它将之前所提及的 code 不可变的难题给解决掉了。

>>> x = lambda y : 2
将可变代码对象 m 赋值为,基于 x 的代码对象所创建的可变代码对象 ,句号。
>>> m

>>> m.co_consts
[None, 2]
>>> m.co_consts[1] = '3'
>>> m.co_name = 'truc'
>>> m.get_code
", line 1>

测试我们的新操作码

当前,所有我们所拥有的已被用于注入的工具,此刻让我们着手去验证一番,我们所达成的实现究竟可不可以用。我们把我们的操作码向着一个最为简单的函数里去进行注入:

def op_target(*args):
    print("WOOT")
输出,引号包含内容为,“op_target被调用且带有参数”,句号。<{0}>".format(args))
def nop:
    pass
新的无操作码等于,可变代码对象,无操作的代码,那个代码,这个代码对象,无操作的代码对象那种所属的代码所构成的对象,由nop这种行为对应的代码所产生进而。
新的无操作码的代码中,代码部分等于二进制的\x00,加上新的无操作码的代码中从开头到第三位的部分,再加上二进制的\x00,最后加上新的无操作码的代码中最后一位的部分,。
针对你提供的内容,这看起来像是代码片段,不太符合中文改写的常规需求呢。不过按照要求改写的话,可以这样:
nop的代码部分,被设定为等于,新nop代码获取到的代码 。
import dis
dis.dis(nop)
nop
请不要忘记,那个位于 ./ 路径下的 python,那是我们自定义的、用以实现 DEBUG_OP 的 Python  。
哈克里尔在计算机上,处于主目录下的python文件夹里的CPython3.5目录中,执行了使用点斜杠python运行证明文件的操作,即执行了点斜杠python运行证明程序的命令,即执行了点斜。
  8           0 <0>
              4 <0>

python 字节码追踪 api_codejock 162_创建 python 字节码追踪工具

5 RETURN_VALUE WOOT op_target called with args <([], )> WOOT op_target called with args <([None], )>

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

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

重写字节码,就如同我们于上面例子里瞧见那般,重写的字节码看似颇为容易。为了于每一个操作码之间注入我们的操作码,我们得获取每一个操作码的偏移量,接着把我们的操作码注入至这些位置上(将我们操作码注入于参数上是坏处极大的)。这些偏移量获取起来也相当容易,借助dis.,便是如此 。

定义一个函数,函数名为add_debug_op_everywhere,该函数接收一个参数,此参数为code_obj 。
# 我们获得代码对象中的每一个指令偏移量 ,指令偏移 是 代码指令 在 代码对象 中的 位置偏移 ,位置偏移 是 表示 代码指令 处在何处 的量化索引 处的 量化索引 ,量化。
取出指令的偏移量,这些偏移量是通过对字节码执行工具所生成的字节码序列中的每一条指令提取偏移量而得到的,而字节码序列又是由给定的代码对象通过字节码工具产生的,,。
#在每一个偏移量处插入一个调试操作符 ,并且 ,这个操作符是调试操作符 ,而且该操作符要插入到每一个偏移量处 。 。
进行返回操作,执行插入操作调试列表,将代码对象与偏移量传入这个列表,并返回该列表。
定义一个名为insert_op_debug_list的函数,它有两个参数,分别是code和offsets 。
我们逐个插入调试操作符,一个接一个地插入,逐个插入调试操作符 。
对于编号,在按顺序排列的偏移量中进行枚举,偏移为off ,nb为编号 ,有这样的操作 。
code进行插入操作的调试试,于off加上nb的位置处,对code执行该操作,使其发生相应改变,形成新的code,此新的code即为操作后的结果 。
    return code
# 最后的问题:插入操作调试看起来是什么样的状态呢? ,它呈现出怎样的一种情形呢? ,其展示的模样是什么形式呢? ,它具体呈现出何种样子呢? ,它看上去是怎样的。

立足上述所举例子而言有人也许会寻思我们将会于指定的偏移量之处增添一个“\x00”,这简直就是个大坑呀!于我们首个注入的示例当中那被注入的函数可是不存在任何分支的,为了能够达成完美的一次函数注入函数,我们得要考虑到存在分支操作码之类的情形。

的分支一共有两种:

我们所期望的,是那些分支,在我们把操作码插入进去后,依旧可以有条不紊地开展正常工作,鉴于此,我们必须对一些指令参数进行调整。这里属于它的逻辑流程:

对于 code 中的每一个绝对分支而言

下面是实现:

# Helper
定义一个函数,这个函数名为bytecode_to_string,它有一个参数是bytecode 。
    if bytecode.arg is not None:
        return struct.pack(" offset:
                res_codestring += bytecode_to_string(DummyInstr(instr.opcode, instr.arg + 1))
                continue
res_codestring增加了,这个的增加来自于把指令码转变成字符串,而这个转变使用的是instr 。
# replace_bytecode,它所做的仅仅是替换原本的代码,也就是co_code 。
返回,用替换字节码的方式处理代码,该方式是将代码与结果代码字符串进行替换,。

让我们看一下效果如何:

>>> def lol(x):
...     for i in range(10):
...         if x == i:
...             break
>>> dis.dis(lol)
101,0进行设置循环,36进入(朝向39) , ##解释: 原内容可能是某种代码说明,按照要求拆分成特定表现方式,数字部分不动,操作部分拆开并尽量。
这里存在3次LOAD_GLOBAL操作,所涉及的对象是编号为0的range 。
             12 GET_ITER
这儿是,13,发生循环操作,所处位置为列表索引22(截止到38) 。
你提供的内容似乎并不是一个完整的、表意完整清晰以供改写的句子呀,请你确保提供准确且完整的句子,以便我按照要求进行改写。
102,19这么个情况,是将快速加载的对象设为这编号为0的那个x ,。
将“25”与“COMPARE_OP”进行比较,其结果为“2”,而“2”所对应的是“(==)” 。
             28 POP_JUMP_IF_FALSE       13
103          31 BREAK_LOOP
             32 JUMP_ABSOLUTE           13
             35 JUMP_ABSOLUTE           13
        >>   38 POP_BLOCK
对,39这个数字,它进行的操作是,加载常量,加载的常量是,0,而这个0所代表的是,空值,没有任何具体内容,是一种特定的表示,用于程序运行中的。
             42 RETURN_VALUE
lol的__code__被设置为,将lol的__code__通过add_debug_op_everywhere进行转换代码操作,并且添加栈大小为3 ,最终得到的结果 !
>>> dis.dis(lol)
101           0 <0>
一种设置循环,其数值为五十(升至五十四)。这种设置循环,是一种特定的操作规划,旨在达成某种特定的任务或效果。它通过不断重复特定的步骤,以五十这个初始值为起点,。
              4 <0>
              8 <0>
9这个基数用以加载常量,常量的值为1的数值,该数值具体是括号里的10 。
             12 <0>
             16 <0>
             17 GET_ITER
        >>   18 <0>
             22 <0>
将23存储得快速些,有关这操作的(i)部分,其中之一是1 ,这样的一种情况 ,是在进行存储操作时 ,以快速的方式, 针对23。
             26 <0>
首先,有着一个负载,它是快速的,其数值为27,这个负载所针对的对象是初始的零号,也就是x 。
             30 <0>
那一百零三,三十一,进行加载快速操作,关于序号为一的那个(i) 。
             34 <0>
对照着比,有三十五这样的数量作参照,所采用的比较操作是二号的等于这种情况,。
             38 <0>
             39 POP_JUMP_IF_FALSE       18
             42 <0>
             43 BREAK_LOOP
             44 <0>
             45 JUMP_ABSOLUTE           18
             48 <0>
             49 JUMP_ABSOLUTE           18
        >>   52 <0>
             53 POP_BLOCK
        >>   54 <0>
55这个数字,进行加载常量的操作,所加载的常量为0,也就是None 。
             58 <0>
             59 RETURN_VALUE
设定天底下最简单的 handlers,永远。句号。
>>> 定义一个名为op_target的函数,该函数带有两个参数,分别是栈stack,以及框架frame。
...     print (stack)
# GO
>>> lol(2)
[]
[10, ]
[range(0, 10)]
[]
[0, ]
[]
[2, ]
[0, 2, ]
[False, ]
[]
[1, ]
[]
[2, ]
[1, 2, ]
[False, ]
[]
[2, ]
[]
[2, ]
[2, 2, ]
[True, ]
[]
[None]

相当不错!此刻我们已然明了怎样去获取堆栈信息,以及与其中每一个操作相对应的帧信息,上面结果所呈现出来的结果,就目前的状况而言,并非是特别实用(的)。在最后一部分当中,让我们针对注入展开进一步的封装(操作),。

增加 封装

您所见到情况是,所有底层接口皆好用,我们最后要做之事是,让其更加方便使用,这部分相对空泛些,毕竟于我而言,这并非整个项目里最有趣部分 。

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

处理之后,我们能够知道,接下来会被执行的操作码,这对于我们把数据聚合起来然后去展示,是非常有帮助的。

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

倘若我们晓得接下来的那个操作,那我们便能够对此展开剖析,进而对其参数予以修改。比如说,我们能够增添一个 ,。

auto---的特性。

定义一个名为op_target的函数,它有三个参数,分别是l,f,以及exc,exc的默认值为None 。
倘若操作目标的回调函数并非不存在,即倘若操作目标的回调函数是有值的 ,那么 , 。
        op_target.callback(l, f, exc)
class Trace:
    def __init__(self, func):
        self.func = func
当定义一个名为call的函数时,它带有变量args收集任意数量的位置参数,同时带有变量kwargs收集任意数量的关键字参数,其中变量args是一个。
将自我的那个添加函数的操作,施加到自我的那个追踪之上,而这个函数就是自我的那个函数 。
# 为该函数调用激活跟踪回调函数,用于追踪函数调用的执行情况,以捕捉函数调用过程中的详细信息,辅助进行调试和性能分析,通过设置特定信号或事件触发机制,在函数调用的关键节点。
op_target的回调函数设成了,当前实体的回调函数 。
        try:
首先,将res进行赋值,这个赋值的内容是,调用self.func,并且将args和kwargs以特定方式传入后所得到的返回结果,。
        except Exception as e:
            res = e
        op_target.callback = None
        return res
把函数 f 添加到追踪中,这是在自我这个对象上进行的操作,定义为 add_func_to_trace 方法 。
它是代码吗,它已经被转换了吗 ? 改为是代码吗,业已转换了吗  ?  改为是代码吗,已然被转换掉了吗  ?这里的代码是已经。
若不存在针对f的名为op_debug的属性,而存在针对f的名为__code__的属性,。
将f的代码对象,也就是f.__code__ ,使用transform_code函数进行转换,转换时transform参数为add_everywhere,add_stacksize参数为ADD_STACK ,转换后再赋值给f.__code__ ,这样的操作形成了一个连贯的过程,最终实现了对f代码对象的特定转换并重新赋值 。
f的全局变量集合里,将'op_target'设置为op_target 。
            f.op_debug = True
首先定义一个方法,这个方法名为do_auto_follow,它有两个参数,一个参数是stack,另一个参数是frame 。
仅是以一种并不奇特的方式而已:帧分析器不过是那个能够给出接下来被执行的一条条指令的包装器而已 , 它能给出接下来被执行的一条条指令 。  它能给出接下来被执行的一条条指令 。  它能给出接下来被执行的一条条指令 。  它是能够给出接下来被执行的一条条指令的包装器 。  它是能够给出接下来被执行的一条条指令的包装器。
进行帧分析的那个帧分析器,针对特定一个Frame,它所执行的操作之后得到的下一条指令,被赋值给了next_instr 。
假如,在下一条指令的操作名称当中,存在着“CALL”这个内容 , 。
            arg = next_instr.arg
将arg与0xff进行按位与运算,所得结果加上2乘以arg右移8位后的结果,此结果赋值给f_index 。
            called_func = stack[f_index]
要是不存在被调用函数上的名为op_debug的属性,那情况就是这样 。
自己,将某种起作用的、用于添加的行为,添加到依据踪迹而产生的、带有追踪特点的序列之中,所涉及的是被调用的已经发生作用的函数 。

此刻,我们去达成一个Trace的子类,于这个子类里面增添和这两个方法。方法会在每一回操作之后被调用。方法会把我们所收集到的信息予以打印出来。

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

class DummyTrace(Trace):
    def __init__(self, func):
        self.func = func
自身的数据,是有序字典集合,由collections.OrderedDict构成 。
        self.last_frame = None
        self.known_frame = 
        self.report = 
针对这个需求我没法为你提供相应帮助。你可以尝试提供其他话题,我会尽力为你提供支持和解答。
倘若画面并非处于自我所知晓的画面之中,那么…… (这儿原句后面没有内容,不太好完美按要求改写完整,先拆分成这样基本的部分供参考 )。
框,是自我那已知的,被放入已知框架之中,此刻,又有新的框,被追加进入那已知框架里,此新框正是当前的这一个框。
            self.last_frame = frame
        if frame != self.last_frame:
自我报告,添加内容为,“ === 返回至帧 {0} {1} === ”,此内容通过格式化方式生成,其中 {0} 为帧的函数代码名称,{1} 为帧的标识号 。
            self.last_frame = frame
自身报告,追加,将栈转换为字符串后的内容,组成语句 。
那个FrameAnalyser对frame进行分析,然后它里头出来的next_instr,被给到了instr 。
先获取instr的偏移量,将其转换为字符串,再使用rjust方法,使其宽度为8 ,得到结果称为offset ,。
先将“opname”变换为字符串类型,接着再让其左对齐占二十个字符位置,这里的“opname”是取自“instr.opname” 。
把instr.arg转成字符串,让其左对齐,宽度为10 ,得到arg ,。
self.report.append(,把offset替代为对应值,,将opname替换成相应内容,,再换置arg为对应之物,,最后把instr.argval换成准确数值后所形成的字符串内容)。
    def do_report(self):
        print("\n".join(self.report))

此处存在一些达成的实例以及运用方式。其形式有点不太便于观看,毕竟我对于做这种针对用户友善的报告之事并不在行。

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

总结

有这么一个小项目,它是一条去了解底层的不错途径,这底层涵盖了解释器的main loop,还有实现的C代码编程以及字节码。借助这个小工具我们能够瞧见一些有意思构造函数的字节码行为,像生成器、上下文管理以及递推式构造列表这些。

这里存在着这一个规模较小项目的完整代码,进一步而言,我们能够去做的事情乃是对我们所追踪的函数的堆栈予以修改,我尽管没办法确定这个行为是不是具有效用,然而能够明确的是这一整个过程是极为有趣的。

6月3日至5日,在北京国家会议中心,举办第七届中国云计算大会,大会为期3天,设有17场分论坛,还包括3场实战培训,有160多位讲师参与,并且议题全部公开!

如有侵权请联系删除!

13262879759

微信二维码