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

在上一个问题( 获取对象调用层次结构 )中,我得到了这个有趣的答案 :

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

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

  1. 调用代码时:

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

    • 检索存储的变量(如果需要)
  3. 返回过程 :撤消我们调用函数时所做的操作,即展开/弹出调用堆栈:

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

题:

如何将其视为“告诉您下一步的去向”而不是“告诉您从哪里来”

在C#的JIT或C#的运行时环境中是否存在使得调用堆栈以不同方式工作的东西?

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

你自己解释过了。 根据定义,“返回地址”会告诉您下一步的位置

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

堆栈的目的是确保当子例程完成时,它的继续 – 接下来发生的事情 – 是正确的。 堆栈的目的不是告诉你你来自哪里。 它通常这样做是一个快乐的事故。

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

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

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

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

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

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,然后解释这一切是如何与即将到来的“异步”function一起工作的。 从底部开始:

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

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

如何使用call / cc实现c#5.0中的新异步function?

为了获得官方“文档”(一堆白皮书),以及C#和VB新的“异步等待”function的预览版本,以及支持问答的论坛,请访问:

http://msdn.com/vstudio/async

请考虑以下代码:

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

这里,函数A最后做的就是调用B A立即返回。 聪明的优化器可能会优化对B调用 ,只需跳转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提到了另一个(更复杂的)案例,其中堆栈信息不包含真正的调用者。

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

这比你想象的要多得多。

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

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

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

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

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

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

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