在 C python 中,访问字节码评估堆栈

In C python, accessing the bytecode evaluation stack

给定一个 C Python 帧指针,我如何查看任意计算堆栈条目? (一些特定的堆栈条目可以通过 locals() 找到,我说的是其他堆栈条目。)

我刚才问了一个更广泛的问题:

但这里我想着重于能够在运行时读取 CPython 堆栈条目。

我将采用适用于 CPython 2.7 或任何 Python 晚于 Python 3.3 的解决方案。但是,如果您有其他可行的方法,请分享,如果没有更好的解决方案,我会接受。

我不想修改 C Python 代码。 In Ruby, I have in fact done this 得到我想要的。我可以根据经验说,这可能不是我们想要的工作方式。但同样,如果没有更好的解决方案,我会接受。 (我对 SO 点的理解是,无论哪种方式,我都会在赏金中失去它。所以我很高兴看到它交给那些表现出最良好精神和意愿的人,假设它有效。)

更新: 见 user2357112 tldr 的评论; 基本上这很难做到。 (不过,如果您认为自己有勇气尝试,请务必尝试。)

因此,让我将范围缩小到这个我认为可行的更简单的问题:

给定一个 python 堆栈帧,如 inspect.currentframe(),找到计算堆栈的开头。在结构的 C 版本中,这是 f_valuestack。从那里我们需要一种方法 Python 从那里读取 Python values/objects 。

更新 2 好吧,赏金期已经结束,没有人(包括我自己的总结答案)提供具体代码。我觉得这是一个好的开始,我现在比以前更了解情况。在强制性的 "describe why you think there should be a bounty" 中,我列出了一个提供的选择 "to draw more attention to this problem" 并且在某种程度上,对问题的先前化身的看法不到十几个,因为我输入了这个观看次数略低于 190 次。所以这是成功的。然而...

如果将来有人决定继续使用它,请联系我,我会设置另一个赏金。

谢谢大家

这有时是可行的,使用 ctypes 直接访问 C 结构成员,但它很快就会变得混乱。

首先,在 C 端或 Python 端,没有 public API,所以就这样了。我们将不得不深入研究 C 实现的未记录的内部。我将专注于 CPython 3.8 实现;在其他版本中,细节应该相似,但可能有所不同。

PyFrameObject 结构有一个指向其计算堆栈底部的 f_valuestack 成员。它还有一个 f_stacktop 成员指向其评估堆栈的顶部......有时。在执行帧期间,Python 实际上使用 _PyEval_EvalFrameDefault 中的 stack_pointer 局部变量跟踪堆栈顶部:

stack_pointer = f->f_stacktop;
assert(stack_pointer != NULL);
f->f_stacktop = NULL;       /* remains NULL unless yield suspends frame */

恢复f_stacktop的情况有两种。一种是框架是否被 yield(或 yield from,或通过相同机制暂停协程的任何多个构造)暂停。另一个是在为 'line''opcode' trace event 调用跟踪函数之前。 f_stacktop 帧取消挂起时,或跟踪功能完成后,再次清除。

也就是说如果

  • 您正在查看一个挂起的生成器或协程框架,或者
  • 您当前正在使用帧的 'line''opcode' 事件的跟踪功能

然后您可以使用 ctypes 访问 f_valuestackf_stacktop 指针,以找到框架计算堆栈的下限和上限,并访问存储在该范围内的 PyObject * 指针。你甚至可以在没有 ctypes 的情况下使用 gc.get_referents(frame_object) 获得堆栈内容的超集,尽管这将包含不在框架堆栈上的其他引用。

调试器使用跟踪函数,所以这会让您在调试时为顶部堆栈帧获取值堆栈条目,大多数时候。它不会为调用堆栈上的任何其他堆栈帧提供值堆栈条目,也不会在跟踪 'exception' 事件或任何其他跟踪事件时为您提供值堆栈条目。


f_stacktop 为NULL 时,确定帧的堆栈内容几乎是不可能的。您仍然可以看到堆栈以 f_valuestack 开始的位置,但看不到它的结束位置。堆栈顶部存储在一个 C 级 stack_pointer 局部变量中,这真的很难访问。

  • 框架的代码对象 co_stacksize,它给出了堆栈大小的上限,但没有给出实际的堆栈大小。
  • 您无法通过检查堆栈本身来判断堆栈的结束位置,因为 Python 在弹出条目时不会清空堆栈上的指针。
  • f_stacktop 为空时,
  • gc.get_referents 不会 return 值堆栈条目。在这种情况下,它也不知道如何安全地检索堆栈条目(并且不需要,因为如果 f_stacktop 为 null 并且堆栈条目存在,则保证可以访问该框架)。
  • 您也许能够检查帧的 f_lasti 以确定它所在的最后一个字节码指令,并尝试找出该指令将从堆栈中离开的位置,但这需要大量深入的知识Python 字节码和字节码评估循环,有时它仍然是模棱两可的(因为帧可能在一条指令的中途)。不过,这至少会为您提供当前堆栈大小的下限,让您至少可以安全地检查其中的一部分。
  • 帧对象具有彼此不连续的独立值堆栈,因此您无法通过查看一个帧堆栈的底部来找到另一个帧的堆栈顶部。 (值堆栈实际上是在框架对象本身内分配的。)
  • 您也许可以使用一些 GDB 魔法或其他方法来查找 stack_pointer 局部变量,但这会是一团糟。

稍后添加注释:请参阅 crusaderky 的 get_stack.py,可能会在此处作为解决方案。

这里有两个可能的解决方案部分解决方案,因为这个问题没有简单明显的答案,缺少:

  • 修改 CPython 解释器或通过:
  • 检测字节码,例如通过 x-python

感谢user2357112对问题难度的指教,以及说明:

  • 运行 时使用的各种 Python 堆栈,
  • 非连续计算堆栈,
  • 评估堆栈的瞬态性和
  • 仅作为 C 本地存在的堆栈指针顶部 变量(在 运行 时间,可能或可能仅保存在 一个寄存器的值)。

现在讨论潜在的解决方案...

第一个解决方案是编写一个 C 扩展来访问 f_valuestack,它是帧的 bottom(不是顶部)。从那里您可以访问值,并且这也必须进入 C 扩展。这里的主要问题是,由于这是栈底,所以要了解哪个条目是栈顶或您感兴趣的。代码在函数中记录了最大栈深度。

C 扩展将包装 PyFrameObject,因此它可以访问未公开的字段 f_valuestack。尽管 PyFrameObject 可以从 Python 版本更改为 Python 版本(因此扩展可能必须检查哪个 python 版本是 运行ning),它仍然是可行的。

从中使用 Abstract Virtual Machine 计算出对于存储在 last_i.

中的给定偏移量,您将处于哪个入场位置

与我的目的类似的东西将用于我的目的是使用 真实的 但替代的 VM,如 Ned Batchhelder 的 byterun。它 运行 是 Python 字节码解释器 Python。

稍后添加的注释:为了支持 Python 2.5 .. 3.7 左右,我做了一些较大的修改,现在称为 x-python

这里的优势在于,由于它充当第二个 VM,因此存储不会更改当前和真实 CPython VM 的 运行ning。但是,您仍然需要处理与外部持久状态交互的事实(例如,跨套接字调用或文件更改)。并且 byte运行 需要扩展以涵盖所有可能需要的操作码和 Python 版本。

顺便说一句,对于以统一方式访问字节码的多版本(因为不仅字节码发生了一点变化,访问它的例程集合也发生了变化),请参见xdis

因此,虽然这不是一个通用的解决方案,但它可能适用于尝试计算 EXEC 的值的特殊情况,它短暂出现在评估堆栈上。

我试过这样做 in this package. As others point out, the main difficulty is in determining the top of the Python stack. I try to do this with some heuristics, which I've documented here

总体思路是,在调用我的快照函数时,堆栈由局部变量(正如您指出的)、嵌套 for 循环的迭代器以及当前正在处理的任何异常三元组组成。 Python 3.6 和 3.7 中有足够的信息来恢复这些状态,从而恢复栈顶。

我还依靠 user2357112 的提示为在 Python 3.8 中实现这项工作铺平了道路。

我写了一些代码来做到这一点。它似乎有效,所以我将它添加到这个问题中。

它是如何通过反汇编指令,并使用dis.stack_effect得到每条指令对堆栈深度的影响。如果有跳跃,它会将堆栈级别设置为跳跃目标。

我认为堆栈级别是确定性的,即它在一段代码中的任何给定字节码指令处始终相同,无论它是如何到达的。因此,您可以通过直接查看字节码反汇编来获取特定字节码的堆栈深度。

有一个小问题,如果您处于活动调用中,代码位置显示为调用的最后一条指令,但堆栈状态实际上是调用之前的状态。这很好,因为这意味着您可以从堆栈中重新创建调用参数,但您需要注意,如果指令是正在进行的调用,则堆栈将处于前一条指令的级别。

这是执行此操作的可恢复异常代码:

cdef get_stack_pos_after(object code,int target,logger):
    stack_levels={}
    jump_levels={}
    cur_stack=0
    for i in dis.get_instructions(code):
        offset=i.offset
        argval=i.argval
        arg=i.arg
        opcode=i.opcode
        if offset in jump_levels:
            cur_stack=jump_levels[offset]
        no_jump=dis.stack_effect(opcode,arg,jump=False)        
        if opcode in dis.hasjabs or opcode in dis.hasjrel:
            # a jump - mark the stack level at jump target
            yes_jump=dis.stack_effect(opcode,arg,jump=True)        
            if not argval in jump_levels:
                jump_levels[argval]=cur_stack+yes_jump
        cur_stack+=no_jump
        stack_levels[offset]=cur_stack
        logger(offset,i.opname,argval,cur_stack)
    return stack_levels[target]

https://github.com/joemarshall/unthrow