【问题标题】:Java call stack inspection and manipulationJava 调用堆栈检查和操作
【发布时间】:2023-03-05 00:01:01
【问题描述】:

我的问题是:是否可以(以任何方式)在运行时分析和修改调用堆栈(帧内容和堆栈内容)?

我正在寻找任何可能性 - 低级、不安全或内部 API、编写 C 扩展的可能性等。唯一的限制:它应该可以在标准运行时使用,无需调试或分析模式。这就是我正在研究“这可能吗?”,而不是“这是个好主意吗?”。

我想从一个框架中收集所有本地数据,将其存储在某处,然后从堆栈中删除该框架,并有可能在以后恢复它。这有效地为我们提供了 JVM 中的延续,它们将允许快速异步框架(如来自 python 的 gevent)和生成器构造(如来自 python 的那些)出现。

这看起来像是重复的问题,但我只找到了用“使用Thread.currentThread().getStackTrace()”或“应该使用调试工具来完成”回答的问题。有similiar question to mine,但它只在询问人想要做什么的情况下得到回答(从事异步计算),而我需要更一般的(面向java堆栈的)答案。 This question 也是类似的,但和以前一样,它侧重于并行化,答案也侧重于此。

我再说一遍:这是提出新语言功能提案过程中的研究步骤。我不想冒险破坏 JVM 中的任何东西 - 我正在寻找可能性,然后我将分析可能的风险并寻找它们。我知道手动操作堆栈是丑陋的,但是使用省略构造函数创建实例也是如此 - 它是对象的基础。肮脏的 hack 可能很脏,但它们可能有助于引入一些很酷的东西。

PS。我知道QuasarLightwolf 存在,但是如上所述,它们是以并发为中心的框架。

编辑

一点澄清:我正在寻找与未来 JVM 和库版本兼容的东西。最好我们谈论的是被认为是稳定的公共 API 的东西,但如果解决方案存在于内部,但几乎是标准的或在成为内部的标准之后(如 sun.misc.Unsafe) - 那也可以。如果它可以通过仅使用 C JVM API 的 C 扩展来实现 - 没关系。如果通过字节码操作可以做到这一点 - 那也可以(我认为 ASM 可以做到)。

【问题讨论】:

  • 我没用过,但也许你可以用JVM Tool Interface做点什么。它有一些functions to work with stack frames
  • 谢谢!这看起来很有希望,但不幸的是,它只允许弹出帧(甚至在弹出后不提供)并获取一些原始帧信息。这不足以存储和恢复完整的执行上下文。
  • 恐怕你不能使用超过 JVMTI 允许的堆栈。需要 JVM 支持才能执行您想要的操作。即使您碰巧使用直接内存访问成功地操作了堆栈,下一次 JVM 更新也可能很容易破坏这一点。
  • 我将稍微澄清一下未来的兼容性,以避免 cmets 和答案警告我有关 JVM 更新的信息;)不过,也许有人知道一些事情......
  • “为什么决定禁止它” - 不是。也没有决定允许它。直接操作堆栈的动机尚不清楚。如果你想在 Java 中继续,有other ways 来实现这一点。

标签: java jvm stack-trace


【解决方案1】:

不确定是否使用此工具,但您可以查看http://en.wikipedia.org/wiki/GNU_Debugger

GDB 提供了广泛的工具来跟踪和更改计算机程序的执行。用户可以监控和修改程序内部变量的值,甚至可以独立于程序的正常行为调用函数。

【讨论】:

  • 我知道调试器是什么。我特别指出,我正在寻找一种在没有任何特殊模式的情况下执行此操作的可能性。
【解决方案2】:

我认为有一种方法可以使用 JVMTI 实现您想要的。

尽管您不能直接做您想做的事(如上面的评论中所述),但您可以在运行时检测/重新定义方法(或整个类)。所以你可以定义每个方法来直接调用另一个方法来“恢复执行上下文”,一旦你有了你想要的堆栈,就用你的原始代码重新定义它们。

例如:假设您要恢复一个堆栈,其中只有 A 调用 B,B 调用 C。 加载A时,改代码直接调用B,一加载B,重新定义直接调用C;调用最顶层的方法(A);一旦 C 被调用(现在应该很快),将 A 和 B 重新定义为它们的原始代码。

如果涉及多个线程并且必须恢复参数值,它会变得有点复杂,但仍然可以使用 JVMTI。但是,这将值得另一个问题;-)。

希望这可以帮助。如果您需要澄清任何事情,请随时与我联系或发表评论。

编辑: 虽然我认为这是可行的,但我也认为这是很多(!!!)工作,尤其是当您想要恢复参数、局部变量和调用上下文(如 this 指针、持有的锁等)时。

按要求编辑:假设与上面相同的堆栈(A 调用 B 调用 C)。虽然 A、B 和 C 内部有任意代码,但只需像这样重新定义它们:

无效 A() { B(); } 无效 B() { C(); } 无效 C() { 重新定义(); }

到达重新定义方法后,立即使用原始代码重新定义所有类。然后你就有了你想要的堆栈。

【讨论】:

  • 如果我猜对了:你想在类加载时修改方法/类以用一些额外的代码来包装它?这太复杂了,我可以轻松地以至少 3 种不同的方式添加穷人方面。我的问题在于存储和恢复上下文 - 所有局部变量和参数、指令计数器(“我们在方法体中的位置?”、程序计数器,但用于单线程)等等。
  • 不要不包装它,只需将其定义为全新的,以便重现您想要的堆栈。然后,一旦你有了堆栈,你就用它们的原始代码重新定义所有方法并让它们继续。这样,您还可以恢复参数和 this 指针(通过您选择的序列化方法)和锁(只需通过添加监视器输入手动要求它们)。我看不到机会将最顶层的帧恢复到任何字节码位置,但第一个位置。
  • 坦率地说,我仍然看不到这一点。类加载如何与执行堆栈相关联?
  • 因为你可以修改代码来创建你想要的堆栈。让我举个例子:假设与上面相同的堆栈(A调用B调用C)。虽然 A、B 和 C 内部有任意代码,但只需像这样重新定义它们:

    void A() { B(); } 无效 B() { C(); } 无效 C() { 重新定义(); }

    到达 redefine 方法后,立即使用原始代码重新定义所有类。然后你就有了你想要的堆栈。

  • 哦,我现在明白了。那……嗯,这是有道理的,尽管这确实是很多工作。无论如何 - 享受吧,你可能刚刚获得了赏金;)
猜你喜欢
  • 1970-01-01
  • 2010-10-15
  • 2014-02-05
  • 2011-09-01
  • 2011-09-14
  • 1970-01-01
  • 1970-01-01
  • 2015-12-17
  • 1970-01-01
相关资源
最近更新 更多