在 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_valuestack
和 f_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]
给定一个 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_valuestack
和 f_stacktop
指针,以找到框架计算堆栈的下限和上限,并访问存储在该范围内的 PyObject *
指针。你甚至可以在没有 ctypes 的情况下使用 gc.get_referents(frame_object)
获得堆栈内容的超集,尽管这将包含不在框架堆栈上的其他引用。
调试器使用跟踪函数,所以这会让您在调试时为顶部堆栈帧获取值堆栈条目,大多数时候。它不会为调用堆栈上的任何其他堆栈帧提供值堆栈条目,也不会在跟踪 'exception'
事件或任何其他跟踪事件时为您提供值堆栈条目。
当f_stacktop
为NULL 时,确定帧的堆栈内容几乎是不可能的。您仍然可以看到堆栈以 f_valuestack
开始的位置,但看不到它的结束位置。堆栈顶部存储在一个 C 级 stack_pointer
局部变量中,这真的很难访问。
- 框架的代码对象
co_stacksize
,它给出了堆栈大小的上限,但没有给出实际的堆栈大小。 - 您无法通过检查堆栈本身来判断堆栈的结束位置,因为 Python 在弹出条目时不会清空堆栈上的指针。 当
gc.get_referents
不会 return 值堆栈条目。在这种情况下,它也不知道如何安全地检索堆栈条目(并且不需要,因为如果f_stacktop
为 null 并且堆栈条目存在,则保证可以访问该框架)。- 您也许能够检查帧的
f_lasti
以确定它所在的最后一个字节码指令,并尝试找出该指令将从堆栈中离开的位置,但这需要大量深入的知识Python 字节码和字节码评估循环,有时它仍然是模棱两可的(因为帧可能在一条指令的中途)。不过,这至少会为您提供当前堆栈大小的下限,让您至少可以安全地检查其中的一部分。 - 帧对象具有彼此不连续的独立值堆栈,因此您无法通过查看一个帧堆栈的底部来找到另一个帧的堆栈顶部。 (值堆栈实际上是在框架对象本身内分配的。)
- 您也许可以使用一些 GDB 魔法或其他方法来查找
stack_pointer
局部变量,但这会是一团糟。
f_stacktop
为空时,稍后添加注释:请参阅 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]