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

Yoc*_*mer 15 .net c# callstack

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

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

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

  1. 调用代码时:

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

  2. 被叫目标代码中:

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

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

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

题:

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

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

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

Eri*_*ert 33

你自己解释过了.根据定义,"返回地址"会告诉您下一步的去向.

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

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

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

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

在延续传递风格中,根本没有堆叠,根本无法告诉你来自哪里; 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"之间的微不足道的区别:

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

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

http://msdn.com/vstudio/async


Vla*_*lad 7

请考虑以下代码:

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

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

void B()
{
}
Run Code Online (Sandbox Code Playgroud)

这里,函数最后要做的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|
----------------------------------------
Run Code Online (Sandbox Code Playgroud)

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

通过优化,调用堆栈就是

----------------------------------------
|address of the code calling A         |
----------------------------------------
Run Code Online (Sandbox Code Playgroud)

所以B直接回到Main.

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

  • 不是真的:堆栈跟踪显示_actual_堆栈.它如何知道"预期"堆栈?这些信息根本不存在. (3认同)