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

roc*_*cky 5 python bytecode cpython reverse-engineering disassembly

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

不久前,我问了一个更广泛的问题:

获取C python exec参数字符串或访问评估堆栈

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

我将采用在CPython 2.7或Python 3.3之后的任何Python上均可使用的解决方案。但是,如果您还有其他工作要解决,请分享,如果没有更好的解决方案,我将接受。

我宁愿不修改C Python代码。实际上,在Ruby中,我这样做是为了获得想要的东西。我可以根据经验说,这可能不是我们想要的工作方式。但是,如果没有更好的解决方案,我会考虑的。(我对SO点的理解是,无论哪种方式,我都会以失败的方式失去它。因此,我很高兴看到它表现出最大的精神和意愿去看待这一点(假设它可行)的人。)

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

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

给定python堆栈框架(如)inspect.currentframe(),找到评估堆栈的开始。在C版本的结构中,这是f_valuestack。然后,我们需要使用Python中的一种方法从那里读取Python值/对象。

更新2好赏金的期限已经结束,没有人(包括我自己的简要答案)提供具体的代码。我觉得这是一个良好的开始,现在我对情况的了解比以前要多得多。在强制性的“描述为什么您应该有赏金的描述”中,我列出了其中一种选择,“以引起更多对这个问题的关注”,并且在某种程度上,对于先前化身的看法少于十二种。问题,当我键入此内容时,它已经被查看了不到190次。所以这是成功的。然而...

如果将来有人决定进一步进行此操作,请与我联系,我将设立另一个赏金。

谢谢大家

use*_*ica 5

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

首先,在C端或Python端没有为此提供公共API,因此就可以了。我们必须深入研究C实现的未记录内部。我将重点介绍CPython 3.6的实现;在其他版本中,细节应该相似,尽管可能有所不同。

PyFrameObject结构具有一个f_valuestack成员,该成员指向其评估堆栈的底部。它还有一个f_stacktop成员,它有时指向其评估堆栈的顶部。在执行框架期间,Python实际上使用以下代码中的stack_pointer局部变量来跟踪堆栈的顶部_PyEval_EvalFrameDefault

stack_pointer = f->f_stacktop;
assert(stack_pointer != NULL);
f->f_stacktop = NULL;       /* remains NULL unless yield suspends frame */
Run Code Online (Sandbox Code Playgroud)

正如代码注释所指出的那样,除非帧中的代码执行yield...或an awaityield from在字节码级别使用实现),否则永远不会还原f_stacktop 。在yield或上await,Python将stack_pointer保存回f_stacktop,但除此之外,有关栈顶位置的信息位于C级局部变量中,并且确实很难访问。

如果f_stacktop为非NULL,则可以通过检查f_valuestackf_stacktop使用ctypes 来确定框架的堆栈内容。您甚至可以使用而不使用ctypes获得堆栈内容的超集gc.get_referents(frame_object),尽管这将包含不在框架堆栈中的其他引用对象。

f_stacktop为NULL时,确定框架的堆栈内容几乎是不可能的。您仍然可以看到堆栈的起始位置f_valuestack,但是看不到堆栈的结束位置。

  • 有框架的代码对象co_stacksize,它给出了堆栈大小的上限,但没有给出实际的堆栈大小。
  • 您无法通过检查堆栈本身来判断堆栈的结束位置,因为Python在弹出条目时不会清空堆栈上的指针。
  • gc.get_referentsf_stacktop为null 时不返回值堆栈条目。在这种情况下,它也不知道如何安全地检索堆栈条目(也不需要这样做,因为如果f_stacktop为null并且存在堆栈条目,则可以保证该帧是可到达的)。
  • 您也许可以检查框架f_lasti以确定最后一条字节码指令,然后尝试找出该指令将离开堆栈的位置,但是这将需要大量的Python字节码和字节码评估循环知识,并且有时还是am昧。但是,这至少会给您当前堆栈大小的下限,让您安全地检查其中的至少一部分。
  • 框架对象具有彼此不连续的独立值堆栈,因此您无法查看一帧堆栈的底部来查找另一帧的顶部。(值堆栈实际上是在框架对象本身内分配的。)
  • 您也许可以stackpointer用一些GDB魔术或类似的东西来搜寻局部变量,但这很混乱。

  • 这里涉及多少堆栈可能会很令人困惑。有一个C堆栈,通常每个C函数调用都有一帧,有Python堆栈帧,其中大多数对应于Python函数调用,并且对于每个Python堆栈帧,都有一个值堆栈,其中Python字节码将值保存到操作。值堆栈是独立的堆栈,而不是较大堆栈的框架。值堆栈是您要检查的内容。 (2认同)

roc*_*cky 2

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

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

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

感谢 user2357112 对问题难度的启发,以及以下描述:

  • 运行时使用的各种Python堆栈,
  • 非连续的评估堆栈,
  • 评估堆栈的瞬态性和
  • 堆栈指针 top 只作为 C 局部变量存在(在运行时,可能或很可能只保存在寄存器的值中)。

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

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

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

由此,使用抽象虚拟机来确定对于存储在 中的给定偏移量,您将位于哪个条目位置last_i

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

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

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

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

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