【问题标题】:How to find a C stack pointer associated with execution of a CPython stack frame如何找到与执行 CPython 堆栈帧相关的 C 堆栈指针
【发布时间】:2018-12-04 10:21:01
【问题描述】:

更新:如果它有助于缩小任何人的问题范围,那么这个问题实际上更多是关于 CPython API 以及我是否错过了某种方式来获取我需要的信息。我不是要求解决更广泛的问题,而是在解决更广泛的问题时,我遇到了一个关于 CPython 的特定问题,以及它是否提供了一种对我来说并不明显的方式来获取某些特定信息。我只标记了问题 ,因为它本质上需要一些 C 专业知识,但它不是关于 C 或特定架构/平台的一般问题。

另请参阅下面关于使用PyEval_SetTrace 的一种可能方法的注释,尽管我希望它们可能是一种更好的方法。作为另一个例子,有一个PyMain_GetArgcArgv 可以在这里解决问题,但只有如果Python 解释器是从python 可执行文件而不是嵌入的(这可能是一个可接受的限制)启动的。此外,PyMain_GetArgcArgv 也没有记录为 API 的一部分。


我希望能够找到与 Python 堆栈帧最密切相关的 C 堆栈帧的地址(即为该平台适当定义的 __builtin_frame_address(0))。特别是,我想找到与 Python 函数调用相关联的最外层框架(或靠近它的框架),以便在下面更好地定义。

总而言之,上下文是我正在包装一个 C 库,该库使用一个晦涩的自定义垃圾收集器,它需要一个指向堆栈底部的指针——至少只要有局部变量指向到应该被 GC 跟踪的对象。理想情况下,我可以标记堆栈的底部一次;在这种情况下,因为它被包装在一个 Python 模块中,所以向下到最外层的 Python 堆栈框架就足够了。最好的替代方法是在输入对库的调用时手动标记堆栈底部,但这并不理想,并且还需要修补库(可能需要任何一种方式),因为它目前只允许设置堆栈底部地址一次,在初始化函数期间。

Python 堆栈帧与 C 堆栈帧究竟是如何关联的,目前尚不明确,因为从技术上讲,两者之间没有固定的联系。但是,出于手头的实际目的,它将处于或接近(取决于编译器优化等)正在执行的帧的 PyEval_EvalFrameEx 调用(我对当前不在调用堆栈上的帧不感兴趣因为在那种情况下这显然是一个毫无意义的问题)。

这显然是非常特定于 CPython 的,这对我的目的来说是可以的。在这种情况下,从技术上讲,CPython PyFrameObject struct 实现不能在其成员之一上携带这样的信息是没有理由的,但据我所知,PyFrameObjects 上没有任何具体存储可以让我将其与 C 堆栈帧相关联。例如,就本应用程序而言,如果PyFrameObject 中的某些内容(例如f_cstack)的使用如下:

PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{
    ...
    f->f_executing = 1;
    f->f_cstack = &f;
    ...
}

这将工作 AFAICT——即使 f 通常在寄存器中传递,我的 gcc 将通过将 f 推入堆栈并将其地址存储在堆栈中来处理这样的代码。不幸的是,目前我找不到这样的东西。

我能想到的最好的主意是注册一个PyEval_SetTrace 处理程序,它会在进入Python 堆栈帧时被调用,从而让我有机会从那里围绕堆栈进行根植。但实际上对于手头的应用程序,我只需要能够找到“最外层”PyEval_EvalFrameEx 调用,任何正在运行的 Python 代码都会有一个调用。所以安装跟踪回调不一定能让我明白这一点,而且我不需要每个函数调用都需要额外的开销。

我担心目前没有很好的解决方案,但如果有的话会很方便。

(PS 我也只关心主堆栈,而不是线程,尽管任何适用于主线程的解决方案都可能在辅助线程上具有类似的解决方案)。

【问题讨论】:

  • 在哪个操作系统上?
  • 假设这是一个独立于操作系统的问题,因此它不能依赖特定的二进制格式或任何东西。我正在添加一个小更新。
  • 在实践中,它不是操作系统中立的
  • 可能是XY problem...您遇到的实际问题是什么?这应该在问题中提到
  • 这不是 XY 问题。实际问题还有其他(尽管不太令人满意)的解决方案,我只是好奇是否有人足够聪明地找到我无法解决问题的解决方案。感谢您尝试提供帮助,但 实际 问题如所述。

标签: c python c cpython


【解决方案1】:

一般而言,原则上,您可能无法始终按照自己的意愿行事(众所周知,在某些情况下,C 实现甚至可能不需要任何调用堆栈)。因为有时像GCC(或Clang)这样的编译器能够tail-call编译器optimizations(结合链接时优化,可能会产生令人惊讶的结果)。某些calling conventions 或编译模式(例如gcc -fomit-frame-pointer -m32 on 32 位x86)使call stack 的遍历变得困难(至少,没有附加 数据)。

在实践中,您应该研究使用 GNU backtrace 函数,甚至更好的是 Ian Taylor 的 libbacktrace。这个libbacktrace 库解析DWARF 调试信息(因此它可能是特定于Linux 的,可能不适用于Windows)。在 Linux 上,dladdr(3) 能够获得接近给定地址的符号名称。

因此,您最好编译您的主程序和 Python 运行时(可能还有其他库),并将 -g 标志传递给 gccg++(以获取 DWARF 调试信息),然后使用 libbacktrace .请记住,GCC 能够同时处理 both -g 和像 -O2 这样的优化标志。二进制文件或库的性能不会受到影响(因为优化是由 GCC 编译器完成的)。

为了寻找memory leaks(在一些评论中间接提到,但在问题本身中没有提到),可以使用一些工具(例如valgrind)。询问它们是否适合混合 Python + C 程序是另一个问题。

垃圾收集错误很难找到(我自己也写过几个 GC——特别是在我过时的 GCC MELT 和我的 bismon- 中,所以我凭经验说话;另请阅读 GC handbook)。将一个 GC 与另一个混合(Python 引用计数机制是一种 GC 机制)是痛苦和脆弱的。 在实践中使用inter-process communication 工具将您的软件拆分为多个进程可能更合理(这些是特定于操作系统的)。

由于CPythonfree software,您可以在fork 它内部添加libbacktrace 支持(从技术上讲,这样做应该相当容易)。

【讨论】:

  • 我意识到这是有问题的,“一般来说”,而且我认为尾调用优化在调用 Python 解释器时不会发挥太大作用(即使是这样,在这种情况下实际上也不是什么大问题)。这里的应用程序实际上不是调试相关的,而是垃圾收集相关的。我当然可以(并且确实)使用调试信息编译 Python,但我不能保证到处都是这样。
  • “将一个 GC 与另一个混合(Python refcounting 机制是一种 GC 机制)是痛苦和脆弱的。”在这种情况下,它已经很好地工作了,并且在 Python 包装器(用于其他 GC 跟踪的对象)和 Python 引用计数之间存在简单的确定性交互。事实上,将这些对象包装在 Python 对象中更容易,因为我们可以使用 refcounts 将这些对象标记为对其他 GC 来说是活动的。问题出在局部变量上。我们可能只是确保其他 GC 处理的所有对象都从某个池中返回,而不是从堆栈中返回。
  • 我已准备好面对面讨论(与您的计算机和电路板)。我什至可以在一两个小时内到达南巴黎(如果您通过电子邮件询问)。如果您提出要求,我愿意(仅在今天 2018 年 12 月 4 日)成为您的rubber duck
  • 谢谢你的好意,但我今天没空;我必须在一小时后去赴约,否则我可能会带你去。你看起来是个有趣的人,我相信我可以从他那里学到一两件事,但坦率地说,除非你是 Python 内部专家,否则这种互动让我没有兴趣进一步研究这个特定的问题;我可能会问 CPython 开发人员他们的想法,因为我真正要问的是 CPython 的特殊性。不过很想在其他时间见面和聊天。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-11-16
  • 2021-12-26
  • 2014-10-16
  • 2013-01-20
  • 2015-08-05
  • 1970-01-01
  • 2017-06-06
相关资源
最近更新 更多