【问题标题】:In C python, accessing the bytecode evaluation stack在 C python 中,访问字节码评估堆栈
【发布时间】:2017-11-04 21:33:36
【问题描述】:

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

前段时间我问了一个更广泛的问题:

getting the C python exec argument string or accessing the evaluation stack

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

我将采用适用于 CPython 2.7 或 Python 3.3 之后的任何 Python 的解决方案。但是,如果您有除此之外的工作,请分享,如果没有更好的解决方案,我会接受。

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

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

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

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

更新 2 赏金的时间已经结束,没有人(包括我自己的总结答案)提供具体代码。我觉得这是一个好的开始,我现在比以前更了解情况。在强制性的“描述你为什么认为应该有赏金”中,我列出了“引起对这个问题的更多关注”的一种选择,并且在某种程度上,对前世的看法只有不到一打问题是,当我输入此内容时,它的浏览量略低于 190 次。所以这是成功的。不过……

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

谢谢大家。

【问题讨论】:

  • Rocky,你能在问题中添加一些细节吗?您是否想要适用于原始 CPython 的纯 Python 解决方案,或者您可以修改 CPython?使用的 CPython 的确切版本是什么?如果出现异常或致命错误,CPython 中是否有回溯打印?您只想回溯(哪个函数被哪个调用)还是想查找和访问存储在堆栈中的局部变量?
  • @osgx 问题已修改。我怀疑如果你让回溯工作,你通常可以做到。但为简化起见,如果您有一个仅适用于当前框架指针以外的 Python 帧指针的解决方案,我会接受这一点,假设没有更好的解决方案。
  • Rocky,您能否提供更多关于您在概念上试图实现的目标以及上下文的信息?例如。您是否正在编写调试器,或者您想要从堆栈帧中获得什么信息?您在哪里/如何获得堆栈帧,作为异常的一部分,自定义调试器等?我(我们)不想回答很多可能不相关的代码。
  • @Abraham 查看讨论和聊天。如果这还不够,请告诉我。

标签: python reverse-engineering bytecode cpython disassembly


【解决方案1】:

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

首先,在 C 端或 Python 端没有用于此的公共 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 * 指针。您甚至可以使用gc.get_referents(frame_object) 获得没有 ctypes 的堆栈内容的超集,尽管这将包含不在框架堆栈上的其他引用。

调试器使用跟踪函数,因此在大多数情况下,这可以让您在调试时为顶部堆栈帧的堆栈条目赋值。它不会为调用堆栈上的任何其他堆栈帧获取堆栈条目的值,并且在跟踪 'exception' 事件或任何其他跟踪事件时,它不会为您获取堆栈条目的值。


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

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

【讨论】:

  • 好的。如果它会简化事情,让我们只处理当前帧以外的帧。这样我认为我们可以使用f_valuestack,如果我理解正确的话,它是当前帧的底部,如果是这种情况,那么就是前一帧的顶部。而且我想从那里到最后一次调用之前f_valuestack 的先前值的所有内容都会得到我们想要的。这有意义吗?
  • @rocky:那行不通。帧具有完全独立的值栈;它们不像 C 堆栈帧那样连续。您无法通过查看一个堆栈帧的底部来找到另一个堆栈帧的顶部。
  • 好的。但是 C 程序仍然必须有一种方法来(重新)设置从函数返回时堆栈顶部的位置。这是怎么做的?某种 C 局部变量?
  • 这里涉及到多少堆栈可能很令人困惑。有 C 堆栈,每个 C 函数调用(通常)有一个帧,有 Python 堆栈帧,其中大多数对应于 Python 函数调用,并且对于每个 Python 堆栈帧,都有一个值堆栈,Python 字节码将值保存到操作。值堆栈是独立堆栈,而不是更大堆栈的帧。值堆栈是您要检查的内容。
  • @moos:对于 3.8 中的循环,它不再这样做了。 SETUP_LOOP 消失了。它的存在总是有点傻,因为循环处理所需的堆栈级别信息总是可以静态确定的。
【解决方案2】:

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

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

  • 修改 CPython 解释器或通过:
  • 通过byteplay 检测之前的字节码

感谢user2357112对问题难度的启发,并描述:

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

现在是潜在的解决方案...

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

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

从中使用Abstract Virtual Machine 来确定对于存储在last_i 中的给定偏移量,您将处于哪个入口位置。

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

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

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

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

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

【讨论】:

  • get_stack.py 的东西依赖于f_stacktop,所以除了挂起的生成器和协程框架的简单情况外,它对任何事情都不起作用。 (它还有一些不正确的字段类型声明。)
  • @user2357112supportsMonica 好的。这就是我写的原因:“可能在这里成为一个解决方案。”
【解决方案3】:

我尝试过这样做in this package。正如其他人指出的那样,主要困难在于确定 Python 堆栈的顶部。我尝试用一​​些启发式方法来做到这一点,我已经documented here

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

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

【讨论】:

    【解决方案4】:

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

    它是如何做到的,通过反汇编指令,并使用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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-05-04
      • 2011-08-03
      • 2013-05-02
      • 2021-05-02
      • 2015-02-14
      • 2020-07-15
      • 1970-01-01
      • 2012-10-07
      相关资源
      最近更新 更多