调用堆栈没有说“你来自哪里”,而是“你下一步去哪里”?

时间:2011-07-06 11:13:33

标签: c# .net callstack

在上一个问题(Get object call hierarchy)中,我得到了this interesting answer

  

调用堆栈不能告诉你你来自哪里。它是告诉你下一步的去向。

据我所知,在到达函数调用时,程序通常会执行以下操作:

  1. 调用代码:

    • 存储返回地址(在调用堆栈上)
    • 保存寄存器的状态(在调用堆栈上)
    • 写入将传递给函数的参数(在调用堆栈或寄存器中)
    • 跳转到目标功能

  2. 调用目标代码中:

    • 检索存储的变量(如果需要)

  3. 返回流程:撤消我们调用该函数时所执行的操作,即展开/弹出调用堆栈:

    • 从调用堆栈中删除局部变量
    • 从调用堆栈中删除函数变量
    • 恢复寄存器状态(我们之前存储的状态)
    • 跳转到返回地址(我们之前存储的地址)

  4. 问题:

    如何将其视为“告诉您下一步的位置”而非“告诉您来自哪里”

    C#的JIT或C#的运行时环境中是否有某些内容使得调用堆栈的工作方式不同?

    感谢您对有关调用堆栈描述的文档的任何指示 - 有大量关于传统调用堆栈如何工作的文档。

5 个答案:

答案 0 :(得分:33)

你自己解释过了。根据定义,“返回地址”会告诉您您下次去哪里

没有要求放在堆栈上的返回地址是调用现在所用方法的方法内的地址。 通常是,这确实使调试更容易。但是没有要求,返回地址是调用者内部的地址。如果这样做会使程序更快(或更小,或者无论其优化的是什么)而不改变其含义,那么优化器被允许 - 有时会 - 返回地址。

堆栈的目的是确保当这个子例程完成时, continuation - 接下来会发生什么 - 是正确的。堆栈的目的不是告诉你你来自哪里。通常情况下这是一个快乐的事故。

此外:堆栈只是 continuation activation 概念的实现细节。不要求两个概念都由同一堆栈实现;可能有两个堆栈,一个用于激活(局部变量),另一个用于连续(返回地址)。这样的体系结构显然更能抵抗恶意软件的堆栈粉碎攻击,因为返回地址远不及数据。

更有趣的是,没有要求任何堆栈!我们使用调用堆栈来实现延续,因为它们便于我们通常执行的编程:基于子程序的同步调用。我们可以选择将C#实现为“Continuation Passing Style”语言,其中continuation实际上是reified作为堆上的对象,而不是推送的一堆字节一百万字节的系统堆栈。然后,该对象从一个方法传递给另一个方法,其中没有一个使用任何堆栈。 (然后通过将每个方法分解为可能的许多委托来激活激活,每个委托都与激活对象相关联。)

在延续传球风格中,根本就没有堆叠,根本不知道你来自哪里; continuation对象没有该信息。它只知道你下一步的去向。

这可能看起来像是一个很高的理论,但是我们基本上是在下一个版本中将C#和VB变成继续传递样式语言;即将到来的“异步”功能只是继续以轻薄的伪装传递风格。在下一个版本中,如果使用异步功能,您将基本上放弃基于堆栈的编程;没有办法查看调用堆栈并知道你是如何到达这里的,因为堆栈通常是空的。

由于调用堆栈以外的其他内容对于很多人来说是一个难以理解的事情,因此实现了持续性。它当然适合我。但是一旦你得到它,它只是点击并且非常有意义。对于一个温和的介绍,这里有一些关于这个主题的文章:

CPS简介,以及JScript中的示例:

http://blogs.msdn.com/b/ericlippert/archive/2005/08/08/recursion-part-four-continuation-passing-style.aspx

http://blogs.msdn.com/b/ericlippert/archive/2005/08/11/recursion-part-five-more-on-cps.aspx

http://blogs.msdn.com/b/ericlippert/archive/2005/08/15/recursion-part-six-making-cps-work.aspx

这里有十几篇文章,首先深入探讨CPS,然后解释这一切是如何与即将到来的“异步”功能一起工作的。从底部开始:

http://blogs.msdn.com/b/ericlippert/archive/tags/async/

支持连续传递样式的语言通常有一个魔术控制流原语,称为“具有当前延续的呼叫”,或简称为“call / cc”。在这个stackoverflow问题中,我解释了“await”和“call / cc”之间的微不足道的差异:

How could the new async feature in c# 5.0 be implemented with call/cc?

要获得官方“文档”(一堆白皮书),以及C#和VB新的“异步等待”功能的预览版本,以及支持Q& A的论坛,请转到:

http://msdn.com/vstudio/async

答案 1 :(得分:7)

请考虑以下代码:

void Main()
{
    // do something
    A();
    // do something else
}

void A()
{
    // do some processing
    B();
}

void B()
{
}

这里,函数A正在做的最后一件事就是调用BA之后立即返回。聪明的优化器可能会将调用优化为B,并将其替换为 jump B的起始地址。 (不确定当前的C#编译器是否进行了这样的优化,但几乎所有的C ++编译器都这样做)。为什么会这样?因为堆栈中有A个调用者的地址,所以当B完成时,它不会返回A,而是直接返回A的调用者。

因此,您可以看到堆栈不一定包含有关执行来自何处​​的信息,而是包含应该去哪里的信息。

没有优化,在B内调用堆栈(为了清楚起见,我省略了局部变量和其他东西):

----------------------------------------
|address of the code calling A         |
----------------------------------------
|address of the return instruction in A|
----------------------------------------

所以从B返回A并立即退出`A.

通过优化,调用堆栈只是

----------------------------------------
|address of the code calling A         |
----------------------------------------

因此B会直接返回Main

在他的回答中,Eric提到了另一个(更复杂的)案例,其中堆栈信息不包含真正的调用者。

答案 2 :(得分:3)

Eric在帖子中说的是执行指针不需要知道它来自何处,只有在当前方法结束时它必须去的地方。表面上看这两件事看起来是一回事,但如果(例如)我们来自哪里的尾递归以及我们下一步的情况可能会分歧。

答案 3 :(得分:1)

这比你想象的更多。

在C中,完全可以让程序重写调用堆栈。实际上,这种技术是被称为return oriented programming的漏洞利用方式的基础。

我还用一种语言编写代码,让你直接控制callstack。你可以弹出调用你的函数,并在其位置推送其他函数。您可以复制调用堆栈顶部的项目,因此调用函数中的其余代码将执行两次,以及其他一些有趣的事情。事实上,对调用堆栈的直接操作是该语言提供的主要控制结构。 (挑战:任何人都可以通过此描述识别语言吗?)

它确实清楚地表明调用堆栈指示了你要去的地方,而不是你去过的地方。

答案 4 :(得分:0)

我想他试图说它告诉Called方法下一步该去哪里。

  • 方法A调用方法B.
  • 方法B完成,下一步在哪里?

它将被调用者方法地址从堆栈顶部弹出,然后转到那里。

所以方法B知道完成后要去哪里。方法B,并不关心它来自何处。